Больше Lua API в root, 2function
This commit is contained in:
parent
b0e978b2d1
commit
12232fbd18
@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.util.PathStack
|
||||
import java.util.*
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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 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()) {
|
||||
val elem = gson.fromJson(file.reader(), JsonObject::class.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 {
|
||||
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 {
|
||||
val key = this.key.invoke(value.value)
|
||||
fun add(value: T, json: JsonObject, file: IStarboundFile, gson: Gson, pathStack: PathStack, key: String): Boolean {
|
||||
return add(RegistryObject(value, json, file, gson, pathStack), key)
|
||||
}
|
||||
|
||||
private fun add(value: RegistryObject<T>, key: String): Boolean {
|
||||
val existing = objects.put(key, value)
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
@ -1,5 +1,6 @@
|
||||
package ru.dbotthepony.kstarbound
|
||||
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.collect.Interner
|
||||
import com.google.common.collect.Interners
|
||||
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.Object2ObjectOpenHashMap
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.lwjgl.stb.STBImage
|
||||
import ru.dbotthepony.kstarbound.api.ISBFileLocator
|
||||
import ru.dbotthepony.kstarbound.api.IStarboundFile
|
||||
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.WriteOnce
|
||||
import ru.dbotthepony.kstarbound.util.traverseJsonPath
|
||||
import ru.dbotthepony.kvector.vector.nint.Vector2i
|
||||
import java.io.*
|
||||
import java.text.DateFormat
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.BinaryOperator
|
||||
import java.util.function.Function
|
||||
import java.util.function.Supplier
|
||||
import java.util.stream.Collector
|
||||
import kotlin.NoSuchElementException
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class Starbound : ISBFileLocator {
|
||||
private val logger = LogManager.getLogger()
|
||||
|
||||
val pathStack = PathStack(Starbound.STRINGS)
|
||||
val pathStack = PathStack(STRINGS)
|
||||
|
||||
private val _tiles = ObjectRegistry("tiles", TileDefinition::materialName, TileDefinition::materialId)
|
||||
val tiles = _tiles.view
|
||||
@ -108,6 +113,12 @@ class Starbound : ISBFileLocator {
|
||||
private val _techs = ObjectRegistry("techs", TechDefinition::name)
|
||||
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()) {
|
||||
serializeNulls()
|
||||
setDateFormat(DateFormat.LONG)
|
||||
@ -115,7 +126,12 @@ class Starbound : ISBFileLocator {
|
||||
setPrettyPrinting()
|
||||
|
||||
registerTypeAdapter(InternedStringAdapter(STRINGS))
|
||||
registerTypeAdapter(InternedJsonElementAdapter(STRINGS))
|
||||
|
||||
InternedJsonElementAdapter(STRINGS).also {
|
||||
registerTypeAdapter(it)
|
||||
registerTypeAdapter(it.arrays)
|
||||
registerTypeAdapter(it.objects)
|
||||
}
|
||||
|
||||
registerTypeAdapter(Nothing::class.java, NothingAdapter)
|
||||
|
||||
@ -160,6 +176,7 @@ class Starbound : ISBFileLocator {
|
||||
registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
|
||||
registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
|
||||
registerTypeAdapter(JsonFunction.Companion)
|
||||
registerTypeAdapterFactory(Json2Function.Companion)
|
||||
|
||||
// Общее
|
||||
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
|
||||
@ -193,6 +210,12 @@ class Starbound : ISBFileLocator {
|
||||
|
||||
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 {
|
||||
return ItemDescriptor(items[name] ?: return ItemDescriptor.EMPTY)
|
||||
}
|
||||
@ -271,11 +294,42 @@ class Starbound : ISBFileLocator {
|
||||
}
|
||||
|
||||
val read = file.readToString()
|
||||
it.load(read, chunkName = file.computeFullPath())
|
||||
it.load(read, chunkName = "@" + file.computeFullPath())
|
||||
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 ->
|
||||
luaRequire(args.lua, args)
|
||||
0
|
||||
@ -286,18 +340,62 @@ class Starbound : ISBFileLocator {
|
||||
state.pushTable()
|
||||
state.storeGlobal("root")
|
||||
state.loadGlobal("root")
|
||||
val root = state.stackTop
|
||||
|
||||
state.push("assetJson")
|
||||
state.pushWeak(this) {args ->
|
||||
state.setTableFunction("assetJson", this) {args ->
|
||||
args.lua.push(loadJsonAsset(args.getString()))
|
||||
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.load(polyfill, "starbound.jar!/scripts/polyfill.lua")
|
||||
state.load(polyfill, "@starbound.jar!/scripts/polyfill.lua")
|
||||
state.call()
|
||||
}
|
||||
|
||||
@ -488,7 +586,9 @@ class Starbound : ISBFileLocator {
|
||||
|
||||
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, _tileModifiers, ext2files["matmod"] ?: listOf())
|
||||
@ -539,26 +639,28 @@ class Starbound : ISBFileLocator {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadItemDefinitions(callback: (String) -> Unit) {
|
||||
val files = linkedMapOf(
|
||||
".item" to ItemPrototype::class.java,
|
||||
".currency" to CurrencyItemPrototype::class.java,
|
||||
".liqitem" to LiquidItemPrototype::class.java,
|
||||
".matitem" to MaterialItemPrototype::class.java,
|
||||
".flashlight" to FlashlightPrototype::class.java,
|
||||
".harvestingtool" to HarvestingToolPrototype::class.java,
|
||||
".head" to HeadArmorItemPrototype::class.java,
|
||||
".chest" to ChestArmorItemPrototype::class.java,
|
||||
".legs" to LegsArmorItemPrototype::class.java,
|
||||
".back" to BackArmorItemPrototype::class.java,
|
||||
private fun loadItemDefinitions(callback: (String) -> Unit, files: Map<String, Collection<IStarboundFile>>) {
|
||||
val fileMap = mapOf(
|
||||
"item" to ItemPrototype::class.java,
|
||||
"currency" to CurrencyItemPrototype::class.java,
|
||||
"liqitem" to LiquidItemPrototype::class.java,
|
||||
"matitem" to MaterialItemPrototype::class.java,
|
||||
"flashlight" to FlashlightPrototype::class.java,
|
||||
"harvestingtool" to HarvestingToolPrototype::class.java,
|
||||
"head" to HeadArmorItemPrototype::class.java,
|
||||
"chest" to ChestArmorItemPrototype::class.java,
|
||||
"legs" to LegsArmorItemPrototype::class.java,
|
||||
"back" to BackArmorItemPrototype::class.java,
|
||||
)
|
||||
|
||||
for (fs in fileSystems) {
|
||||
for (listedFile in fs.explore().filter { it.isFile }.filter { f -> files.keys.any { f.name.endsWith(it) } }) {
|
||||
for ((ext, clazz) in fileMap) {
|
||||
val fileList = files[ext] ?: continue
|
||||
|
||||
for (listedFile in fileList) {
|
||||
try {
|
||||
callback("Loading $listedFile")
|
||||
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)
|
||||
} catch (err: Throwable) {
|
||||
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 {
|
||||
/**
|
||||
* Глобальный [Interner] для [String]
|
||||
|
@ -175,7 +175,7 @@ class StarboundClient(val starbound: Starbound) : Closeable {
|
||||
input.installCallback(window)
|
||||
|
||||
GLFW.glfwMakeContextCurrent(window)
|
||||
gl = GLStateTracker(starbound)
|
||||
gl = GLStateTracker(this)
|
||||
|
||||
GLFW.glfwSetFramebufferSizeCallback(window) { _, w, h ->
|
||||
if (w == 0 || h == 0) {
|
||||
|
@ -5,6 +5,7 @@ import org.lwjgl.opengl.GL
|
||||
import org.lwjgl.opengl.GL46.*
|
||||
import org.lwjgl.opengl.GLCapabilities
|
||||
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.InvalidArgumentException
|
||||
import ru.dbotthepony.kstarbound.client.gl.shader.GLPrograms
|
||||
@ -116,7 +117,7 @@ private class TexturesTracker(maxValue: Int) : ReadWriteProperty<GLStateTracker,
|
||||
}
|
||||
|
||||
@Suppress("PropertyName", "unused")
|
||||
class GLStateTracker(val locator: ISBFileLocator) {
|
||||
class GLStateTracker(val client: StarboundClient) {
|
||||
private fun isMe(state: GLStateTracker?) {
|
||||
if (state != null && state != this) {
|
||||
throw InvalidArgumentException("Provided object does not belong to $this state tracker (belongs to $state)")
|
||||
@ -441,18 +442,18 @@ class GLStateTracker(val locator: ISBFileLocator) {
|
||||
ensureSameThread()
|
||||
|
||||
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.textureMagFilter = GL_NEAREST
|
||||
}
|
||||
}
|
||||
|
||||
return named2DTextures.computeIfAbsent(path) {
|
||||
if (!locator.exists(path)) {
|
||||
if (!client.starbound.exists(path)) {
|
||||
LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath)
|
||||
missingTexture!!
|
||||
} else {
|
||||
newTexture(path).upload(locator.readDirect(path)).generateMips().also {
|
||||
newTexture(path).upload(client.starbound.imageData(path)).generateMips().also {
|
||||
it.textureMinFilter = GL_NEAREST
|
||||
it.textureMagFilter = GL_NEAREST
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.client.gl
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.lwjgl.opengl.GL46.*
|
||||
import org.lwjgl.stb.STBImage
|
||||
import ru.dbotthepony.kstarbound.io.ImageData
|
||||
import ru.dbotthepony.kvector.vector.nint.Vector2i
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@ -248,6 +249,22 @@ class GLTexture2D(val state: GLStateTracker, val name: String = "<unknown>") : A
|
||||
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
|
||||
private set
|
||||
|
||||
|
@ -1,66 +1,118 @@
|
||||
package ru.dbotthepony.kstarbound.defs
|
||||
|
||||
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.TypeAdapter
|
||||
import com.google.gson.TypeAdapterFactory
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
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.IStringSerializable
|
||||
import ru.dbotthepony.kstarbound.io.json.Vector2dTypeAdapter
|
||||
import ru.dbotthepony.kvector.util.linearInterpolation
|
||||
import ru.dbotthepony.kvector.vector.ndouble.Vector2d
|
||||
|
||||
enum class JsonFunctionInterpolation(vararg aliases: String) : IStringSerializable {
|
||||
LINEAR;
|
||||
enum class JsonFunctionInterpolation {
|
||||
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() }
|
||||
|
||||
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)
|
||||
}
|
||||
abstract fun interpolate(t: Double, a: Double, b: Double): Double
|
||||
}
|
||||
|
||||
enum class JsonFunctionConstraint(vararg aliases: String) : IStringSerializable {
|
||||
CLAMP;
|
||||
enum class JsonFunctionConstraint {
|
||||
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 {
|
||||
for (alias in aliases)
|
||||
if (name == alias)
|
||||
return true
|
||||
|
||||
return name == this.name
|
||||
data class DoubleRange(val start: Double, val end: Double) : Comparable<Double> {
|
||||
init {
|
||||
require(start <= end) { "start <= end: $start <= $end" }
|
||||
}
|
||||
|
||||
override fun write(out: JsonWriter) {
|
||||
out.value(this.name)
|
||||
val length = end - start
|
||||
|
||||
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(
|
||||
val interpolation: JsonFunctionInterpolation,
|
||||
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>() {
|
||||
val CONSTRAINT_ADAPTER = EnumAdapter(JsonFunctionConstraint::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")
|
||||
}
|
||||
|
||||
override fun read(reader: JsonReader): JsonFunction {
|
||||
override fun read(reader: JsonReader): JsonFunction? {
|
||||
if (reader.peek() == JsonToken.NULL)
|
||||
return null
|
||||
|
||||
reader.beginArray()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt
Normal file
54
src/main/kotlin/ru/dbotthepony/kstarbound/io/ImageData.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@ -24,24 +24,46 @@ class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAda
|
||||
JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(`in`.nextString()))
|
||||
JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE
|
||||
JsonToken.NULL -> JsonNull.INSTANCE
|
||||
JsonToken.BEGIN_ARRAY -> {
|
||||
val output = JsonArray()
|
||||
`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
|
||||
}
|
||||
JsonToken.BEGIN_ARRAY -> arrays.read(`in`)
|
||||
JsonToken.BEGIN_OBJECT -> objects.read(`in`)
|
||||
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 {
|
||||
val TRUE = JsonPrimitive(true)
|
||||
val FALSE = JsonPrimitive(false)
|
||||
|
@ -526,6 +526,15 @@ class LuaState private constructor(private val pointer: Pointer, val stringInter
|
||||
if (position > this.top) return null
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,7 +13,7 @@ private val messageHandlerLua by lazy { loadInternalScript("message_handler") }
|
||||
private val configLua by lazy { loadInternalScript("config") }
|
||||
|
||||
fun LuaState.exposeConfig(config: JsonElement) {
|
||||
load(configLua, "starbound.jar!/scripts/config.lua")
|
||||
load(configLua, "@starbound.jar!/scripts/config.lua")
|
||||
call()
|
||||
|
||||
loadGlobal("config")
|
||||
@ -30,7 +30,7 @@ fun LuaState.exposeConfig(config: JsonElement) {
|
||||
|
||||
class MessageHandler(val lua: LuaState) {
|
||||
init {
|
||||
lua.load(messageHandlerLua, "starbound.jar!/scripts/message_handler.lua")
|
||||
lua.load(messageHandlerLua, "@starbound.jar!/scripts/message_handler.lua")
|
||||
lua.call()
|
||||
|
||||
lua.loadGlobal("message")
|
||||
|
@ -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.
|
||||
*/
|
||||
fun cleanupItems() {
|
||||
|
||||
TODO()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the specified item to the player's inventory.
|
||||
*/
|
||||
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.pushTable()
|
||||
|
@ -120,8 +120,8 @@ class QuestInstance(
|
||||
|
||||
lua.pop()
|
||||
|
||||
avatar.starbound.expose(lua)
|
||||
avatar.expose(lua)
|
||||
avatar.starbound.pushLuaAPI(lua)
|
||||
avatar.pushLuaAPI(lua)
|
||||
lua.exposeConfig(template.scriptConfig)
|
||||
}
|
||||
|
||||
@ -170,7 +170,7 @@ class QuestInstance(
|
||||
}
|
||||
|
||||
try {
|
||||
lua.load(script.readToString(), template.script.fullPath)
|
||||
lua.load(script.readToString(), "@" + template.script.fullPath)
|
||||
} catch(err: Throwable) {
|
||||
LOGGER.error("Error loading Lua code for quest ${descriptor.questId}", err)
|
||||
return false
|
||||
|
1457
src/test/java/ru/dbotthepony/kstarbound/test/Json2FunctionsTest.java
Normal file
1457
src/test/java/ru/dbotthepony/kstarbound/test/Json2FunctionsTest.java
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user