Больше Lua API в root, 2function

This commit is contained in:
DBotThePony 2023-03-26 20:52:48 +07:00
parent b0e978b2d1
commit 12232fbd18
Signed by: DBot
GPG Key ID: DCC23B5715498507
13 changed files with 2037 additions and 88 deletions

View File

@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.util.PathStack
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
inline fun <reified T : Any> ObjectRegistry(name: String, noinline key: (T) -> String, noinline intKey: ((T) -> Int)? = null): ObjectRegistry<T> { inline fun <reified T : Any> ObjectRegistry(name: String, noinline key: ((T) -> String)? = null, noinline intKey: ((T) -> Int)? = null): ObjectRegistry<T> {
return ObjectRegistry(T::class, name, key, intKey) return ObjectRegistry(T::class, name, key, intKey)
} }
@ -48,7 +48,7 @@ class RegistryObject<T : Any>(val value: T, private val json: JsonObject, val fi
} }
} }
class ObjectRegistry<T : Any>(val clazz: KClass<T>, val name: String, val key: (T) -> String, val intKey: ((T) -> Int)? = null) { class ObjectRegistry<T : Any>(val clazz: KClass<T>, val name: String, val key: ((T) -> String)? = null, val intKey: ((T) -> Int)? = null) {
private val objects = Object2ObjectOpenHashMap<String, RegistryObject<T>>() private val objects = Object2ObjectOpenHashMap<String, RegistryObject<T>>()
private val intObjects = Int2ObjectOpenHashMap<RegistryObject<T>>() private val intObjects = Int2ObjectOpenHashMap<RegistryObject<T>>()
@ -160,17 +160,19 @@ class ObjectRegistry<T : Any>(val clazz: KClass<T>, val name: String, val key: (
return pathStack(file.computeDirectory()) { return pathStack(file.computeDirectory()) {
val elem = gson.fromJson(file.reader(), JsonObject::class.java) val elem = gson.fromJson(file.reader(), JsonObject::class.java)
val value = gson.fromJson<T>(JsonTreeReader(elem), clazz.java) val value = gson.fromJson<T>(JsonTreeReader(elem), clazz.java)
add(RegistryObject(value, elem, file, gson, pathStack)) add(RegistryObject(value, elem, file, gson, pathStack), this.key?.invoke(value) ?: throw UnsupportedOperationException("No key mapper"))
} }
} }
fun add(value: T, json: JsonObject, file: IStarboundFile, gson: Gson, pathStack: PathStack): Boolean { fun add(value: T, json: JsonObject, file: IStarboundFile, gson: Gson, pathStack: PathStack): Boolean {
return add(RegistryObject(value, json, file, gson, pathStack)) return add(RegistryObject(value, json, file, gson, pathStack), this.key?.invoke(value) ?: throw UnsupportedOperationException("No key mapper"))
} }
private fun add(value: RegistryObject<T>): Boolean { fun add(value: T, json: JsonObject, file: IStarboundFile, gson: Gson, pathStack: PathStack, key: String): Boolean {
val key = this.key.invoke(value.value) return add(RegistryObject(value, json, file, gson, pathStack), key)
}
private fun add(value: RegistryObject<T>, key: String): Boolean {
val existing = objects.put(key, value) val existing = objects.put(key, value)
if (existing != null) { if (existing != null) {
@ -184,7 +186,7 @@ class ObjectRegistry<T : Any>(val clazz: KClass<T>, val name: String, val key: (
val intExisting = intObjects.put(intKey, value) val intExisting = intObjects.put(intKey, value)
if (intExisting != null) { if (intExisting != null) {
LOGGER.warn("Registry $name already has object with ID $intKey (new $key, old ${this.key.invoke(intExisting.value)})! Overwriting. (old originated from ${intExisting.file}, new originate from ${value.file}).") LOGGER.warn("Registry $name already has object with ID $intKey (new $key, old ${ objects.entries.firstOrNull { it.value === intExisting }?.key })! Overwriting. (old originated from ${intExisting.file}, new originate from ${value.file}).")
} }
return existing != null || intExisting != null return existing != null || intExisting != null

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.common.cache.CacheBuilder
import com.google.common.collect.Interner import com.google.common.collect.Interner
import com.google.common.collect.Interners import com.google.common.collect.Interners
import com.google.gson.* import com.google.gson.*
@ -7,6 +8,7 @@ import com.google.gson.internal.bind.JsonTreeReader
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.stb.STBImage
import ru.dbotthepony.kstarbound.api.ISBFileLocator import ru.dbotthepony.kstarbound.api.ISBFileLocator
import ru.dbotthepony.kstarbound.api.IStarboundFile import ru.dbotthepony.kstarbound.api.IStarboundFile
import ru.dbotthepony.kstarbound.api.NonExistingFile import ru.dbotthepony.kstarbound.api.NonExistingFile
@ -63,20 +65,23 @@ import ru.dbotthepony.kstarbound.util.PathStack
import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.SBPattern
import ru.dbotthepony.kstarbound.util.WriteOnce import ru.dbotthepony.kstarbound.util.WriteOnce
import ru.dbotthepony.kstarbound.util.traverseJsonPath import ru.dbotthepony.kstarbound.util.traverseJsonPath
import ru.dbotthepony.kvector.vector.nint.Vector2i
import java.io.* import java.io.*
import java.text.DateFormat import java.text.DateFormat
import java.time.Duration
import java.util.* import java.util.*
import java.util.function.BiConsumer import java.util.function.BiConsumer
import java.util.function.BinaryOperator import java.util.function.BinaryOperator
import java.util.function.Function import java.util.function.Function
import java.util.function.Supplier import java.util.function.Supplier
import java.util.stream.Collector import java.util.stream.Collector
import kotlin.NoSuchElementException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class Starbound : ISBFileLocator { class Starbound : ISBFileLocator {
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
val pathStack = PathStack(Starbound.STRINGS) val pathStack = PathStack(STRINGS)
private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId) private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId)
val tiles = _tiles.view val tiles = _tiles.view
@ -108,6 +113,12 @@ class Starbound : ISBFileLocator {
private val _techs = ObjectRegistry("techs", TechDefinition::name) private val _techs = ObjectRegistry("techs", TechDefinition::name)
val techs = _techs.view val techs = _techs.view
private val _jsonFunctions = ObjectRegistry<JsonFunction>("json functions")
val jsonFunctions = _jsonFunctions.view
private val _json2Functions = ObjectRegistry<Json2Function>("json 2functions")
val json2Functions = _json2Functions.view
val gson: Gson = with(GsonBuilder()) { val gson: Gson = with(GsonBuilder()) {
serializeNulls() serializeNulls()
setDateFormat(DateFormat.LONG) setDateFormat(DateFormat.LONG)
@ -115,7 +126,12 @@ class Starbound : ISBFileLocator {
setPrettyPrinting() setPrettyPrinting()
registerTypeAdapter(InternedStringAdapter(STRINGS)) registerTypeAdapter(InternedStringAdapter(STRINGS))
registerTypeAdapter(InternedJsonElementAdapter(STRINGS))
InternedJsonElementAdapter(STRINGS).also {
registerTypeAdapter(it)
registerTypeAdapter(it.arrays)
registerTypeAdapter(it.objects)
}
registerTypeAdapter(Nothing::class.java, NothingAdapter) registerTypeAdapter(Nothing::class.java, NothingAdapter)
@ -160,6 +176,7 @@ class Starbound : ISBFileLocator {
registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER) registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
registerTypeAdapter(JsonFunction.Companion) registerTypeAdapter(JsonFunction.Companion)
registerTypeAdapterFactory(Json2Function.Companion)
// Общее // Общее
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
@ -193,6 +210,12 @@ class Starbound : ISBFileLocator {
val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson) val atlasRegistry = AtlasConfiguration.Registry(this, pathStack, gson)
private val imageCache = CacheBuilder.newBuilder()
.concurrencyLevel(8)
.softValues()
.expireAfterAccess(Duration.ofMinutes(10))
.build<String, ImageData>()
fun item(name: String): ItemDescriptor { fun item(name: String): ItemDescriptor {
return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY) return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY)
} }
@ -271,11 +294,42 @@ class Starbound : ISBFileLocator {
} }
val read = file.readToString() val read = file.readToString()
it.load(read, chunkName = file.computeFullPath()) it.load(read, chunkName = "@" + file.computeFullPath())
it.call() it.call()
} }
fun expose(state: LuaState) { fun imageData(path: String): ImageData {
return imageCache.get(path) {
val file = locate(path)
if (!file.exists) {
throw FileNotFoundException("No such file $file")
}
if (!file.isFile) {
throw IllegalStateException("File $file is a directory")
}
val getWidth = intArrayOf(0)
val getHeight = intArrayOf(0)
val components = intArrayOf(0)
val data = STBImage.stbi_load_from_memory(
file.readDirect(),
getWidth, getHeight,
components, 0
) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted")
ImageData(data, getWidth[0], getHeight[0], components[0])
}
}
fun imageSize(path: String): Vector2i {
val image = imageData(path)
return Vector2i(image.width, image.height)
}
fun pushLuaAPI(state: LuaState) {
state.pushWeak(this) { args -> state.pushWeak(this) { args ->
luaRequire(args.lua, args) luaRequire(args.lua, args)
0 0
@ -286,18 +340,62 @@ class Starbound : ISBFileLocator {
state.pushTable() state.pushTable()
state.storeGlobal("root") state.storeGlobal("root")
state.loadGlobal("root") state.loadGlobal("root")
val root = state.stackTop
state.push("assetJson") state.setTableFunction("assetJson", this) {args ->
state.pushWeak(this) {args ->
args.lua.push(loadJsonAsset(args.getString())) args.lua.push(loadJsonAsset(args.getString()))
1 1
} }
state.setTableValue(root) state.setTableFunction("makeCurrentVersionedJson", this) {args ->
TODO("makeCurrentVersionedJson")
}
state.setTableFunction("loadVersionedJson", this) {args ->
TODO("loadVersionedJson")
}
state.setTableFunction("evalFunction", this) {args ->
val name = args.getString()
val fn = jsonFunctions[name] ?: throw NoSuchElementException("No such function $name")
args.push(fn.value.evaluate(args.getDouble()))
1
}
state.setTableFunction("evalFunction2", this) {args ->
val name = args.getString()
val fn = json2Functions[name] ?: throw NoSuchElementException("No such 2function $name")
args.push(fn.value.evaluate(args.getDouble(), args.getDouble()))
1
}
state.setTableFunction("imageSize", this) {args ->
val (width, height) = imageSize(args.getString())
with(args.lua) {
pushTable(hashSize = 2)
val table = stackTop
setTableValue("width", width)
setTableValue("height", height)
setTableValue("x", width)
setTableValue("y", height)
push(1)
push(width)
setTableValue(table)
push(2)
push(height)
setTableValue(table)
}
1
}
state.pop() state.pop()
state.load(polyfill, "starbound.jar!/scripts/polyfill.lua") state.load(polyfill, "@starbound.jar!/scripts/polyfill.lua")
state.call() state.call()
} }
@ -488,7 +586,9 @@ class Starbound : ISBFileLocator {
callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info)) callback(false, false, "Finished building file index in ${System.currentTimeMillis() - time}ms".also(logger::info))
loadStage(callback, this::loadItemDefinitions, "item definitions") loadStage(callback, { loadItemDefinitions(it, ext2files) }, "item definitions")
loadStage(callback, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions")
loadStage(callback, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions")
loadStage(callback, _tiles, ext2files["material"] ?: listOf()) loadStage(callback, _tiles, ext2files["material"] ?: listOf())
loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf()) loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf())
@ -539,26 +639,28 @@ class Starbound : ISBFileLocator {
} }
} }
private fun loadItemDefinitions(callback: (String) -> Unit) { private fun loadItemDefinitions(callback: (String) -> Unit, files: Map<String, Collection<IStarboundFile>>) {
val files = linkedMapOf( val fileMap = mapOf(
".item" to ItemPrototype::class.java, "item" to ItemPrototype::class.java,
".currency" to CurrencyItemPrototype::class.java, "currency" to CurrencyItemPrototype::class.java,
".liqitem" to LiquidItemPrototype::class.java, "liqitem" to LiquidItemPrototype::class.java,
".matitem" to MaterialItemPrototype::class.java, "matitem" to MaterialItemPrototype::class.java,
".flashlight" to FlashlightPrototype::class.java, "flashlight" to FlashlightPrototype::class.java,
".harvestingtool" to HarvestingToolPrototype::class.java, "harvestingtool" to HarvestingToolPrototype::class.java,
".head" to HeadArmorItemPrototype::class.java, "head" to HeadArmorItemPrototype::class.java,
".chest" to ChestArmorItemPrototype::class.java, "chest" to ChestArmorItemPrototype::class.java,
".legs" to LegsArmorItemPrototype::class.java, "legs" to LegsArmorItemPrototype::class.java,
".back" to BackArmorItemPrototype::class.java, "back" to BackArmorItemPrototype::class.java,
) )
for (fs in fileSystems) { for ((ext, clazz) in fileMap) {
for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.keys.any { f.name.endsWith(it) } }) { val fileList = files[ext] ?: continue
for (listedFile in fileList) {
try { try {
callback("Loading $listedFile") callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
val def: ItemPrototype = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), files.entries.first { listedFile.name.endsWith(it.key) }.value) } val def: ItemPrototype = pathStack(listedFile.computeDirectory()) { gson.fromJson(JsonTreeReader(json), clazz) }
_items.add(def, json, listedFile, gson, pathStack) _items.add(def, json, listedFile, gson, pathStack)
} catch (err: Throwable) { } catch (err: Throwable) {
logger.error("Loading item definition file $listedFile", err) logger.error("Loading item definition file $listedFile", err)
@ -571,6 +673,46 @@ class Starbound : ISBFileLocator {
} }
} }
private fun loadJsonFunctions(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
try {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
for ((k, v) in json.entrySet()) {
val fn = gson.fromJson<JsonFunction>(JsonTreeReader(v), JsonFunction::class.java)
_jsonFunctions.add(fn, json, listedFile, gson, pathStack, k)
}
} catch (err: Throwable) {
logger.error("Loading json function definition file $listedFile", err)
}
if (terminateLoading) {
return
}
}
}
private fun loadJson2Functions(callback: (String) -> Unit, files: Collection<IStarboundFile>) {
for (listedFile in files) {
try {
callback("Loading $listedFile")
val json = gson.fromJson(listedFile.reader(), JsonObject::class.java)
for ((k, v) in json.entrySet()) {
val fn = gson.fromJson<Json2Function>(JsonTreeReader(v), Json2Function::class.java)
_json2Functions.add(fn, json, listedFile, gson, pathStack, k)
}
} catch (err: Throwable) {
logger.error("Loading json 2function definition file $listedFile", err)
}
if (terminateLoading) {
return
}
}
}
companion object { companion object {
/** /**
* Глобальный [Interner] для [String] * Глобальный [Interner] для [String]

View File

@ -175,7 +175,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
input.installCallback(window) input.installCallback(window)
GLFW.glfwMakeContextCurrent(window) GLFW.glfwMakeContextCurrent(window)
gl = GLStateTracker(starbound) gl = GLStateTracker(this)
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h -> GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
if (w == 0 || h == 0) { if (w == 0 || h == 0) {

View File

@ -5,6 +5,7 @@ import org.lwjgl.opengl.GL
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import org.lwjgl.opengl.GLCapabilities import org.lwjgl.opengl.GLCapabilities
import ru.dbotthepony.kstarbound.api.ISBFileLocator import ru.dbotthepony.kstarbound.api.ISBFileLocator
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.freetype.FreeType import ru.dbotthepony.kstarbound.client.freetype.FreeType
import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException import ru.dbotthepony.kstarbound.client.freetype.InvalidArgumentException
import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
@ -116,7 +117,7 @@ private class TexturesTracker(maxValue: Int) : ReadWriteProperty<GLStateTracker,
} }
@Suppress("PropertyName", "unused") @Suppress("PropertyName", "unused")
class GLStateTracker(val locator: ISBFileLocator) { class GLStateTracker(val client: StarboundClient) {
private fun isMe(state: GLStateTracker?) { private fun isMe(state: GLStateTracker?) {
if (state != null && state != this) { if (state != null && state != this) {
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)") throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
@ -441,18 +442,18 @@ class GLStateTracker(val locator: ISBFileLocator) {
ensureSameThread() ensureSameThread()
if (missingTexture == null) { if (missingTexture == null) {
missingTexture = newTexture(missingTexturePath).upload(locator.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also { missingTexture = newTexture(missingTexturePath).upload(client.starbound.readDirect(missingTexturePath), GL_RGBA, GL_RGBA).generateMips().also {
it.textureMinFilter = GL_NEAREST it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST
} }
} }
return named2DTextures.computeIfAbsent(path) { return named2DTextures.computeIfAbsent(path) {
if (!locator.exists(path)) { if (!client.starbound.exists(path)) {
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
missingTexture!! missingTexture!!
} else { } else {
newTexture(path).upload(locator.readDirect(path)).generateMips().also { newTexture(path).upload(client.starbound.imageData(path)).generateMips().also {
it.textureMinFilter = GL_NEAREST it.textureMinFilter = GL_NEAREST
it.textureMagFilter = GL_NEAREST it.textureMagFilter = GL_NEAREST
} }

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.client.gl
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.lwjgl.opengl.GL46.* import org.lwjgl.opengl.GL46.*
import org.lwjgl.stb.STBImage import org.lwjgl.stb.STBImage
import ru.dbotthepony.kstarbound.io.ImageData
import ru.dbotthepony.kvector.vector.nint.Vector2i import ru.dbotthepony.kvector.vector.nint.Vector2i
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -248,6 +249,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
return this return this
} }
fun upload(data: ImageData): GLTexture2D {
state.ensureSameThread()
val bufferFormat = when (val numChannels = data.amountOfChannels) {
1 -> GL_R
2 -> GL_RG
3 -> GL_RGB
4 -> GL_RGBA
else -> throw IllegalArgumentException("Weird amount of channels in file: $numChannels")
}
upload(bufferFormat, data.width, data.height, bufferFormat, GL_UNSIGNED_BYTE, data.data)
return this
}
var isValid = true var isValid = true
private set private set

View File

@ -1,66 +1,118 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.io.json.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.io.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.io.json.builder.EnumAdapter
import ru.dbotthepony.kstarbound.io.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.io.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
import ru.dbotthepony.kvector.util.linearInterpolation
import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.ndouble.Vector2d
enum class JsonFunctionInterpolation(vararg aliases: String) : IStringSerializable { enum class JsonFunctionInterpolation {
LINEAR; LINEAR {
override fun interpolate(t: Double, a: Double, b: Double): Double {
return linearInterpolation(t, a, b)
}
};
private val aliases: Array<String> = Array(aliases.size) { aliases[it].uppercase() } abstract fun interpolate(t: Double, a: Double, b: Double): Double
override fun match(name: String): Boolean {
for (alias in aliases)
if (name == alias)
return true
return name == this.name
}
override fun write(out: JsonWriter) {
out.value(this.name)
}
} }
enum class JsonFunctionConstraint(vararg aliases: String) : IStringSerializable { enum class JsonFunctionConstraint {
CLAMP; CLAMP {
override fun process(x: Double, lower: Double, upper: Double): Double {
return x.coerceIn(lower, upper)
}
};
private val aliases: Array<String> = Array(aliases.size) { aliases[it].uppercase() } abstract fun process(x: Double, lower: Double, upper: Double): Double
}
override fun match(name: String): Boolean { data class DoubleRange(val start: Double, val end: Double) : Comparable<Double> {
for (alias in aliases) init {
if (name == alias) require(start <= end) { "start <= end: $start <= $end" }
return true
return name == this.name
} }
override fun write(out: JsonWriter) { val length = end - start
out.value(this.name)
operator fun contains(value: Double): Boolean {
return value in start .. end
}
fun determine(value: Double): Double {
if (value < start) {
return 0.0
} else if (value > end) {
return 1.0
} else {
return (value - start) / length
}
}
override fun compareTo(other: Double): Int {
if (other < start) {
return 1
} else if (other > end) {
return -1
} else {
return 0
}
} }
} }
class JsonFunction( class JsonFunction(
val interpolation: JsonFunctionInterpolation, val interpolation: JsonFunctionInterpolation,
val constraint: JsonFunctionConstraint, val constraint: JsonFunctionConstraint,
val ranges: List<Vector2d> ranges: Collection<Vector2d>
) { ) {
val ranges: ImmutableList<Vector2d> = ranges.stream().sorted { o1, o2 -> o1.x.compareTo(o2.x) }.collect(ImmutableList.toImmutableList())
val lowerBound = this.ranges.first().x
val upperBound = this.ranges.last().x
fun evaluate(input: Double): Double {
val x = constraint.process(input, lowerBound, upperBound)
when (interpolation) {
JsonFunctionInterpolation.LINEAR -> {
if (x < ranges[0].x) {
return ranges[0].y
}
for (i in 0 until ranges.size - 1) {
val a = ranges[i]
val b = ranges[i + 1]
if (x in a.x .. b.x) {
return linearInterpolation((x - a.x) / (b.x - a.x), a.y, b.y)
}
}
return ranges.last().y
}
}
}
companion object : TypeAdapter<JsonFunction>() { companion object : TypeAdapter<JsonFunction>() {
val CONSTRAINT_ADAPTER = EnumAdapter(JsonFunctionConstraint::class) val CONSTRAINT_ADAPTER = EnumAdapter(JsonFunctionConstraint::class)
val INTERPOLATION_ADAPTER = EnumAdapter(JsonFunctionInterpolation::class) val INTERPOLATION_ADAPTER = EnumAdapter(JsonFunctionInterpolation::class)
override fun write(out: JsonWriter, value: JsonFunction) { override fun write(out: JsonWriter, value: JsonFunction?) {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun read(reader: JsonReader): JsonFunction { override fun read(reader: JsonReader): JsonFunction? {
if (reader.peek() == JsonToken.NULL)
return null
reader.beginArray() reader.beginArray()
val interpolation = INTERPOLATION_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing interpolation") val interpolation = INTERPOLATION_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing interpolation")
@ -78,3 +130,196 @@ class JsonFunction(
} }
} }
} }
class Json2Function(
val interpolation: JsonFunctionInterpolation,
val constraint: JsonFunctionConstraint,
xRanges: List<Ranges>, // значения [a, b, c, d]
yRanges: List<Ranges>, // значения [a, [b, c, d]]
) {
init {
require(xRanges.isNotEmpty()) { "X ranges are empty" }
require(yRanges.isNotEmpty()) { "Y ranges are empty" }
}
val xRanges: ImmutableList<Ranges> = xRanges.stream().sorted { o1, o2 -> o1.edge.compareTo(o2.edge) }.collect(ImmutableList.toImmutableList())
val yRanges: ImmutableList<Ranges> = yRanges.stream().sorted { o1, o2 -> o1.edge.compareTo(o2.edge) }.collect(ImmutableList.toImmutableList())
data class Ranges(val interpolation: JsonFunctionInterpolation, val edge: Double, val ranges: ImmutableList<DoubleRange>) {
val step = 1.0 / ranges.size.toDouble()
fun map(value: Double): Double {
if (value <= ranges.first().start) {
return 0.0
} else if (value >= ranges.last().end) {
return 1.0
}
var i = 0.0
for (range in ranges) {
if (value in range) {
return i + range.determine(value) * step
} else {
i += step
}
}
return i
}
fun unmap(t: Double): Double {
if (t <= 0.0) {
return ranges.first().start
} else if (t >= 1.0) {
return ranges.last().end
}
val unmap = t / step
val range = ranges[unmap.toInt()]
return interpolation.interpolate(unmap % 1.0, range.start, range.end)
}
}
fun evaluate(x: Double, y: Double): Double {
var xT = 0.0
if (xRanges.size == 1) {
xT = xRanges[0].map(x)
} else {
var hit = false
for (i in 1 until xRanges.size) {
val a = xRanges[i - 1]
val b = xRanges[i]
if (x in a.edge .. b.edge) {
xT = interpolation.interpolate((x - a.edge) / (b.edge - a.edge), a.map(x), b.map(x))
hit = true
break
}
}
if (!hit) {
xT = xRanges.last().map(x)
}
}
if (yRanges.size == 1) {
return yRanges[0].unmap(xT)
} else {
for (i in 1 until yRanges.size) {
val a = yRanges[i - 1]
val b = yRanges[i]
if (y in a.edge .. b.edge) {
return interpolation.interpolate((y - a.edge) / (b.edge - a.edge), a.unmap(xT), b.unmap(xT))
}
}
return yRanges.last().unmap(xT)
}
}
companion object : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == Json2Function::class.java) {
return object : TypeAdapter<Json2Function>() {
val elements = gson.getAdapter(JsonArray::class.java)
override fun write(out: JsonWriter, value: Json2Function?) {
TODO("Not yet implemented")
}
override fun read(reader: JsonReader): Json2Function? {
if (reader.peek() == JsonToken.NULL)
return null
reader.beginArray()
val interpolation = JsonFunction.INTERPOLATION_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing interpolation")
val constraint = JsonFunction.CONSTRAINT_ADAPTER.read(reader) ?: throw JsonSyntaxException("Missing constraint")
val xRanges = ArrayList<Ranges>()
val yRanges = ArrayList<Ranges>()
val elements = elements.read(reader)
for ((i, row) in elements.withIndex()) {
if (row !is JsonArray) {
throw JsonSyntaxException("Invalid function ranges at row $i")
}
if (row.size() < 2) {
throw JsonSyntaxException("Row at $i should have at least 2 elements")
}
val edge = row[0].asDouble
if (row[1] is JsonArray) {
val ySource = row[1].asJsonArray
if (ySource.size() < 2) {
throw JsonSyntaxException("Y ranges at row $i should have at least 2 elements")
}
val ranges = ImmutableList.Builder<DoubleRange>()
var prev = ySource[0].asDouble
for (elem in 1 until ySource.size()) {
val v = ySource[elem].asDouble
ranges.add(DoubleRange(prev, v))
prev = v
}
yRanges.add(Ranges(interpolation, edge, ranges.build()))
} else {
if (row.size() < 3) {
throw JsonSyntaxException("Row at $i should have at least 3 elements")
}
val ranges = ImmutableList.Builder<DoubleRange>()
var prev = row[1].asDouble
for (elem in 2 until row.size()) {
val v = row[elem].asDouble
ranges.add(DoubleRange(prev, v))
prev = v
}
xRanges.add(Ranges(interpolation, edge, ranges.build()))
}
}
reader.endArray()
if (xRanges.isEmpty()) {
throw JsonSyntaxException("No X ranges")
}
if (yRanges.isEmpty()) {
throw JsonSyntaxException("No Y ranges")
}
val size = xRanges.first().ranges.size
var broken = xRanges.firstOrNull { it.ranges.size != size }
if (broken != null) {
throw JsonSyntaxException("One or more of X ranges are having different size (found ${broken.ranges.size}, expected $size)")
}
broken = yRanges.firstOrNull { it.ranges.size != size }
if (broken != null) {
throw JsonSyntaxException("One or more of Y ranges are having different size (found ${broken.ranges.size}, expected $size)")
}
return Json2Function(interpolation, constraint, xRanges, yRanges)
}
} as TypeAdapter<T>
}
return null
}
}
}

View File

@ -0,0 +1,54 @@
package ru.dbotthepony.kstarbound.io
import org.lwjgl.stb.STBImage
import org.lwjgl.system.MemoryUtil.memAddress
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
import ru.dbotthepony.kvector.vector.nint.Vector2i
import java.lang.ref.Cleaner
import java.nio.ByteBuffer
private val cleaner = Cleaner.create { Thread(it, "STB Image Cleaner") }
class ImageData(val data: ByteBuffer, val width: Int, val height: Int, val amountOfChannels: Int) {
init {
val address = memAddress(data)
cleaner.register(data) { STBImage.nstbi_image_free(address) }
check(width >= 0) { "Invalid width $width" }
check(height >= 0) { "Invalid height $height" }
check(amountOfChannels in 1 .. 4) { "Unknown number of channels $amountOfChannels" }
}
operator fun get(x: Int, y: Int): Int {
require(x in 0 until width) { "X out of bounds: $x" }
require(y in 0 until height) { "Y out of bounds: $y" }
val offset = y * width * amountOfChannels + x * amountOfChannels
when (amountOfChannels) {
4 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8) or
data[offset + 2].toInt().shl(16) or
data[offset + 3].toInt().shl(24)
3 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8) or
data[offset + 2].toInt().shl(16)
2 -> return data[offset].toInt() or
data[offset + 1].toInt().shl(8)
1 -> return data[offset].toInt()
else -> throw IllegalStateException()
}
}
fun worldSpaces(pos: Vector2d, spaceScan: Double, flip: Boolean): List<Vector2i> {
when (amountOfChannels) {
3 -> TODO()
4 -> TODO()
else -> throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels")
}
}
}

View File

@ -24,24 +24,46 @@ class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAda
JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(`in`.nextString())) JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(`in`.nextString()))
JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE
JsonToken.NULL -> JsonNull.INSTANCE JsonToken.NULL -> JsonNull.INSTANCE
JsonToken.BEGIN_ARRAY -> { JsonToken.BEGIN_ARRAY -> arrays.read(`in`)
val output = JsonArray() JsonToken.BEGIN_OBJECT -> objects.read(`in`)
`in`.beginArray()
while (`in`.hasNext()) { output.add(read(`in`)) }
`in`.endArray()
output
}
JsonToken.BEGIN_OBJECT -> {
val output = JsonObject()
`in`.beginObject()
while (`in`.hasNext()) { output.add(stringInterner.intern(`in`.nextName()), read(`in`)) }
`in`.endObject()
output
}
else -> throw IllegalArgumentException(p.toString()) else -> throw IllegalArgumentException(p.toString())
} }
} }
val objects = object : TypeAdapter<JsonObject>() {
override fun write(out: JsonWriter, value: JsonObject?) {
return TypeAdapters.JSON_ELEMENT.write(out, value)
}
override fun read(`in`: JsonReader): JsonObject? {
if (`in`.peek() == JsonToken.NULL)
return null
val output = JsonObject()
`in`.beginObject()
while (`in`.hasNext()) { output.add(stringInterner.intern(`in`.nextName()), this@InternedJsonElementAdapter.read(`in`)) }
`in`.endObject()
return output
}
}
val arrays = object : TypeAdapter<JsonArray>() {
override fun write(out: JsonWriter, value: JsonArray?) {
return TypeAdapters.JSON_ELEMENT.write(out, value)
}
override fun read(`in`: JsonReader): JsonArray? {
if (`in`.peek() == JsonToken.NULL)
return null
val output = JsonArray()
`in`.beginArray()
while (`in`.hasNext()) { output.add(this@InternedJsonElementAdapter.read(`in`)) }
`in`.endArray()
return output
}
}
companion object { companion object {
val TRUE = JsonPrimitive(true) val TRUE = JsonPrimitive(true)
val FALSE = JsonPrimitive(false) val FALSE = JsonPrimitive(false)

View File

@ -526,6 +526,15 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
if (position > this.top) return null if (position > this.top) return null
return this.getBoolOrNil(position) return this.getBoolOrNil(position)
} }
fun push() = this@LuaState.push()
fun push(value: Int) = this@LuaState.push(value)
fun push(value: Long) = this@LuaState.push(value)
fun push(value: Double) = this@LuaState.push(value)
fun push(value: Float) = this@LuaState.push(value)
fun push(value: Boolean) = this@LuaState.push(value)
fun push(value: String?) = this@LuaState.push(value)
fun push(value: JsonElement?) = this@LuaState.push(value)
} }
/** /**

View File

@ -13,7 +13,7 @@ private val messageHandlerLua by lazy { loadInternalScript("message_handler") }
private val configLua by lazy { loadInternalScript("config") } private val configLua by lazy { loadInternalScript("config") }
fun LuaState.exposeConfig(config: JsonElement) { fun LuaState.exposeConfig(config: JsonElement) {
load(configLua, "starbound.jar!/scripts/config.lua") load(configLua, "@starbound.jar!/scripts/config.lua")
call() call()
loadGlobal("config") loadGlobal("config")
@ -30,7 +30,7 @@ fun LuaState.exposeConfig(config: JsonElement) {
class MessageHandler(val lua: LuaState) { class MessageHandler(val lua: LuaState) {
init { init {
lua.load(messageHandlerLua, "starbound.jar!/scripts/message_handler.lua") lua.load(messageHandlerLua, "@starbound.jar!/scripts/message_handler.lua")
lua.call() lua.call()
lua.loadGlobal("message") lua.loadGlobal("message")

View File

@ -196,14 +196,14 @@ class Avatar(val starbound: Starbound, val uniqueId: UUID) {
* Triggers an immediate cleanup of the player's inventory, removing item stacks with 0 quantity. May rarely be required in special cases of making several sequential modifications to the player's inventory within a single tick. * Triggers an immediate cleanup of the player's inventory, removing item stacks with 0 quantity. May rarely be required in special cases of making several sequential modifications to the player's inventory within a single tick.
*/ */
fun cleanupItems() { fun cleanupItems() {
TODO()
} }
/** /**
* Adds the specified item to the player's inventory. * Adds the specified item to the player's inventory.
*/ */
fun giveItem(descriptor: ItemDescriptor) { fun giveItem(descriptor: ItemDescriptor) {
TODO()
} }
/** /**
@ -297,7 +297,7 @@ class Avatar(val starbound: Starbound, val uniqueId: UUID) {
} }
} }
fun expose(lua: LuaState) { fun pushLuaAPI(lua: LuaState) {
lua.load(playerLua) lua.load(playerLua)
lua.pushTable() lua.pushTable()

View File

@ -120,8 +120,8 @@ class QuestInstance(
lua.pop() lua.pop()
avatar.starbound.expose(lua) avatar.starbound.pushLuaAPI(lua)
avatar.expose(lua) avatar.pushLuaAPI(lua)
lua.exposeConfig(template.scriptConfig) lua.exposeConfig(template.scriptConfig)
} }
@ -170,7 +170,7 @@ class QuestInstance(
} }
try { try {
lua.load(script.readToString(), template.script.fullPath) lua.load(script.readToString(), "@" + template.script.fullPath)
} catch(err: Throwable) { } catch(err: Throwable) {
LOGGER.error("Error loading Lua code for quest ${descriptor.questId}", err) LOGGER.error("Error loading Lua code for quest ${descriptor.questId}", err)
return false return false

File diff suppressed because it is too large Load Diff