From 2d87575bfc76345f73a7d4ae432b05dea422c33c Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Fri, 31 Mar 2023 11:05:48 +0700 Subject: [PATCH] =?UTF-8?q?root.isTreasurePool=20=D0=B8=20root.createTreas?= =?UTF-8?q?ure,=20=D0=B8=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B0=20treasure=20pool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/ru/dbotthepony/kstarbound/Main.kt | 2 + .../dbotthepony/kstarbound/ObjectRegistry.kt | 72 ++++- .../ru/dbotthepony/kstarbound/Starbound.kt | 112 ++++++-- .../kstarbound/defs/DynamicDefinition.kt | 19 +- .../kstarbound/defs/ItemReference.kt | 9 +- .../kstarbound/defs/RegistryReference.kt | 39 ++- .../defs/item/TreasurePoolDefinition.kt | 259 ++++++++++++++++++ .../kstarbound/io/json/EitherTypeAdapter.kt | 12 +- .../ru/dbotthepony/kstarbound/util/Either.kt | 12 +- 9 files changed, 456 insertions(+), 80 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index f304def7..519e306b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -221,6 +221,8 @@ fun main() { last = JVMTimeSource.INSTANCE.millis } } + + println(starbound.treasurePools["motherpoptopTreasure"]!!.value.evaluate(Random(), 2.0)) } //ent.position += Vector2d(y = 14.0, x = -10.0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt index aeeb09db..75b61523 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/ObjectRegistry.kt @@ -1,7 +1,10 @@ package ru.dbotthepony.kstarbound import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive import com.google.gson.internal.bind.JsonTreeReader import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap @@ -24,25 +27,74 @@ inline fun ObjectRegistry(name: String, noinline key: ((T) -> return ObjectRegistry(T::class, name, key, intKey) } -private fun merge(source: JsonObject, destination: JsonObject): JsonObject { +fun mergeJsonElements(source: JsonObject, destination: JsonObject): JsonObject { for ((k, v) in source.entrySet()) { if (!destination.has(k)) { destination[k] = v.deepCopy() - } else if (destination[k] is JsonObject && v is JsonObject) { - merge(v, destination[k] as JsonObject) + } else { + mergeJsonElements(v, destination[k]) } } return destination } -class RegistryObject(val value: T, private val json: JsonObject, val file: IStarboundFile, val gson: Gson, val pathStack: PathStack) { +fun mergeJsonElements(source: JsonArray, destination: JsonArray): JsonArray { + for ((i, v) in source.withIndex()) { + if (i >= destination.size()) { + destination.add(v.deepCopy()) + } else { + destination[i] = mergeJsonElements(v, destination[i]) + } + } + + return destination +} + +fun mergeJsonElements(source: JsonElement, destination: JsonElement): JsonElement { + if (destination is JsonPrimitive) { + return destination + } + + if (destination is JsonObject && source is JsonObject) { + return mergeJsonElements(source, destination) + } + + if (destination is JsonArray && source is JsonArray) { + return mergeJsonElements(source, destination) + } + + return destination +} + +class RegistryObject( + /** + * Объект реестра + */ + val value: T, + /** + * Оригинальный JSON объекта без каких либо изменений + */ + private val json: JsonElement, + /** + * Файл, откуда данный объект был загружен + */ + val file: IStarboundFile, + /** + * [Gson], который загрузил данный объект из JSON + */ + val gson: Gson, + /** + * [PathStack] который используется для загрузки и чтения, из какого файла читается объект + */ + val pathStack: PathStack +) { /** * Возвращает копию оригинальной JSON структуры, из которой был спрототипирован [value] * * Более полная JSON структура (обработанная) доступа из метода [toJson] */ - fun copy(): JsonObject { + fun copy(): JsonElement { return json.deepCopy() } @@ -60,8 +112,8 @@ class RegistryObject(val value: T, private val json: JsonObject, val fi * Полнота определяется тем, что [value] может иметь свойства по умолчанию, которые не указаны * в оригинальной JSON структуре. [copy] не вернёт данные свойства по умолчанию, а [toJson] вернёт. */ - fun toJson(): JsonObject { - return merge(json, gson.toJsonTree(value) as JsonObject) + fun toJson(): JsonElement { + return mergeJsonElements(json, gson.toJsonTree(value)) } override fun equals(other: Any?): Boolean { @@ -195,17 +247,17 @@ class ObjectRegistry(val clazz: KClass, val name: String, val key: ( fun add(gson: Gson, file: IStarboundFile, pathStack: PathStack): Boolean { return pathStack(file.computeDirectory()) { - val elem = gson.fromJson(file.reader(), JsonObject::class.java) + val elem = gson.fromJson(file.reader(), JsonElement::class.java) val value = gson.fromJson(JsonTreeReader(elem), clazz.java) 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: JsonElement, file: IStarboundFile, gson: Gson, pathStack: PathStack): Boolean { return add(RegistryObject(value, json, file, gson, pathStack), this.key?.invoke(value) ?: throw UnsupportedOperationException("No key mapper")) } - fun add(value: T, json: JsonObject, file: IStarboundFile, gson: Gson, pathStack: PathStack, key: String): Boolean { + fun add(value: T, json: JsonElement, file: IStarboundFile, gson: Gson, pathStack: PathStack, key: String): Boolean { return add(RegistryObject(value, json, file, gson, pathStack), key) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 791f0384..a40720b3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -28,6 +28,7 @@ import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.InventoryIcon +import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.impl.ItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemPrototype import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemPrototype @@ -146,6 +147,9 @@ class Starbound : ISBFileLocator { val recipeRegistry = RecipeRegistry() + private val _treasurePools = ObjectRegistry("treasure pools", TreasurePoolDefinition::name) + val treasurePools = _treasurePools.view + val gson: Gson = with(GsonBuilder()) { serializeNulls() setDateFormat(DateFormat.LONG) @@ -221,22 +225,24 @@ class Starbound : ISBFileLocator { registerTypeAdapter(ItemStack.Adapter(this@Starbound)) registerTypeAdapterFactory(ItemReference.Factory(STRINGS)) + registerTypeAdapterFactory(TreasurePoolDefinition.Companion) registerTypeAdapterFactory(with(RegistryReferenceFactory()) { - add(tiles::get) - add(tileModifiers::get) - add(liquid::get) - add(items::get) - add(species::get) - add(statusEffects::get) - add(particles::get) - add(questTemplates::get) - add(techs::get) - add(jsonFunctions::get) - add(json2Functions::get) - add(npcTypes::get) - add(projectiles::get) - add(tenants::get) + add(_tiles) + add(_tileModifiers) + add(_liquid) + add(_items) + add(_species) + add(_statusEffects) + add(_particles) + add(_questTemplates) + add(_techs) + add(_jsonFunctions) + add(_json2Functions) + add(_npcTypes) + add(_projectiles) + add(_tenants) + add(_treasurePools) }) registerTypeAdapter(LongRangeAdapter) @@ -614,6 +620,31 @@ class Starbound : ISBFileLocator { 1 } + state.setTableFunction("npcPortrait", this) { args -> + // JsonArray root.npcPortrait(String portraitMode, String species, String npcType, float level, [unsigned seed], [Json parameters]) + // Generates an NPC with the specified type, level, seed and parameters and returns a portrait in the given portraitMode as a list of drawables. + TODO() + } + + state.setTableFunction("monsterPortrait", this) { args -> + // JsonArray root.monsterPortrait(String typeName, [Json parameters]) + // Generates a monster of the given type with the given parameters and returns its portrait as a list of drawables. + TODO() + } + + state.setTableFunction("isTreasurePool", this) { args -> + args.push(args.getString() in treasurePools) + 1 + } + + state.setTableFunction("createTreasure", this) { args -> + val name = args.getString() + val level = args.getDouble() + val rand = if (args.hasSomethingAt()) java.util.Random(args.getLong()) else java.util.Random() + args.push(treasurePools[name]?.value?.evaluate(rand, level)?.stream()?.map { it.toJson() }?.filterNotNull()?.collect(JsonArrayCollector) ?: throw NoSuchElementException("No such treasure pool $name")) + 1 + } + state.pop() state.load(polyfill, "@starbound.jar!/scripts/polyfill.lua") @@ -796,6 +827,7 @@ class Starbound : ISBFileLocator { loadStage(callback, { loadJsonFunctions(it, ext2files["functions"] ?: listOf()) }, "json functions") loadStage(callback, { loadJson2Functions(it, ext2files["2functions"] ?: listOf()) }, "json 2functions") loadStage(callback, { loadRecipes(it, ext2files["recipe"] ?: listOf()) }, "recipes") + loadStage(callback, { loadTreasurePools(it, ext2files["treasurepools"] ?: listOf()) }, "treasure pools") loadStage(callback, _tiles, ext2files["material"] ?: listOf()) loadStage(callback, _tileModifiers, ext2files["matmod"] ?: listOf()) @@ -888,16 +920,17 @@ class Starbound : ISBFileLocator { private fun loadJsonFunctions(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { - try { - callback("Loading $listedFile") - val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) + callback("Loading $listedFile") + val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) - for ((k, v) in json.entrySet()) { + for ((k, v) in json.entrySet()) { + try { + callback("Loading $k from $listedFile") val fn = gson.fromJson(JsonTreeReader(v), JsonFunction::class.java) - _jsonFunctions.add(fn, json, listedFile, gson, pathStack, k) + _jsonFunctions.add(fn, v, listedFile, gson, pathStack, k) + } catch (err: Throwable) { + logger.error("Loading json function definition $k from file $listedFile", err) } - } catch (err: Throwable) { - logger.error("Loading json function definition file $listedFile", err) } if (terminateLoading) { @@ -908,16 +941,39 @@ class Starbound : ISBFileLocator { private fun loadJson2Functions(callback: (String) -> Unit, files: Collection) { for (listedFile in files) { - try { - callback("Loading $listedFile") - val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) + callback("Loading $listedFile") + val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) - for ((k, v) in json.entrySet()) { + for ((k, v) in json.entrySet()) { + try { + callback("Loading $k from $listedFile") val fn = gson.fromJson(JsonTreeReader(v), Json2Function::class.java) - _json2Functions.add(fn, json, listedFile, gson, pathStack, k) + _json2Functions.add(fn, v, listedFile, gson, pathStack, k) + } catch (err: Throwable) { + logger.error("Loading json 2function definition $k from file $listedFile", err) + } + } + + if (terminateLoading) { + return + } + } + } + + private fun loadTreasurePools(callback: (String) -> Unit, files: Collection) { + for (listedFile in files) { + callback("Loading $listedFile") + val json = gson.fromJson(listedFile.reader(), JsonObject::class.java) + + for ((k, v) in json.entrySet()) { + try { + callback("Loading $k from $listedFile") + val result = gson.fromJson(JsonTreeReader(v), TreasurePoolDefinition::class.java) + result.name = k + _treasurePools.add(result, v, listedFile, gson, pathStack) + } catch (err: Throwable) { + logger.error("Loading treasure pool definition $k from file $listedFile", err) } - } catch (err: Throwable) { - logger.error("Loading json 2function definition file $listedFile", err) } if (terminateLoading) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DynamicDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DynamicDefinition.kt index 893c3027..be6e1bcb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DynamicDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/DynamicDefinition.kt @@ -3,22 +3,7 @@ package ru.dbotthepony.kstarbound.defs import com.google.gson.JsonObject import com.google.gson.internal.bind.JsonTreeReader import ru.dbotthepony.kstarbound.RegistryObject - -private fun merge(destination: JsonObject, source: JsonObject) { - for ((k, v) in source.entrySet()) { - if (v is JsonObject) { - val original = destination[k] - - if (original is JsonObject) { - merge(original, v) - } else { - destination.add(k, v) - } - } else { - destination.add(k, v) - } - } -} +import ru.dbotthepony.kstarbound.mergeJsonElements abstract class DynamicDefinition(val original: RegistryObject) { @Volatile @@ -36,7 +21,7 @@ abstract class DynamicDefinition(val original: RegistryObject) { if (!isDirty) return field val copy = original.copy() - merge(copy, dynamicData) + mergeJsonElements(copy, dynamicData) original.pathStack(original.file.computeDirectory()) { field = original.gson.fromJson(JsonTreeReader(copy), field::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt index e8061671..3ee9e8a1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt @@ -31,14 +31,15 @@ data class ItemReference( override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == ItemReference::class.java) { return object : TypeAdapter() { - private val regular = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, logMisses = true, asList = false), gson, stringInterner) + private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, logMisses = true, asList = false), gson, stringInterner) + private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, logMisses = true, asList = true), gson, stringInterner) private val references = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, IItemDefinition::class.java)) as TypeAdapter> override fun write(out: JsonWriter, value: ItemReference?) { if (value == null) out.nullValue() else - regular.write(out, value) + regularObject.write(out, value) } override fun read(`in`: JsonReader): ItemReference? { @@ -47,8 +48,10 @@ data class ItemReference( if (`in`.peek() == JsonToken.STRING) { return ItemReference(references.read(`in`)) + } else if (`in`.peek() == JsonToken.BEGIN_ARRAY) { + return regularList.read(`in`) } else { - return regular.read(`in`) + return regularObject.read(`in`) } } } as TypeAdapter diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt index 410a63d9..f00c5bf6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/RegistryReference.kt @@ -9,12 +9,15 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.ObjectRegistry import ru.dbotthepony.kstarbound.RegistryObject +import ru.dbotthepony.kstarbound.io.json.consumeNull import java.lang.reflect.ParameterizedType import java.util.function.Supplier class RegistryReferenceFactory : TypeAdapterFactory { - private val types = Reference2ObjectArrayMap, (String) -> RegistryObject?>() + private val types = Reference2ObjectArrayMap, Pair<(String) -> RegistryObject?, String>>() private var isLenient = false fun lenient(): RegistryReferenceFactory { @@ -22,26 +25,30 @@ class RegistryReferenceFactory : TypeAdapterFactory { return this } - fun add(clazz: Class, resolver: (String) -> RegistryObject?): RegistryReferenceFactory { - check(types.put(clazz, resolver as (String) -> RegistryObject?) == null) { "Already has resolver for class $clazz!" } + fun add(clazz: Class, resolver: (String) -> RegistryObject?, name: String): RegistryReferenceFactory { + check(types.put(clazz, (resolver as (String) -> RegistryObject?) to name) == null) { "Already has resolver for class $clazz!" } return this } - inline fun add(noinline resolver: (String) -> RegistryObject?) = add(T::class.java, resolver) + fun add(registry: ObjectRegistry): RegistryReferenceFactory { + return add(registry.clazz.java, registry.view::get, registry.name) + } + + inline fun add(noinline resolver: (String) -> RegistryObject?, name: String) = add(T::class.java, resolver, name) override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == RegistryReference::class.java) { val ptype = type.type as? ParameterizedType ?: return null val registryType = ptype.actualTypeArguments[0] val resolver = types[registryType] ?: return if (isLenient) null else throw NoSuchElementException("Can't deserialize registry reference with type $registryType!") - return RegistryReferenceTypeAdapter(resolver, gson.getAdapter(String::class.java)) as TypeAdapter + return RegistryReferenceTypeAdapter(resolver.first, gson.getAdapter(String::class.java), resolver.second) as TypeAdapter } return null } } -class RegistryReferenceTypeAdapter(val resolver: (String) -> RegistryObject?, val strings: TypeAdapter) : TypeAdapter>() { +class RegistryReferenceTypeAdapter(val resolver: (String) -> RegistryObject?, val strings: TypeAdapter, val name: String) : TypeAdapter>() { override fun write(out: JsonWriter, value: RegistryReference?) { if (value == null) out.nullValue() @@ -50,19 +57,27 @@ class RegistryReferenceTypeAdapter(val resolver: (String) -> RegistryOb } override fun read(`in`: JsonReader): RegistryReference? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null if (`in`.peek() == JsonToken.STRING) { - return RegistryReference(strings.read(`in`)!!, resolver) + return RegistryReference(strings.read(`in`)!!, resolver, name) } throw JsonSyntaxException("Expecting string for registry reference, ${`in`.peek()} given, near ${`in`.path}") } } -data class RegistryReference(val name: String, val resolver: (String) -> RegistryObject?) : Supplier?>, () -> RegistryObject?, Lazy?> { - private val lazy = lazy { resolver.invoke(name) } +data class RegistryReference(val name: String, val resolver: (String) -> RegistryObject?, val registryName: String) : Supplier?>, () -> RegistryObject?, Lazy?> { + private val lazy = lazy { + val result = resolver.invoke(name) + + if (result == null) { + LOGGER.error("No such object '$name' in registry '$registryName'! Expect stuff being broken!") + } + + result + } override fun get(): RegistryObject? { return lazy.value @@ -78,4 +93,8 @@ data class RegistryReference(val name: String, val resolver: (String) - override fun invoke(): RegistryObject? { return lazy.value } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt new file mode 100644 index 00000000..57894bf2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt @@ -0,0 +1,259 @@ +package ru.dbotthepony.kstarbound.defs.item + +import com.google.common.collect.ImmutableList +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.internal.bind.JsonTreeReader +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.defs.ItemReference +import ru.dbotthepony.kstarbound.defs.RegistryReference +import ru.dbotthepony.kstarbound.io.json.consumeNull +import ru.dbotthepony.kstarbound.io.json.stream +import ru.dbotthepony.kstarbound.util.Either +import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.util.WriteOnce +import java.util.random.RandomGenerator + +class TreasurePoolDefinition(pieces: List) { + var name: String by WriteOnce() + + init { + require(pieces.isNotEmpty()) { "Treasure pool is empty" } + } + + val pieces: ImmutableList = pieces.stream().sorted { o1, o2 -> o1.level.compareTo(o2.level) }.collect(ImmutableList.toImmutableList()) + + fun evaluate(random: RandomGenerator, level: Double): List { + require(level >= 0.0) { "Invalid loot level: $level" } + @Suppress("name_shadowing") + var level = level + + for (piece in pieces) { + if (level <= piece.level) { + return piece.evaluate(random, level) + } else { + level -= piece.level + } + } + + if (pieces.last().level <= level) { + return pieces.last().evaluate(random, level) + } else { + return emptyList() + } + } + + data class Piece( + val level: Double, + val pool: ImmutableList = ImmutableList.of(), + val fill: ImmutableList>> = ImmutableList.of(), + val poolRounds: IPoolRounds = OneRound, + // TODO: что оно делает? + // оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool + // а так же не "дублирует" содержимое если уровень генерации выше, чем указанный level + val allowDuplication: Boolean = false + ) { + val maxWeight = pool.stream().mapToDouble { it.weight }.sum() + + fun evaluate(random: RandomGenerator, actualLevel: Double): List { + val rounds = poolRounds.evaluate(random) + if (rounds <= 0) return emptyList() + val result = ArrayList() + + for (round in 0 until rounds) { + for (entry in fill) { + entry.map(left = { + val stack = it.makeStack() + if (stack.isNotEmpty) result.add(stack) + }, right = { + it.value?.value?.evaluate(random, actualLevel) + }) + } + + if (pool.isNotEmpty()) { + var chosen = random.nextDouble(maxWeight) + + for (entry in pool) { + if (chosen <= entry.weight) { + entry.treasure.map(left = { + val stack = it.makeStack() + if (stack.isNotEmpty) result.add(stack) + }, right = { + it.value?.value?.evaluate(random, actualLevel) + }) + + break + } else { + chosen -= entry.weight + } + } + } + } + + return result + } + } + + interface IPoolRounds { + fun evaluate(random: RandomGenerator): Int + } + + object OneRound : IPoolRounds { + override fun evaluate(random: RandomGenerator): Int { + return 1 + } + } + + data class ConstantRound(val amount: Int) : IPoolRounds { + init { + check(amount >= 1) { "Invalid treasure pool rounds: $amount" } + } + + override fun evaluate(random: RandomGenerator): Int { + return amount + } + } + + data class PoolRounds(val edges: ImmutableList) : IPoolRounds { + val maxWeight = edges.stream().mapToDouble { it.weight }.sum() + + override fun evaluate(random: RandomGenerator): Int { + val result = random.nextDouble(maxWeight) + var lower = 0.0 + + for (edge in edges) { + if (result in lower .. lower + edge.weight) { + return edge.rounds + } else { + lower += edge.weight + } + } + + return edges.last().rounds + } + } + + data class RoundEdge(val weight: Double, val rounds: Int) { + init { + require(weight > 0.0) { "Invalid round weight: $weight" } + require(rounds >= 0) { "Invalid rounds amount: $rounds" } + } + } + + data class PoolEntry( + val weight: Double, + val treasure: Either> + ) { + init { + require(weight > 0.0) { "Invalid pool entry weight: $weight" } + } + } + + companion object : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType === TreasurePoolDefinition::class.java) { + return object : TypeAdapter() { + private val itemAdapter = gson.getAdapter(ItemReference::class.java) + private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(RegistryReference::class.java, TreasurePoolDefinition::class.java)) as TypeAdapter> + private val objReader = gson.getAdapter(JsonObject::class.java) + + override fun write(out: JsonWriter, value: TreasurePoolDefinition?) { + if (value == null) { + out.nullValue() + } else { + TODO() + } + } + + override fun read(`in`: JsonReader): TreasurePoolDefinition? { + if (`in`.consumeNull()) { + return null + } + + val pieces = ArrayList() + + `in`.beginArray() + + while (`in`.hasNext()) { + `in`.beginArray() + + val level = `in`.nextDouble() + val things = objReader.read(`in`) + + val pool = ImmutableList.Builder() + val fill = ImmutableList.Builder>>() + var poolRounds: IPoolRounds = OneRound + val allowDuplication = things["allowDuplication"]?.asBoolean ?: false + + things["poolRounds"]?.let { + if (it is JsonPrimitive) { + poolRounds = ConstantRound(it.asInt) + } else if (it is JsonArray) { + poolRounds = PoolRounds(it.stream().map { it as JsonArray; RoundEdge(it[0].asDouble, it[1].asInt) }.collect(ImmutableList.toImmutableList())) + } else { + throw JsonSyntaxException("Expected either a number or an array for poolRounds, but got ${it::class.simpleName}") + } + } + + things["pool"]?.let { + it as? JsonArray ?: throw JsonSyntaxException("pool must be an array") + + for ((i, elem) in it.withIndex()) { + elem as? JsonObject ?: throw JsonSyntaxException("Pool element at $i is not an object") + val weight = (elem["weight"] as? JsonPrimitive)?.asDouble ?: throw JsonSyntaxException("Pool element at $i is missing weight") + + if (elem.has("item")) { + pool.add(PoolEntry(weight, Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!))))) + } else if (elem.has("pool")) { + pool.add(PoolEntry(weight, Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!))))) + } else { + throw JsonSyntaxException("Pool element at $i is missing both 'item' and 'pool' entries") + } + } + } + + things["fill"]?.let { + it as? JsonArray ?: throw JsonSyntaxException("fill must be an array") + + for ((i, elem) in it.withIndex()) { + elem as? JsonObject ?: throw JsonSyntaxException("Fill element at $i is not an object") + + if (elem.has("item")) { + fill.add(Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!)))) + } else if (elem.has("pool")) { + fill.add(Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!)))) + } else { + throw JsonSyntaxException("Fill element at $i is missing both 'item' and 'pool' entries") + } + } + } + + pieces.add(Piece( + pool = pool.build(), + fill = fill.build(), + poolRounds = poolRounds, + level = level, + allowDuplication = allowDuplication + )) + + `in`.endArray() + } + + `in`.endArray() + + return TreasurePoolDefinition(pieces) + } + } as TypeAdapter + } + + return null + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EitherTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EitherTypeAdapter.kt index 4a827759..0d304fcd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EitherTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/json/EitherTypeAdapter.kt @@ -21,26 +21,26 @@ object EitherTypeAdapter : TypeAdapterFactory { val params = type.type as? ParameterizedType ?: return null val (left, right) = params.actualTypeArguments - return object : TypeAdapter>() { + return object : TypeAdapter>() { private val leftAdapter = gson.getAdapter(TypeToken.get(left)) as TypeAdapter private val rightAdapter = gson.getAdapter(TypeToken.get(right)) as TypeAdapter - override fun write(out: JsonWriter, value: Either?) { + override fun write(out: JsonWriter, value: Either?) { if (value == null) out.nullValue() else - value.consume({ leftAdapter.write(out, it) }, { rightAdapter.write(out, it) }) + value.map({ leftAdapter.write(out, it) }, { rightAdapter.write(out, it) }) } - override fun read(`in`: JsonReader): Either? { + override fun read(`in`: JsonReader): Either? { if (`in`.peek() == JsonToken.NULL) return null return try { - Either.left(leftAdapter.read(`in`)) + Either.left(leftAdapter.read(`in`) ?: throw NullPointerException("left was empty")) } catch(leftError: Throwable) { try { - Either.right(rightAdapter.read(`in`)) + Either.right(rightAdapter.read(`in`) ?: throw NullPointerException("right was empty")) } catch(rightError: Throwable) { val error = JsonSyntaxException("Can't read Either of values (left is $left, right is $right)") error.addSuppressed(leftError) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt index ddc7c3d4..8bcb0a65 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Either.kt @@ -7,13 +7,13 @@ import ru.dbotthepony.kstarbound.io.json.EitherTypeAdapter * * JSON адаптер реализуется через [EitherTypeAdapter] */ -data class Either(val left: L?, val right: R?) { +data class Either(val left: L?, val right: R?) { init { require(left != null || right != null) { "Both inputs are null" } require(!(left != null && right != null)) { "Both inputs are not null" } } - inline fun consume(left: (L) -> Unit, right: (R) -> Unit) { + inline fun map(left: (L) -> Unit, right: (R) -> Unit) { if (this.left != null) left.invoke(this.left) else @@ -29,13 +29,13 @@ data class Either(val left: L?, val right: R?) { companion object { @JvmStatic - fun left(value: L): Either { - return Either(left = value ?: throw NullPointerException("Left is null"), right = null) + fun left(value: L): Either { + return Either(left = value, right = null) } @JvmStatic - fun right(value: R): Either { - return Either(left = null, right = value ?: throw NullPointerException("Right is null")) + fun right(value: R): Either { + return Either(left = null, right = value) } } }