diff --git a/ADDITIONS.md b/ADDITIONS.md index 86584574..3f8a0495 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -57,6 +57,15 @@ val color: TileColor = TileColor.DEFAULT ### Prototypes +#### Items + * `inventoryIcon` additions if specified as array: + * `scale`, either as float or as vector (for x and y scales); both in prototype file and in `parameters`. + * `color` (defaults to white `[255, 255, 255, 255]`) + * `rotation` (in degrees, defaults to `0`) + * `mirrored` (defaults to `false`, this is different from setting scale to `-1f` since it obeys center point) + * `centered` (defaults to `true`) + * `fullbright` (defaults to `false`) + #### .matierial * Implemented `isConnectable`, which was planned by original developers, but scrapped in process (defaults to `true`, by default only next meta-materials have it set to false: `empty`, `null` and `boundary`) * Used by object and plant anchoring code to determine valid placement diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 791bce53..003de6d3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.defs.UniverseServerConfig import ru.dbotthepony.kstarbound.defs.WorldServerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig +import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig @@ -25,6 +26,7 @@ import ru.dbotthepony.kstarbound.defs.world.SystemWorldObjectConfig import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.util.AssetPathStack +import java.util.concurrent.CompletableFuture import java.util.concurrent.ForkJoinTask import java.util.concurrent.Future import kotlin.properties.Delegates @@ -102,30 +104,20 @@ object Globals { var instanceWorlds by Delegates.notNull>() private set - private object EmptyTask : ForkJoinTask() { - private fun readResolve(): Any = EmptyTask - override fun getRawResult() { - } + var itemParameters by Delegates.notNull() + private set - override fun setRawResult(value: Unit?) { - } - - override fun exec(): Boolean { - return true - } - } - - private fun load(path: String, accept: KMutableProperty0, adapter: TypeAdapter): Future<*> { + private fun load(path: String, accept: KMutableProperty0, adapter: Lazy>): Future<*> { val file = Starbound.loadJsonAsset(path) if (file == null) { LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!") - return EmptyTask + return CompletableFuture.completedFuture(Unit) } else { return Starbound.EXECUTOR.submit { try { AssetPathStack("/") { - accept.set(adapter.fromJsonTree(file)) + accept.set(adapter.value.fromJsonTree(file)) } } catch (err: Throwable) { LOGGER.fatal("Error while reading $path, expect bad things to happen!", err) @@ -136,7 +128,7 @@ object Globals { } private inline fun load(path: String, accept: KMutableProperty0): Future<*> { - return load(path, accept, Starbound.gson.getAdapter(T::class.java)) + return load(path, accept, lazy(LazyThreadSafetyMode.NONE) { Starbound.gson.getAdapter(T::class.java) }) } fun load(): List> { @@ -157,16 +149,17 @@ object Globals { tasks.add(load("/celestial.config", ::celestialBaseInformation)) tasks.add(load("/celestial.config", ::celestialConfig)) tasks.add(load("/celestial/names.config", ::celestialNames)) + tasks.add(load("/items/defaultParameters.config", ::itemParameters)) tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/tiles/defaultDamage.config", ::tileDamage)) - tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) - tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) - tasks.add(load("/system_objects.config", ::systemObjects, Starbound.gson.mapAdapter())) - tasks.add(load("/instance_worlds.config", ::instanceWorlds, Starbound.gson.mapAdapter())) + tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, lazy { Starbound.gson.mapAdapter() })) + tasks.add(load("/currencies.config", ::currencies, lazy { Starbound.gson.mapAdapter() })) + tasks.add(load("/system_objects.config", ::systemObjects, lazy { Starbound.gson.mapAdapter() })) + tasks.add(load("/instance_worlds.config", ::instanceWorlds, lazy { Starbound.gson.mapAdapter() })) return tasks } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index 238524fd..9255acad 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -10,8 +10,15 @@ import java.net.InetSocketAddress private val LOGGER = LogManager.getLogger() fun main() { - Starbound.addPakPath(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) - Starbound.doBootstrap() + Starbound.addArchive(File("J:\\Steam\\steamapps\\common\\Starbound\\assets\\packed.pak")) + + /*for (f in File("J:\\Steam\\steamapps\\workshop\\content\\211820").listFiles()!!) { + for (f2 in f.listFiles()!!) { + if (f2.isFile) { + Starbound.addArchive(f2) + } + } + }*/ LOGGER.info("Running LWJGL ${Version.getVersion()}") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index bc9309ea..c691d9e3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -19,17 +19,6 @@ import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.BackArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.ChestArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.CurrencyItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.FlashlightDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.HarvestingToolPrototype -import ru.dbotthepony.kstarbound.defs.item.impl.HeadArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.LegsArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.LiquidItemDefinition -import ru.dbotthepony.kstarbound.defs.item.impl.MaterialItemDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition @@ -49,6 +38,8 @@ import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition +import ru.dbotthepony.kstarbound.item.ItemRegistry +import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -75,7 +66,6 @@ object Registries { val species = Registry("species").also(registriesInternal::add).also { adapters.add(it.adapter()) } val statusEffects = Registry("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) } val particles = Registry("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) } - val items = Registry("item").also(registriesInternal::add).also { adapters.add(it.adapter()) } val questTemplates = Registry("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) } val techs = Registry("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) } val jsonFunctions = Registry("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) } @@ -115,19 +105,18 @@ object Registries { private inline fun loadRegistry( registry: Registry, - files: List, + patches: Map>, + files: Collection, noinline keyProvider: (T) -> Pair>, noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } ): List> { val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } - val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) } return files.map { listedFile -> Starbound.EXECUTOR.submit { try { AssetPathStack(listedFile.computeDirectory()) { - // TODO: json patch support - val elem = elementAdapter.read(listedFile.jsonReader()) + val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) val read = adapter.fromJsonTree(elem) val keys = keyProvider(read) @@ -154,91 +143,49 @@ object Registries { registriesInternal.forEach { it.finishLoad() } } - fun load(fileTree: Map>): List> { + fun load(fileTree: Map>, patchTree: Map>): List> { val tasks = ArrayList>() - tasks.addAll(loadItemDefinitions(fileTree)) + tasks.addAll(ItemRegistry.load(fileTree, patchTree)) - tasks.addAll(loadTerrainSelectors(fileTree)) + tasks.addAll(loadTerrainSelectors(fileTree, patchTree)) - tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) - tasks.addAll(loadRegistry(tileModifiers, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId))) - tasks.addAll(loadRegistry(liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId))) + tasks.addAll(loadRegistry(tiles, patchTree, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) + tasks.addAll(loadRegistry(tileModifiers, patchTree, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId))) + tasks.addAll(loadRegistry(liquid, patchTree, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId))) tasks.add(loadMetaMaterials()) - tasks.addAll(loadRegistry(dungeons, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name))) + tasks.addAll(loadRegistry(dungeons, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name))) - tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) - tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) - tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind))) - tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() })) - tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) - tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) - tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) - tasks.addAll(loadRegistry(monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) - tasks.addAll(loadRegistry(biomes, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name))) - tasks.addAll(loadRegistry(grassVariants, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name))) - tasks.addAll(loadRegistry(treeStemVariants, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) - tasks.addAll(loadRegistry(treeFoliageVariants, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) - tasks.addAll(loadRegistry(bushVariants, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name))) + tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) + tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) + tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind))) + tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() })) + tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) + tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) + tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) + tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) + tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name))) + tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name))) + tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name))) + tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) + tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name))) - tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf())) - tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf())) - tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf())) - tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf()) { name = it }) + tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree)) + tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree)) + tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree)) + tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it }) return tasks } - private fun loadItemDefinitions(files: Map>): List> { - val fileMap = mapOf( - "item" to ItemDefinition::class.java, - "currency" to CurrencyItemDefinition::class.java, - "liqitem" to LiquidItemDefinition::class.java, - "matitem" to MaterialItemDefinition::class.java, - "flashlight" to FlashlightDefinition::class.java, - "harvestingtool" to HarvestingToolPrototype::class.java, - "head" to HeadArmorItemDefinition::class.java, - "chest" to ChestArmorItemDefinition::class.java, - "legs" to LegsArmorItemDefinition::class.java, - "back" to BackArmorItemDefinition::class.java, - ) - - val tasks = ArrayList>() - val objects = Starbound.gson.getAdapter(JsonObject::class.java) - - for ((ext, clazz) in fileMap) { - val fileList = files[ext] ?: continue - val adapter by lazy { Starbound.gson.getAdapter(clazz) } - - for (listedFile in fileList) { - tasks.add(Starbound.EXECUTOR.submit { - try { - val json = objects.read(listedFile.jsonReader()) - val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) } - - items.add { - items.add(key = def.itemName, value = def, json = json, file = listedFile) - } - } catch (err: Throwable) { - LOGGER.error("Loading item definition file $listedFile", err) - } - }) - } - } - - return tasks - } - - private inline fun loadCombined(registry: Registry, files: Collection, noinline transform: T.(String) -> Unit = {}): List> { + private inline fun loadCombined(registry: Registry, files: Collection, patches: Map>, noinline transform: T.(String) -> Unit = {}): List> { val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } - val elementAdapter by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } return files.map { listedFile -> Starbound.EXECUTOR.submit { try { - // TODO: json patch support - val json = elementAdapter.read(JsonReader(listedFile.reader()).also { it.isLenient = true }) + val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject for ((k, v) in json.entrySet()) { try { @@ -259,9 +206,9 @@ object Registries { } } - private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?) { + private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map>) { try { - val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) + val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") val factory = TerrainSelectorType.factory(json, false, type) @@ -273,12 +220,12 @@ object Registries { } } - private fun loadTerrainSelectors(files: Map>): List> { + private fun loadTerrainSelectors(files: Map>, patches: Map>): List> { val tasks = ArrayList>() tasks.addAll((files["terrain"] ?: listOf()).map { listedFile -> Starbound.EXECUTOR.submit { - loadTerrainSelector(listedFile, null) + loadTerrainSelector(listedFile, null, patches) } }) @@ -286,7 +233,7 @@ object Registries { for (type in TerrainSelectorType.entries) { tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile -> Starbound.EXECUTOR.submit { - loadTerrainSelector(listedFile, type) + loadTerrainSelector(listedFile, type, patches) } }) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 7426602a..2fdd495d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -1,10 +1,11 @@ package ru.dbotthepony.kstarbound +import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Scheduler import com.google.gson.* -import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import com.google.gson.stream.JsonReader +import it.unimi.dsi.fastutil.objects.ObjectArraySet import kotlinx.coroutines.asCoroutineDispatcher import org.apache.logging.log4j.LogManager import org.classdump.luna.compiler.CompilerChunkLoader @@ -24,12 +25,12 @@ import ru.dbotthepony.kommons.gson.Vector3iTypeAdapter import ru.dbotthepony.kommons.gson.Vector4dTypeAdapter import ru.dbotthepony.kommons.gson.Vector4fTypeAdapter import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.InventoryIcon import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList import ru.dbotthepony.kstarbound.defs.animation.Particle @@ -42,6 +43,7 @@ import ru.dbotthepony.kstarbound.defs.world.BiomePlacementItemType import ru.dbotthepony.kstarbound.defs.world.WorldLayout import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.io.* +import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedStringAdapter @@ -56,8 +58,9 @@ import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory import ru.dbotthepony.kstarbound.server.world.UniverseChunk -import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.item.RecipeRegistry import ru.dbotthepony.kstarbound.json.JsonAdapterTypeFactory +import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.JsonPath import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.util.BlockableEventLoop @@ -70,6 +73,8 @@ import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat +import java.time.Duration +import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executor @@ -82,11 +87,6 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport -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 @@ -102,12 +102,19 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca // compile flags. uuuugh const val DEDUP_CELL_STATES = true const val USE_CAFFEINE_INTERNER = false + const val USE_INTERNER = true fun interner(): Interner { + if (!USE_INTERNER) + return Interner { it } + return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner() } fun interner(bits: Int): Interner { + if (!USE_INTERNER) + return Interner { it } + return if (USE_CAFFEINE_INTERNER) Interner.newWeakInterner() else HashTableInterner(bits) } @@ -248,6 +255,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca return loader.compileTextChunk(name, chunk) } + val ELEMENTS_ADAPTER = InternedJsonElementAdapter(STRINGS) + val gson: Gson = with(GsonBuilder()) { // serializeNulls() setDateFormat(DateFormat.LONG) @@ -256,11 +265,9 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca registerTypeAdapter(InternedStringAdapter(STRINGS)) - InternedJsonElementAdapter(STRINGS).also { - registerTypeAdapter(it) - registerTypeAdapter(it.arrays) - registerTypeAdapter(it.objects) - } + registerTypeAdapter(ELEMENTS_ADAPTER) + registerTypeAdapter(ELEMENTS_ADAPTER.arrays) + registerTypeAdapter(ELEMENTS_ADAPTER.objects) registerTypeAdapter(Nothing::class.java, NothingAdapter) @@ -336,18 +343,11 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE)) - registerTypeAdapter(InventoryIcon.Companion) - - registerTypeAdapterFactory(IArmorItemDefinition.Frames.Factory) registerTypeAdapterFactory(AssetPath.Companion) registerTypeAdapter(SpriteReference.Companion) registerTypeAdapterFactory(AssetReference.Companion) - registerTypeAdapter(ItemStack.Adapter(this@Starbound)) - - registerTypeAdapterFactory(TreasurePoolDefinition.Companion) - registerTypeAdapterFactory(UniverseChunk.Companion) registerTypeAdapter(Image.Companion) @@ -375,51 +375,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca create() } - data class ItemConfig( - val directory: String?, - val parameters: JsonObject, - val config: JsonObject - ) - - fun itemConfig(name: String, parameters: JsonObject, level: Double? = null, seed: Long? = null): ItemConfig { - val obj = Registries.items[name] ?: throw NoSuchElementException("No such item $name") - - val directory = obj.file?.computeDirectory() - val parameters = parameters.deepCopy() - - TODO() - } - - fun item(name: String): ItemStack { - TODO() - } - - fun item(name: String, count: Long): ItemStack { - TODO() - } - - fun item(name: String, count: Long, parameters: JsonObject): ItemStack { - TODO() - } - - fun item(descriptor: JsonObject): ItemStack { - return item( - (descriptor["name"] as? JsonPrimitive)?.asString ?: return ItemStack.EMPTY, - descriptor["count"]?.asLong ?: return ItemStack.EMPTY, - (descriptor["parameters"] as? JsonObject)?.deepCopy() ?: JsonObject() - ) - } - - fun item(descriptor: JsonElement?): ItemStack { - if (descriptor is JsonPrimitive) { - return item(descriptor.asString) - } else if (descriptor is JsonObject) { - return item(descriptor) - } else { - return ItemStack.EMPTY - } - } - var initializing = false private set var initialized = false @@ -435,8 +390,12 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca var loaded = 0 private set - @Volatile - var terminateLoading = false + private val jsonAssetsCache = Caffeine.newBuilder() + .maximumSize(4096L) + .expireAfterAccess(Duration.ofMinutes(5L)) + .scheduler(this) + .executor(EXECUTOR) + .build>() fun loadJsonAsset(path: String): JsonElement? { val filename: String @@ -450,24 +409,84 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca jsonPath = null } - val file = locate(filename) + val json = jsonAssetsCache.get(filename) { + val file = locate(it) - if (!file.isFile) - return null + if (!file.isFile) + return@get KOptional() + + val findPatches = locateAll("$filename.patch") + KOptional(JsonPatch.apply(ELEMENTS_ADAPTER.read(file.jsonReader()), findPatches)) + }.orNull() ?: return null val pathTraverser = if (jsonPath == null) JsonPath.EMPTY else JsonPath.query(jsonPath) - return pathTraverser.get(gson.fromJson(file.reader(), JsonElement::class.java)) + return pathTraverser.get(json) } - private val archivePaths = ArrayList() private val fileSystems = ArrayList() + private val toLoadPaks = ObjectArraySet() + private val toLoadPaths = ObjectArraySet() - fun addFilePath(path: File) { - fileSystems.add(PhysicalFile(path)) + var loadingProgressText: String = "" + private set + + fun addArchive(path: File) { + toLoadPaks.add(path) } - fun addPak(pak: StarboundPak) { - fileSystems.add(pak.root) + fun addPath(path: File) { + toLoadPaths.add(path) + } + + fun doBootstrap() { + if (!bootstrapped && !bootstrapping) { + bootstrapping = true + } else { + return + } + + val fileSystems = ArrayList>() + + for (path in toLoadPaks) { + LOGGER.info("Reading PAK archive $path") + + try { + loadingProgressText = "Indexing $path" + val pak = StarboundPak(path) { _, s -> loadingProgressText = "Indexing $path: $s" } + val priority = pak.metadata.get("priority", 0L) + fileSystems.add(priority to pak.root) + } catch (err: Throwable) { + LOGGER.error("Error reading PAK archive $path. Not a PAK archive?") + } + } + + for (path in toLoadPaths) { + val metadata: JsonObject + val metadataPath = File(path, "_metadata") + + if (metadataPath.exists() && metadataPath.isFile) { + metadata = gson.fromJson(JsonReader(metadataPath.reader()), JsonObject::class.java) + } else { + metadata = JsonObject() + } + + val priority = metadata.get("priority", 0L) + fileSystems.add(priority to PhysicalFile(path)) + } + + fileSystems.sortByDescending { it.first } + + for ((_, fs) in fileSystems) { + this.fileSystems.add(fs) + } + + LOGGER.info("Finished reading PAK archives") + bootstrapped = true + bootstrapping = false + } + + fun bootstrapGame(): CompletableFuture<*> { + return submit { doBootstrap() } } override fun exists(path: String): Boolean { @@ -506,40 +525,25 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca return NonExistingFile(path.split("/").last(), fullPath = path) } - fun locate(vararg path: String): IStarboundFile { - for (p in path) { - val get = locate(p) + fun locateAll(path: String): List { + @Suppress("name_shadowing") + var path = path - if (get.exists) { - return get + if (path[0] == '/') { + path = path.substring(1) + } + + val files = ArrayList() + + for (fs in fileSystems.asReversed()) { + val file = fs.locate(path) + + if (file.exists) { + files.add(file) } } - return NonExistingFile(path[0].split("/").last(), fullPath = path[0]) - } - - /** - * Добавляет pak к чтению при initializeGame - */ - fun addPakPath(pak: File) { - archivePaths.add(pak) - } - - fun doBootstrap() { - if (!bootstrapped && !bootstrapping) { - bootstrapping = true - } else { - return - } - - for (path in archivePaths) { - LOGGER.info("Reading PAK archive $path") - addPak(StarboundPak(path)) - } - - LOGGER.info("Finished reading PAK archives") - bootstrapped = true - bootstrapping = false + return files } private fun doInitialize() { @@ -551,46 +555,34 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca doBootstrap() - val ext2files = fileSystems.parallelStream() - .flatMap { it.explore() } - .filter { it.isFile } - .collect(object : - Collector>, Map>> - { - override fun supplier(): Supplier>> { - return Supplier { Object2ObjectOpenHashMap() } - } + loadingProgressText = "Building file tree..." + val fileTree = HashMap>() + val patchTree = HashMap>() - override fun accumulator(): BiConsumer>, IStarboundFile> { - return BiConsumer { t, u -> - t.computeIfAbsent(u.name.substringAfterLast('.'), Object2ObjectFunction { ArrayList() }).add(u) - } - } + // finding assets, assets originating from top-most priority PAKs are overriding + // same assets from other PAKs + fileSystems.forEach { + it.explore { file -> + if (file.isFile) + fileTree.computeIfAbsent(file.name.substringAfterLast('.')) { HashSet() }.add(file) + } + } - override fun combiner(): BinaryOperator>> { - return BinaryOperator { t, u -> - for ((k, v) in u) - t.computeIfAbsent(k, Object2ObjectFunction { ArrayList() }).addAll(v) - - t - } - } - - override fun finisher(): Function>, Map>> { - return Function { it } - } - - override fun characteristics(): Set { - return setOf(Collector.Characteristics.IDENTITY_FINISH) - } - }) + // finding asset patches, patches from bottom-most priority PAKs are applied first + fileSystems.asReversed().forEach { + it.explore { file -> + if (file.isFile && file.name.endsWith(".patch")) + patchTree.computeIfAbsent(file.computeFullPath().substringAfterLast('.')) { ArrayList() }.add(file) + } + } + loadingProgressText = "Dispatching load tasks..." val tasks = ArrayList>() - tasks.addAll(Registries.load(ext2files)) - tasks.addAll(RecipeRegistry.load(ext2files)) + tasks.addAll(Registries.load(fileTree, patchTree)) + tasks.addAll(RecipeRegistry.load(fileTree, patchTree)) tasks.addAll(Globals.load()) - tasks.add(VersionRegistry.load()) + tasks.add(VersionRegistry.load(patchTree)) val total = tasks.size.toDouble() toLoad = tasks.size @@ -599,11 +591,13 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca tasks.removeIf { it.isDone } loaded = toLoad - tasks.size loadingProgress = (total - tasks.size) / total + loadingProgressText = "Loading JSON assets, $loaded / $toLoad" LockSupport.parkNanos(5_000_000L) } Registries.finishLoad() RecipeRegistry.finishLoad() + ItemRegistry.finishLoad() Registries.validate() @@ -615,10 +609,6 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca return submit { doInitialize() } } - fun bootstrapGame(): CompletableFuture<*> { - return submit { doBootstrap() } - } - private var fontPath: File? = null fun loadFont(): CompletableFuture { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt index b6a53a93..e84c7edc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound import com.google.common.collect.ImmutableMap import com.google.gson.stream.JsonReader import ru.dbotthepony.kstarbound.io.StarboundPak +import ru.dbotthepony.kstarbound.util.sbIntern import java.io.BufferedInputStream import java.io.File import java.io.FileNotFoundException @@ -48,6 +49,9 @@ fun interface ISBFileLocator { fun jsonReader(path: String) = locate(path).jsonReader() } +/** + * Two files should be considered equal if they have same absolute path + */ interface IStarboundFile : ISBFileLocator { val exists: Boolean val isDirectory: Boolean @@ -75,6 +79,11 @@ interface IStarboundFile : ISBFileLocator { return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() }) } + fun explore(visitor: (IStarboundFile) -> Unit) { + visitor(this) + children?.values?.forEach { it.explore(visitor) } + } + fun computeFullPath(): String { var path = name var parent = parent @@ -158,7 +167,7 @@ interface IStarboundFile : ISBFileLocator { * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ - @Deprecated("This does not reflect json patches") + @Deprecated("Careful! This does not reflect json patches") fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true } /** @@ -253,29 +262,44 @@ fun getPathFilename(path: String): String { return path.substringAfterLast('/') } -class PhysicalFile(val real: File) : IStarboundFile { +class PhysicalFile(val real: File, override val parent: PhysicalFile? = null) : IStarboundFile { override val exists: Boolean get() = real.exists() override val isDirectory: Boolean get() = real.isDirectory - override val parent: PhysicalFile? - get() { - return PhysicalFile(real.parentFile ?: return null) - } override val isFile: Boolean get() = real.isFile override val children: Map? get() { - return real.list()?.stream()?.map { it to PhysicalFile(File(it)) }?.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + return real.list()?.associate { it to PhysicalFile(File(it), this) } } override val name: String get() = real.name + private val fullPatch by lazy { super.computeFullPath().sbIntern() } + private val directory by lazy { super.computeDirectory().sbIntern() } + + override fun computeFullPath(): String { + return fullPatch + } + + override fun computeDirectory(): String { + return directory + } + override fun open(): InputStream { return BufferedInputStream(real.inputStream()) } + override fun equals(other: Any?): Boolean { + return other is IStarboundFile && computeFullPath() == other.computeFullPath() + } + + override fun hashCode(): Int { + return computeFullPath().hashCode() + } + override fun toString(): String { return "PhysicalFile[$real]" } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt index 7d15ce9c..c80b9753 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/VersionRegistry.kt @@ -31,7 +31,7 @@ object VersionRegistry { private val adapter by lazy { Starbound.gson.getAdapter(VersionedJson::class.java) } - fun load(): Future<*> { + fun load(patchTree: Map>): Future<*> { return Starbound.EXECUTOR.submit(Runnable { val json = Starbound.loadJsonAsset("/versioning.config") ?: throw NoSuchElementException("Unable to load /versioning.config! Expect HUGE problems!") json as JsonObject diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index ff6dfc85..931e2463 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -38,9 +38,6 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn client.mailbox.execute { task.invoke(client) } } - override fun inGame() { - } - override fun toString(): String { val channel = if (hasChannel) channel.remoteAddress().toString() else "" return "ClientConnection[ID=$connectionID channel=$channel]" diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index 1272db60..34e900d1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -666,7 +666,7 @@ class StarboundClient private constructor(val clientID: Int) : BlockableEventLoo builder.builder.quad(0f, viewportHeight - 20f, viewportWidth * Starbound.loadingProgress.toFloat(), viewportHeight.toFloat()) { color(RGBAColor.GREEN) } - GLFW.glfwSetWindowTitle(window, "KStarbound: Loading JSON assets ${Starbound.loaded} / ${Starbound.toLoad}") + GLFW.glfwSetWindowTitle(window, "KStarbound: ${Starbound.loadingProgressText}") renderedLoadingScreen = true val runtime = Runtime.getRuntime() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt index 0ff166fa..801d2ee0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt @@ -8,6 +8,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.sbIntern data class AssetPath(val path: String, val fullPath: String) { constructor(path: String) : this(path, path) @@ -28,7 +29,7 @@ data class AssetPath(val path: String, val fullPath: String) { override fun read(`in`: JsonReader): AssetPath? { val path = strings.read(`in`) ?: return null if (path == "") return null - return AssetPath(path, AssetPathStack.remap(path)) + return AssetPath(path.sbIntern(), AssetPathStack.remap(path).sbIntern()) } } as TypeAdapter } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt index 1a92b33d..21d9c16d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt @@ -15,6 +15,7 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.sbIntern import java.lang.reflect.ParameterizedType import java.util.* import java.util.concurrent.ConcurrentHashMap @@ -64,7 +65,6 @@ class AssetReference { private val cache = Collections.synchronizedMap(HashMap>()) private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter private val strings = gson.getAdapter(String::class.java) - private val jsons = gson.getAdapter(JsonElement::class.java) private val missing = Collections.synchronizedSet(ObjectOpenHashSet()) private val logger = LogManager.getLogger() @@ -84,7 +84,7 @@ class AssetReference { val get = cache[fullPath] if (get != null) - return AssetReference(path, fullPath, get.first, get.second) + return AssetReference(path.sbIntern(), fullPath.sbIntern(), get.first, get.second) if (fullPath in missing) return null @@ -92,9 +92,10 @@ class AssetReference { val json = Starbound.loadJsonAsset(fullPath) if (json == null) { - logger.error("JSON asset does not exist: $fullPath") - missing.add(fullPath) - return AssetReference(path, fullPath, null, null) + if (missing.add(fullPath)) + logger.error("JSON asset does not exist: $fullPath") + + return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, null) } val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) { @@ -103,13 +104,13 @@ class AssetReference { if (value == null) { missing.add(fullPath) - return AssetReference(path, fullPath, null, json) + return AssetReference(path.sbIntern(), fullPath.sbIntern(), null, json) } cache[fullPath] = value to json - return AssetReference(path, fullPath, value, json) + return AssetReference(path.sbIntern(), fullPath.sbIntern(), value, json) } else { - val json = jsons.read(`in`)!! + val json = Starbound.ELEMENTS_ADAPTER.read(`in`) val value = adapter.read(JsonTreeReader(json)) ?: return null return AssetReference(null, null, value, json) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt index 0d43363c..9d06e0b6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt @@ -1,9 +1,6 @@ package ru.dbotthepony.kstarbound.defs -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition - data class CurrencyDefinition( - val representativeItem: Registry.Ref, + val representativeItem: String, val playerMax: Long = 999999, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt index d3c0c22a..923782d9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Drawable.kt @@ -22,6 +22,9 @@ import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.math.Line2d @JsonAdapter(Drawable.Adapter::class) @@ -43,6 +46,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig override fun flop(): Drawable { TODO("Not yet implemented") } + + override fun boundingBox(cropImages: Boolean): AABB { + TODO("Not yet implemented") + } + + override fun scale(scale: Vector2f): Drawable { + TODO("Not yet implemented") + } + + override fun translate(translation: Vector2f): Drawable { + TODO("Not yet implemented") + } } class Poly( @@ -58,6 +73,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig override fun flop(): Drawable { TODO("Not yet implemented") } + + override fun boundingBox(cropImages: Boolean): AABB { + TODO("Not yet implemented") + } + + override fun scale(scale: Vector2f): Drawable { + TODO("Not yet implemented") + } + + override fun translate(translation: Vector2f): Drawable { + TODO("Not yet implemented") + } } class Image( @@ -81,35 +108,75 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig return Image(path, transform.flatMap({ it.copy().scale(-1f, 1f) }, { it.copy(mirrored = !it.mirrored) }), position, color, fullbright) } + override fun scale(scale: Vector2f): Drawable { + return Image(path, transform.flatMap({ it.scale(scale) }, { it.copy(scale = Either.right(it.scale.map({ scale * it }, { scale * it }))) }), position, color, fullbright) + } + + override fun translate(translation: Vector2f): Drawable { + return Image(path, transform, position + translation, color, fullbright) + } + + private fun getTransforms(sprite: ru.dbotthepony.kstarbound.defs.image.Image.Sprite) = transform.map({ it.copy() }, { + val mat = Matrix3f.identity() + + it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) }) + + if (it.centered) { + mat.translate(sprite.width / -2f, sprite.height / -2f) + } + + if (it.rotation != 0f) { + mat.rotateAroundZ(it.rotation) + } + + if (it.mirrored) { + mat.translate(sprite.width.toFloat(), 0f) + mat.scale(-1f, 1f) + } + + mat + }) + + // Original engine here calculates bounding box wrong + // if "cropImages" is false + override fun boundingBox(cropImages: Boolean): AABB { + val sprite = path.sprite ?: return AABB.ZERO + var result: AABB + + if (cropImages) { + result = AABB( + Vector2d(sprite.nonEmptyRegion.x.toDouble(), sprite.nonEmptyRegion.y.toDouble()), + Vector2d(sprite.nonEmptyRegion.z.toDouble(), sprite.nonEmptyRegion.w.toDouble()), + ) + } else { + result = AABB( + Vector2d.ZERO, + Vector2d(sprite.width.toDouble(), sprite.height.toDouble()) + ) + } + + val transforms = getTransforms(sprite) + + result = result.expand((Vector2f(result.mins.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector()) + result = result.expand((Vector2f(result.mins.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector()) + result = result.expand((Vector2f(result.maxs.x.toFloat(), result.mins.y.toFloat()) * transforms).toDoubleVector()) + result = result.expand((Vector2f(result.maxs.x.toFloat(), result.maxs.y.toFloat()) * transforms).toDoubleVector()) + + result += position.toDoubleVector() + + return result + } + override fun render(client: StarboundClient, layer: IGeometryLayer, x: Float, y: Float) { val sprite = path.sprite ?: return val texture = path.image!!.texture val program = if (fullbright) client.programs.positionTexture else client.programs.positionTextureLightmap - val mat = transform.map({ it.copy() }, { - val mat = Matrix3f.identity() - - it.scale.map({ mat.scale(it / PIXELS_IN_STARBOUND_UNITf, it / PIXELS_IN_STARBOUND_UNITf) }, { mat.scale(it / PIXELS_IN_STARBOUND_UNITf) }) - - if (it.centered) { - mat.translate(sprite.width / -2f, sprite.height / -2f) - } - - if (it.rotation != 0f) { - mat.rotateAroundZ(it.rotation) - } - - if (it.mirrored) { - mat.translate(sprite.width.toFloat(), 0f) - mat.scale(-1f, 1f) - } - - mat - }) + val mat = getTransforms(sprite) val builder = layer.getBuilder(program.config(texture)) - mat.preTranslate(x, y) + mat.preTranslate(position.x + x, position.y + y) client.stack.last().mulIntoOther(mat) builder.mode(GeometryType.QUADS) @@ -126,6 +193,18 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig override fun flop(): Drawable { return this } + + override fun boundingBox(cropImages: Boolean): AABB { + return AABB.ZERO + } + + override fun scale(scale: Vector2f): Drawable { + return this + } + + override fun translate(translation: Vector2f): Drawable { + return Empty(position + translation, color, fullbright) + } } open fun with(values: (String) -> String?): Drawable { @@ -134,6 +213,19 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig abstract fun render(client: StarboundClient = StarboundClient.current(), layer: IGeometryLayer, x: Float = 0f, y: Float = 0f) + abstract fun boundingBox(cropImages: Boolean = false): AABB + + abstract fun scale(scale: Vector2f): Drawable + + fun scale(scale: Float): Drawable { + if (scale == 1f) + return this + + return scale(Vector2f(scale, scale)) + } + + abstract fun translate(translation: Vector2f): Drawable + /** * mirror along X axis */ @@ -141,12 +233,12 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig companion object { val EMPTY = Empty() + val CENTERED = Transformations(true) private val LOGGER = LogManager.getLogger() } class Adapter(gson: Gson) : TypeAdapter() { private val lines = gson.getAdapter(Line2d::class.java) - private val objects = gson.getAdapter(JsonObject::class.java) private val vectors = gson.getAdapter(Vector2f::class.java) private val vectors3 = gson.getAdapter(Vector3f::class.java) private val colors = gson.getAdapter(RGBAColor::class.java) @@ -162,7 +254,7 @@ sealed class Drawable(val position: Vector2f, val color: RGBAColor, val fullbrig if (`in`.consumeNull()) { return EMPTY } else { - val value = objects.read(`in`)!! + val value = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!! val position = value["position"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO val color = value["color"]?.let { colors.fromJsonTree(it) } ?: RGBAColor.WHITE diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt index 5e2ac2e2..62506d93 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt @@ -229,8 +229,6 @@ class Json2Function( override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == Json2Function::class.java) { return object : TypeAdapter() { - val elements = gson.getAdapter(JsonArray::class.java) - override fun write(out: JsonWriter, value: Json2Function?) { TODO("Not yet implemented") } @@ -247,7 +245,7 @@ class Json2Function( val xRanges = ArrayList() val yRanges = ArrayList() - val elements = elements.read(reader) + val elements = Starbound.ELEMENTS_ADAPTER.arrays.read(reader) for ((i, row) in elements.withIndex()) { if (row !is JsonArray) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt index 25628902..125702b6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Species.kt @@ -4,8 +4,8 @@ import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -38,8 +38,8 @@ data class Species( val characterImage: SpriteReference, val hairGroup: String? = null, val hair: ImmutableSet, - val shirt: ImmutableSet>, - val pants: ImmutableSet>, + val shirt: ImmutableSet, + val pants: ImmutableSet, val facialHairGroup: String? = null, val facialHair: ImmutableSet = ImmutableSet.of(), val facialMaskGroup: String? = null, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatusEffectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatusEffectDefinition.kt index 6bc81ebf..c79f9eb9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatusEffectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatusEffectDefinition.kt @@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory data class StatusEffectDefinition( val name: String, - val defaultDuration: Double, + val defaultDuration: Double = 0.0, val blockingStat: String? = null, val label: String? = null, val icon: SpriteReference? = null, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt index 9665f959..28184e0b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt @@ -12,6 +12,7 @@ import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec @@ -21,8 +22,6 @@ import java.io.DataOutputStream @JsonAdapter(StatModifier.Adapter::class) data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) { class Adapter(gson: Gson) : TypeAdapter() { - private val objects = gson.getAdapter(JsonObject::class.java) - override fun write(out: JsonWriter, value: StatModifier?) { if (value == null) { out.nullValue() @@ -40,7 +39,7 @@ data class StatModifier(val stat: String, val value: Double, val type: StatModif if (`in`.consumeNull()) { return null } else { - val read = objects.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val stat = read["stat"]?.asString ?: throw JsonSyntaxException("No stat to modify specified") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt index 47aa56b4..e3fb4a44 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt @@ -17,9 +17,9 @@ data class BagFilterConfig( return false if (typeBlacklist != null) { - return !typeBlacklist.contains(t.config.value!!.category) + return !typeBlacklist.contains(t.category) } else if (typeWhitelist != null) { - return typeWhitelist.contains(t.config.value!!.category) + return typeWhitelist.contains(t.category) } return true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BlueprintLearnList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BlueprintLearnList.kt index 90ed33cb..89ca6fbb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BlueprintLearnList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BlueprintLearnList.kt @@ -11,15 +11,14 @@ import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import ru.dbotthepony.kommons.gson.consumeNull -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.json.builder.JsonFactory class BlueprintLearnList private constructor(private val tiers: Int2ObjectArrayMap>) { constructor(tiers: Map>) : this(Int2ObjectArrayMap>().also { for ((k, v) in tiers.entries) it.put(k, ImmutableList.copyOf(v)) }) @JsonFactory - data class Entry(val item: Registry.Ref) + data class Entry(val item: ItemDescriptor) operator fun get(tier: Int): List { return tiers.getOrDefault(tier, ImmutableList.of()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/DeploymentConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/DeploymentConfig.kt index e4cd814c..9cee8db0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/DeploymentConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/DeploymentConfig.kt @@ -2,10 +2,9 @@ package ru.dbotthepony.kstarbound.defs.actor.player import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap -import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.IScriptable -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -13,8 +12,8 @@ data class DeploymentConfig( override val scripts: ImmutableList, override val scriptDelta: Int, - val starterMechSet: ImmutableMap>, - val speciesStarterMechBody: ImmutableMap>, + val starterMechSet: ImmutableMap, + val speciesStarterMechBody: ImmutableMap, val enemyDetectRadius: Double, val enemyDetectTypeNames: ImmutableList, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt index 29a99fc2..f247e0a3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt @@ -14,7 +14,7 @@ import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -27,11 +27,11 @@ data class PlayerConfig( val species: ImmutableSet>, val nametagColor: RGBAColor, val ageItemsEvery: Int, - val defaultItems: ImmutableSet>, + val defaultItems: ImmutableSet, val defaultBlueprints: BlueprintLearnList, - val defaultCodexes: ImmutableMap>>, + val defaultCodexes: ImmutableMap>, val metaBoundBox: AABB, val movementParameters: ActorMovementParameters, val zeroGMovementParameters: ActorMovementParameters, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/TechDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/TechDefinition.kt index c9a2672d..db8cce59 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/TechDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/TechDefinition.kt @@ -9,7 +9,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory data class TechDefinition( val name: String, val type: String, - val chipCost: Int, override val scriptDelta: Int = 1, override val scripts: ImmutableList ) : IScriptable diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt index ac30ad1a..d572e0c0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt @@ -52,7 +52,7 @@ data class AnimatedPartsDefinition( v.index = index++ if (v.mode == AnimationMode.TRANSITION && v.transition !in states) { - throw IllegalArgumentException("State $k has specified TRANSITION as mode, however, it points to non-existing state ${v.transition}!") + throw IllegalArgumentException("State '$k' has specified TRANSITION as mode, however, it points to non-existing state '${v.transition}'!") } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt index c2193758..938093d3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrush.kt @@ -12,6 +12,7 @@ import com.google.gson.stream.JsonWriter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials @@ -47,14 +48,12 @@ abstract class DungeonBrush { } class Adapter(gson: Gson) : TypeAdapter() { - private val arrays = gson.getAdapter(JsonArray::class.java) - override fun write(out: JsonWriter, value: DungeonBrush) { TODO("Not yet implemented") } override fun read(`in`: JsonReader): DungeonBrush { - val read = arrays.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`) // don't delegate to EnumAdapter since this can have bad consequences // such as defaulting to CLEAR action and printing a warning in console val type = DungeonBrushType.entries.firstOrNull { it.jsonName == read[0].asString } ?: throw NoSuchElementException("Unknown brush type ${read[0].asString}!") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt index b2592e79..f3dd4887 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonBrushType.kt @@ -158,10 +158,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable Starbound.gson.getAdapter(DungeonBrush.WorldObject.Extra::class.java) } - private val adapterObject by lazy { - Starbound.gson.getAdapter(JsonObject::class.java) - } - override fun createLegacy(json: JsonArray): DungeonBrush { if (json.size() > 2) { return DungeonBrush.WorldObject(adapter0.fromJsonTree(json[1]), adapter1.fromJsonTree(json[2])) @@ -183,7 +179,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable if (parameters == null || parameters.isJsonNull) parameters = JsonObject() else if (parameters is JsonPrimitive) - parameters = adapterObject.fromJson(parameters.asString) + parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString) return DungeonBrush.WorldObject(ref, direction, parameters as JsonObject) } @@ -251,10 +247,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable return DungeonBrush.NPC(json[1].asJsonObject) } - private val adapterObject by lazy { - Starbound.gson.getAdapter(JsonObject::class.java) - } - override fun readTiled(json: JsonObject): DungeonBrush? { if ("npc" in json) { val brush = JsonObject() @@ -278,7 +270,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable if (parameters == null || parameters.isJsonNull) parameters = JsonObject() else if (parameters is JsonPrimitive) - parameters = adapterObject.fromJson(parameters.asString) + parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString) brush["parameters"] = parameters return DungeonBrush.NPC(brush) @@ -299,7 +291,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable if (parameters == null || parameters.isJsonNull) parameters = JsonObject() else if (parameters is JsonPrimitive) - parameters = adapterObject.fromJson(parameters.asString) + parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString) brush["parameters"] = parameters return DungeonBrush.NPC(brush) @@ -314,10 +306,6 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable return DungeonBrush.Stagehand(json[1].asJsonObject) } - private val adapterObject by lazy { - Starbound.gson.getAdapter(JsonObject::class.java) - } - override fun readTiled(json: JsonObject): DungeonBrush? { if ("stagehand" in json) { val brush = JsonObject() @@ -329,7 +317,7 @@ enum class DungeonBrushType(override val jsonName: String) : IStringSerializable if (parameters == null || parameters.isJsonNull) parameters = JsonObject() else if (parameters is JsonPrimitive) - parameters = adapterObject.fromJson(parameters.asString) + parameters = Starbound.ELEMENTS_ADAPTER.objects.fromJson(parameters.asString) brush["parameters"] = parameters diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt index f365b518..b02e902e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonRule.kt @@ -11,6 +11,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.isEmptyLiquid import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyLiquid @@ -332,7 +333,6 @@ abstract class DungeonRule { } class Adapter(gson: Gson) : TypeAdapter() { - private val arrays = gson.getAdapter(JsonArray::class.java) private val types = gson.getAdapter(Type::class.java) override fun write(out: JsonWriter, value: DungeonRule) { @@ -340,7 +340,7 @@ abstract class DungeonRule { } override fun read(`in`: JsonReader): DungeonRule { - val read = arrays.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.arrays.read(`in`) if (read.isEmpty) { throw JsonSyntaxException("Empty rule") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt index 5b4a4ebe..7cfb6884 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonTile.kt @@ -98,7 +98,6 @@ data class DungeonTile( // weird custom parsing rules but ok class Adapter(gson: Gson) : TypeAdapter() { - private val objects = gson.getAdapter(JsonObject::class.java) private val data = gson.getAdapter(BasicData::class.java) private val values = gson.getAdapter>() @@ -119,7 +118,7 @@ data class DungeonTile( } override fun read(`in`: JsonReader): DungeonTile { - val read = objects.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val (brushes, rules, direction, rawIndex, connector, connectForwardOnly) = data.fromJsonTree(read) var connectorIndex = rawIndex diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 60eb9066..c737881d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -490,13 +490,17 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar parent.eventLoop.supplyAsync { for ((obj, direction) in placedObjects) { - val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction) + try { + val orientation = obj!!.config.value.findValidOrientation(parent, obj.tilePosition, direction) - if (orientation != -1) { - obj.orientationIndex = orientation.toLong() - obj.joinWorld(parent) - } else { - LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") + if (orientation != -1) { + obj.orientationIndex = orientation.toLong() + obj.joinWorld(parent) + } else { + LOGGER.error("Tried to place object ${obj.config.key} at ${obj.tilePosition}, but it can't be placed there!") + } + } catch (err: Throwable) { + LOGGER.error("Exception while putting dungeon object $obj at ${obj!!.tilePosition}", err) } } }.await() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt index 97090bd8..23995baa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/ImagePartReader.kt @@ -28,34 +28,18 @@ class ImagePartReader(part: DungeonPart, val images: ImmutableList) : Par for ((i, image) in images.withIndex()) { // go around image cache, since image will be loaded exactly once // and then forgotten - val (bytes, width, height, channels) = Image.readImageDirect(image.source) + val (bytes, width, height) = Image.readImageDirect(image.source) val tileData = IntArray(width * height) - if (channels == 3) { - // RGB - for (x in 0 until image.width) { - for (y in 0 until image.height) { - val offset = (x + y * image.width) * channels + for (x in 0 until image.width) { + for (y in 0 until image.height) { + val offset = (x + y * image.width) * 4 - // flip image as we go - tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or - bytes[offset + 1].toInt().and(0xFF).shl(8) or - bytes[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255 - } - } - } else if (channels == 4) { - // RGBA - - for (x in 0 until image.width) { - for (y in 0 until image.height) { - val offset = (x + y * image.width) * channels - - // flip image as we go - tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or - bytes[offset + 1].toInt().and(0xFF).shl(8) or - bytes[offset + 2].toInt().and(0xFF).shl(16) or - bytes[offset + 3].toInt().and(0xFF).shl(24) - } + // flip image as we go + tileData[x + (image.height - y - 1) * image.width] = bytes[offset].toInt().and(0xFF) or + bytes[offset + 1].toInt().and(0xFF).shl(8) or + bytes[offset + 2].toInt().and(0xFF).shl(16) or + bytes[offset + 3].toInt().and(0xFF).shl(24) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index efe40fda..c6a97f32 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -36,6 +36,7 @@ import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getObject +import ru.dbotthepony.kstarbound.json.JsonPatch import java.io.BufferedInputStream import java.io.FileNotFoundException import java.lang.ref.Reference @@ -53,13 +54,11 @@ class Image private constructor( val path: String, val width: Int, val height: Int, - val amountOfChannels: Int, spritesData: Pair, IStarboundFile>? ) { init { check(width >= 0) { "Invalid width $width" } check(height >= 0) { "Invalid height $height" } - check(amountOfChannels in 1 .. 4) { "Unknown number of channels $amountOfChannels" } } private val spritesInternal = LinkedHashMap() @@ -139,17 +138,10 @@ class Image private constructor( val value = client.named2DTextures0.get(this) { client.named2DTextures1.get(this) { - val (memFormat, fileFormat) = when (amountOfChannels) { - 1 -> GL45.GL_R8 to GL45.GL_RED - 3 -> GL45.GL_RGB8 to GL45.GL_RGB - 4 -> GL45.GL_RGBA8 to GL45.GL_RGBA - else -> throw IllegalArgumentException("Unknown amount of channels in $it: $amountOfChannels") - } - - val tex = GLTexture2D(width, height, memFormat) + val tex = GLTexture2D(width, height, GL45.GL_RGBA8) data.thenApplyAsync({ - tex.upload(fileFormat, GL45.GL_UNSIGNED_BYTE, it) + tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, it) tex.textureMinFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST @@ -211,26 +203,13 @@ class Image private constructor( operator fun get(x: Int, y: Int): Int { require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } - val offset = (this.y + y) * this@Image.width * amountOfChannels + (this.x + x) * amountOfChannels + val offset = (this.y + y) * this@Image.width * 4 + (this.x + x) * 4 val data = data.join() - when (amountOfChannels) { - 4 -> return data[offset].toInt().and(0xFF) or // red - data[offset + 1].toInt().and(0xFF).shl(8) or // green - data[offset + 2].toInt().and(0xFF).shl(16) or // blue - data[offset + 3].toInt().and(0xFF).shl(24) // alpha - - 3 -> return data[offset].toInt().and(0xFF) or - data[offset + 1].toInt().and(0xFF).shl(8) or - data[offset + 2].toInt().and(0xFF).shl(16) or -0x1000000 // leading alpha as 255 - - 2 -> return data[offset].toInt().and(0xFF) or - data[offset + 1].toInt().and(0xFF).shl(8) - - 1 -> return data[offset].toInt() - - else -> throw IllegalStateException() - } + return data[offset].toInt().and(0xFF) or // red + data[offset + 1].toInt().and(0xFF).shl(8) or // green + data[offset + 2].toInt().and(0xFF).shl(16) or // blue + data[offset + 3].toInt().and(0xFF).shl(24) // alpha } /** @@ -248,47 +227,44 @@ class Image private constructor( fun isTransparent(x: Int, y: Int, flip: Boolean): Boolean { if (x !in 0 until width) return true if (y !in 0 until height) return true - if (amountOfChannels != 4) return false return this[x, y, flip] and -0x1000000 == 0x0 } val nonEmptyRegion by lazy { - if (amountOfChannels == 4) { - var x0 = 0 - var y0 = 0 + var x0 = 0 + var y0 = 0 - search@for (y in 0 until height) { - for (x in 0 until width) { - if (this[x, y] and 0xFF != 0x0) { - x0 = x - y0 = y - break@search - } + search@for (y in 0 until height) { + for (x in 0 until width) { + if (!isTransparent(x, y, false)) { + x0 = x + y0 = y + break@search } } - - var x1 = x0 - var y1 = y0 - - search@for (y in height - 1 downTo y0) { - for (x in width - 1 downTo x0) { - if (this[x, y] and 0xFF != 0x0) { - x1 = x - y1 = y - break@search - } - } - } - - return@lazy Vector4i(x0, y0, x1, y1) } - Vector4i(0, 0, width, height) + var x1 = x0 + var y1 = y0 + + search@for (y in height - 1 downTo y0) { + for (x in width - 1 downTo x0) { + if (!isTransparent(x, y, false)) { + x1 = x + y1 = y + break@search + } + } + } + + return@lazy Vector4i(x0, y0, x1, y1) + } + + val imageNonEmptyRegion by lazy { + nonEmptyRegion + Vector4i(x, y, x, y) } fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set { - if (amountOfChannels != 3 && amountOfChannels != 4) throw IllegalStateException("Can not check world space taken by image with $amountOfChannels color channels") - val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi @@ -335,7 +311,6 @@ class Image private constructor( companion object : TypeAdapter() { private val LOGGER = LogManager.getLogger() - private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } private val vectors by lazy { Starbound.gson.getAdapter(Vector4i::class.java) } private val vectors2 by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val configCache = ConcurrentHashMap, IStarboundFile>>>() @@ -354,7 +329,7 @@ class Image private constructor( val data = STBImage.stbi_load_from_memory( idata, getWidth, getHeight, - components, 0 + components, 4 ) ?: throw IllegalArgumentException("File $file is not an image or it is corrupted") Reference.reachabilityFence(idata) @@ -432,7 +407,7 @@ class Image private constructor( if (!status) throw IllegalArgumentException("File $file is not an image or it is corrupted") - Optional.of(Image(file, it, getWidth[0], getHeight[0], components[0], getConfig(it))) + Optional.of(Image(file, it, getWidth[0], getHeight[0], getConfig(it))) } catch (err: Exception) { logger.error("Failed to load image at path $it", err) Optional.empty() @@ -534,12 +509,17 @@ class Image private constructor( } private fun compute(it: String): Optional, IStarboundFile>> { - val find = Starbound.locate("$it.frames") + val locate = Starbound.locate("$it.frames") - if (!find.exists) { + if (!locate.exists) + return Optional.empty() + + val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(locate.jsonReader()), Starbound.locateAll("$it.frames.patch")) + + if (json !is JsonObject) { return Optional.empty() } else { - return Optional.of(parseFrames(objects.read(JsonReader(find.reader()).also { it.isLenient = true })) to find) + return Optional.of(parseFrames(json) to locate) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt index dc3051d6..55b3450b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/SpriteReference.kt @@ -10,10 +10,22 @@ import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.SBPattern +// TODO: While this class creates proper interface and provides caching for +// image/sprite resolution, it, however, does not match original engine behavior. +// Original engine first resolves entire path as pattern (through String.replaceTags) +// and only then determine what is image path, what is sprite and do we have render directives. +// tl;dr: for next string .png:thing. +// and next inputs: image=/something frame=4 directives=?hueshift=4 +// old and new engines will disagree what to do +// old engine will first resolve tags: /something.png:thing.4?hueshift=4 +// and then parse the string (image = /something.png, sprite = thing.4, directives = hueshift=4) +// our engine, however, will first parse the string, and then resolve tags, resulting into +// sprite being "thing.4?hueshift=4" and directives being empty class SpriteReference private constructor( val raw: AssetPath, val imagePath: SBPattern, val spritePath: SBPattern?, + val renderDirectives: SBPattern?, val image: Image?, ) { val sprite by lazy { @@ -28,17 +40,18 @@ class SpriteReference private constructor( fun with(values: (String) -> String?): SpriteReference { val imagePath = this.imagePath.with(values) val spritePath = this.spritePath?.with(values) + val renderDirectives = this.renderDirectives?.with(values) - if (imagePath != this.imagePath || spritePath != this.spritePath) { + if (imagePath != this.imagePath || spritePath != this.spritePath || renderDirectives != this.renderDirectives) { if (imagePath != this.imagePath) { val resolved = imagePath.value if (resolved == null) - return SpriteReference(raw, imagePath, spritePath, null) + return SpriteReference(raw, imagePath, spritePath, renderDirectives, null) else - return SpriteReference(raw, imagePath, spritePath, Image.get(resolved)) + return SpriteReference(raw, imagePath, spritePath, renderDirectives, Image.get(resolved)) } else { - return SpriteReference(raw, imagePath, spritePath, image) + return SpriteReference(raw, imagePath, spritePath, renderDirectives, image) } } @@ -58,11 +71,21 @@ class SpriteReference private constructor( } override fun toString(): String { - return "SpriteReference[$imagePath:$spritePath]" + val image = imagePath.value ?: imagePath.raw + val sprite = spritePath?.value ?: spritePath?.raw ?: "" + val directives = renderDirectives?.value ?: renderDirectives?.raw ?: "" + + if (sprite == "" && directives == "") { + return image + } else if (sprite != "" && directives == "") { + return "$image:$sprite" + } else { + return "$image:$sprite?$directives" + } } companion object : TypeAdapter() { - val NEVER = SpriteReference(AssetPath("", ""), SBPattern.EMPTY, null, null) + val NEVER = SpriteReference(AssetPath("", ""), SBPattern.EMPTY, null, null, null) private val strings by lazy { Starbound.gson.getAdapter(String::class.java) } @@ -75,20 +98,52 @@ class SpriteReference private constructor( if (path == "") return NEVER - val split = path.split(':') + val split = path.lowercase().split(':') if (split.size > 2) { throw JsonSyntaxException("Ambiguous image reference: $path") } - val imagePath = if (split.size == 2) SBPattern.of(split[0]) else SBPattern.of(path) - val spritePath = if (split.size == 2) SBPattern.of(split[1]) else null + val imagePath: SBPattern + + val renderDirectives: SBPattern? + val spritePath: SBPattern? + + if (split.size == 2) { + imagePath = SBPattern.of(split[0]) + + spritePath = if ('?' in split[1]) { + // has render directives + val sprite = split[1].substringBefore('?') + val directives = split[1].substringAfter('?') + renderDirectives = SBPattern.of(directives) + SBPattern.of(sprite) + } else { + // no render directives + renderDirectives = null + SBPattern.of(split[1]) + } + } else { + imagePath = if ('?' in split[0]) { + // has render directives + val image = split[0].substringBefore('?') + val directives = split[0].substringAfter('?') + renderDirectives = SBPattern.of(directives) + SBPattern.of(image) + } else { + // no render directives + renderDirectives = null + SBPattern.of(split[0]) + } + + spritePath = null + } if (imagePath.isPlainString) { - val remapped = AssetPathStack.remap(split[0]) - return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, Image.get(remapped)) + val remapped = AssetPathStack.remap(imagePath.value!!) + return SpriteReference(AssetPath(path, AssetPathStack.remap(path)), SBPattern.raw(remapped), spritePath, renderDirectives, Image.get(remapped)) } else { - return SpriteReference(AssetPath(path, path), imagePath, spritePath, null) + return SpriteReference(AssetPath(path, path), imagePath, spritePath, renderDirectives, null) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IInventoryIcon.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IInventoryIcon.kt deleted file mode 100644 index 211c11a8..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/IInventoryIcon.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item - -import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation - -@JsonImplementation(InventoryIcon::class) -interface IInventoryIcon { - val image: SpriteReference -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt deleted file mode 100644 index 7545e177..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ILeveledStatusEffect.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item - -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation - -@JsonImplementation(LeveledStatusEffect::class) -interface ILeveledStatusEffect { - val levelFunction: String - val stat: String - val baseMultiplier: Double - val amount: Double -} - -@JsonFactory -data class LeveledStatusEffect( - override val levelFunction: String, - override val stat: String, - override val baseMultiplier: Double = 1.0, - override val amount: Double = 0.0, -) : ILeveledStatusEffect diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt deleted file mode 100644 index 807c0ca9..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt +++ /dev/null @@ -1,41 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item - -import com.google.gson.JsonPrimitive -import com.google.gson.TypeAdapter -import com.google.gson.internal.bind.JsonTreeReader -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kommons.gson.consumeNull -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.util.AssetPathStack - -data class InventoryIcon( - override val image: SpriteReference -) : IInventoryIcon { - companion object : TypeAdapter() { - private val adapter by lazy { FactoryAdapter.createFor(InventoryIcon::class, Starbound.gson) } - private val images by lazy { Starbound.gson.getAdapter(SpriteReference::class.java) } - - override fun write(out: JsonWriter, value: InventoryIcon?) { - if (value == null) - out.nullValue() - else - adapter.write(out, value) - } - - override fun read(`in`: JsonReader): InventoryIcon? { - if (`in`.consumeNull()) - return null - - if (`in`.peek() == JsonToken.STRING) { - return InventoryIcon(images.read(JsonTreeReader(JsonPrimitive(AssetPathStack.remap(`in`.nextString()))))) - } - - return adapter.read(`in`) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index 4b7ceb9d..3899bfae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -11,6 +11,7 @@ import com.google.gson.TypeAdapter import com.google.gson.annotations.JsonAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import org.apache.logging.log4j.LogManager import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table @@ -21,26 +22,29 @@ import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeVarLong -import ru.dbotthepony.kstarbound.Registries -import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.VersionRegistry -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.indexNoYield import ru.dbotthepony.kstarbound.lua.toJson +import ru.dbotthepony.kstarbound.lua.toJsonFromLua import java.io.DataInputStream import java.io.DataOutputStream import java.util.function.Supplier +import java.util.random.RandomGenerator private val EMPTY_JSON = JsonObject() @@ -151,11 +155,9 @@ data class ItemDescriptor( val count: Long, val parameters: JsonObject = EMPTY_JSON ) { - constructor(ref: Registry.Ref, count: Long, parameters: JsonObject) : this(ref.key.left(), count, parameters) - val isEmpty get() = count <= 0L || name == "" || ref.isEmpty val isNotEmpty get() = !isEmpty - val ref by lazy { if (name == "") Registries.items.emptyRef else Registries.items.ref(name) } + val ref by lazy { if (name == "") ItemRegistry.AIR else ItemRegistry[name] } override fun toString(): String { return "ItemDescriptor[$name, $count, $parameters]" @@ -195,6 +197,40 @@ data class ItemDescriptor( } } + fun build(level: Double? = null, seed: Long? = null, random: RandomGenerator? = null): ItemDescriptor { + val builder = ref.json["builder"]?.asString ?: return this + + try { + val lua = LuaEnvironment() + lua.attach(Starbound.loadScript(builder)) + lua.random = random ?: lua.random + lua.init(false) + + val (config, parameters) = lua.invokeGlobal("build", ref.directory, lua.from(ref.json), lua.from(parameters), level, seed) + + // we don't care about "config" here since it is treated equally by code as "parameters" + val jConfig = toJsonFromLua(config).asJsonObject + val jParameters = toJsonFromLua(parameters).asJsonObject + + // so, lets promote changed "config" parameters to item parameters + for ((k, v) in jConfig.entrySet()) { + if (ref.json[k] != v) { + if (k !in jParameters) { + jParameters[k] = v + } else { + // existing parameters override changed config parameters + jParameters[k] = mergeJson(v, jParameters[k]) + } + } + } + + return ItemDescriptor(name, count, jParameters) + } catch (err: Throwable) { + LOGGER.error("Error while generating randomized item '$name' using script $builder", err) + return this + } + } + fun write(stream: DataOutputStream) { stream.writeBinaryString(name) stream.writeVarLong(count.coerceAtLeast(0L)) @@ -202,8 +238,6 @@ data class ItemDescriptor( } class Adapter(gson: Gson) : TypeAdapter() { - private val elements = gson.getAdapter(JsonElement::class.java) - override fun write(out: JsonWriter, value: ItemDescriptor) { if (value.isEmpty) out.nullValue() @@ -212,12 +246,13 @@ data class ItemDescriptor( } override fun read(`in`: JsonReader): ItemDescriptor { - return ItemDescriptor(elements.read(`in`)) + return ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`)) } } companion object { val EMPTY = ItemDescriptor("", 0) val CODEC = StreamCodec.Impl(::ItemDescriptor, { a, b -> b.write(a) }) + private val LOGGER = LogManager.getLogger() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemGlobalConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemGlobalConfig.kt new file mode 100644 index 00000000..29989452 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemGlobalConfig.kt @@ -0,0 +1,14 @@ +package ru.dbotthepony.kstarbound.defs.item + +import com.google.common.collect.ImmutableSet +import ru.dbotthepony.kstarbound.defs.AssetPath + +data class ItemGlobalConfig( + val defaultMaxStack: Long, + val defaultPrice: Long, + val pickupSounds: ImmutableSet, + val defaultTimeToLive: Double, + val missingIcon: AssetPath, +) { + val pickupSoundsAbsolute: ImmutableSet = pickupSounds.stream().map { it.fullPath }.collect(ImmutableSet.toImmutableSet()) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemType.kt new file mode 100644 index 00000000..8fd98cfa --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemType.kt @@ -0,0 +1,31 @@ +package ru.dbotthepony.kstarbound.defs.item + +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class ItemType(override val jsonName: String, val extension: String?) : IStringSerializable { + GENERIC ("generic", "item"), + LIQUID ("liquid", "liqitem"), + MATERIAL ("material", "matitem"), + OBJECT ("object", null), + CURRENCY ("currency", "currency"), + MINING_TOOL ("miningtool", "miningtool"), + FLASHLIGHT ("flashlight", "flashlight"), + WIRE_TOOL ("wiretool", "wiretool"), + BEAM_MINING_TOOL ("beamminingtool", "beamaxe"), + HARVEST_TOOL ("harvestingtool", "harvestingtool"), + TILLING_TOOL ("tillingtool", "tillingtool"), + PAINTING_BRUSH ("paintingbeamtool", "painttool"), + HEAD_ARMOR ("headarmor", "head"), + CHEST_ARMOR ("chestarmor", "chest"), + LEGS_ARMOR ("legsarmor", "legs"), + BACK_ARMOR ("backarmor", "back"), + CONSUMABLE ("consumable", "consumable"), + BLUEPRINT ("blueprint", "blueprint"), + CODEX ("codex", null), + INSPECTION_TOOL ("inspectiontool", "inspectiontool"), + PLAYING_INSTRUMENT ("instrument", "instrument"), + THROWABLE ("thrownitem", "thrownitem"), + UNLOCK ("unlockitem", "unlock"), + ACTIVE ("activeitem", "activeitem"), + AUGMENT ("augmentitem", "augment"); +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt index c444eca3..7eff1598 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt @@ -8,19 +8,30 @@ import com.google.gson.JsonPrimitive import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory +import com.google.gson.annotations.JsonAdapter 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 it.unimi.dsi.fastutil.objects.Object2IntArrayMap +import it.unimi.dsi.fastutil.objects.Object2IntMap import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.json.jsonArrayOf import ru.dbotthepony.kstarbound.json.stream -import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.util.WriteOnce -import java.util.Random +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random import java.util.random.RandomGenerator +typealias ItemOrPool = Either> + +@JsonAdapter(TreasurePoolDefinition.Adapter::class) class TreasurePoolDefinition(pieces: List) { var name: String by WriteOnce() @@ -28,72 +39,83 @@ class TreasurePoolDefinition(pieces: List) { require(pieces.isNotEmpty()) { "Treasure pool is empty" } } - val pieces: ImmutableList = pieces.stream().sorted { o1, o2 -> o1.level.compareTo(o2.level) }.collect(ImmutableList.toImmutableList()) + val pieces: ImmutableList = pieces + .stream() + .sorted { o1, o2 -> o1.startingLevel.compareTo(o2.startingLevel) } + .collect(ImmutableList.toImmutableList()) - fun evaluate(random: RandomGenerator, level: Double): List { + fun evaluate(random: RandomGenerator, level: Double, visitedPools: Object2IntMap = Object2IntArrayMap()): List { require(level >= 0.0) { "Invalid loot level: $level" } - for (piece in pieces) { - if (level <= piece.level) { - return piece.evaluate(random, level) - } - } + val new = visitedPools.getInt(name) + 1 - if (pieces.last().level <= level) { - return pieces.last().evaluate(random, level) - } else { - return emptyList() + if (new >= 50) + throw IllegalStateException("Too deep treasure pool references, visited treasure pool $name 50 times (visited: $visitedPools)") + + visitedPools.put(name, new) + + try { + for (piece in pieces) { + if (level <= piece.startingLevel) { + return piece.evaluate(random, level, visitedPools) + } + } + + if (pieces.last().startingLevel <= level) { + return pieces.last().evaluate(random, level, visitedPools) + } else { + return emptyList() + } + } finally { + visitedPools.put(name, visitedPools.getInt(name) - 1) } } - fun evaluate(seed: Long, level: Double): List { - return evaluate(Random(seed), level) + fun evaluate(seed: Long, level: Double): List { + return evaluate(random(seed), level) } data class Piece( - val level: Double, - val pool: ImmutableList = ImmutableList.of(), - val fill: ImmutableList>> = ImmutableList.of(), + val startingLevel: Double, + val pool: WeightedList = WeightedList(), + val fill: ImmutableList = ImmutableList.of(), val poolRounds: IPoolRounds = OneRound, - // TODO: что оно делает? - // оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool - // а так же не "дублирует" содержимое если уровень генерации выше, чем указанный level - val allowDuplication: Boolean = false + val allowDuplication: Boolean = true, + val levelVariance: Vector2d = Vector2d.ZERO ) { - val maxWeight = pool.stream().mapToDouble { it.weight }.sum() + fun evaluate(random: RandomGenerator, level: Double, visitedPools: Object2IntMap): List { + val result = ArrayList() + val previousDescriptors = HashSet() - fun evaluate(random: RandomGenerator, actualLevel: Double): List { - val rounds = poolRounds.evaluate(random) - if (rounds <= 0) return emptyList() - val result = ArrayList() + for (entry in fill) { + entry.map({ + val stack = it.build(level = level + random.nextRange(levelVariance), random = random, seed = random.nextLong()) - for (round in 0 until rounds) { - for (entry in fill) { - entry.map({ - val stack = it.makeStack() - if (stack.isNotEmpty) result.add(stack) - }, { - it.value?.evaluate(random, actualLevel) - }) - } + if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) { + result.add(stack) + } + }, { + it.value?.evaluate(random, level + random.nextRange(levelVariance), visitedPools)?.forEach { + if (allowDuplication || previousDescriptors.add(it.copy(count = 1L))) + result.add(it) + } + }) + } - if (pool.isNotEmpty()) { - var chosen = random.nextDouble(maxWeight) + if (pool.isNotEmpty) { + for (round in 0 until poolRounds.evaluate(random)) { + pool.sample(random).orThrow { RuntimeException() }.map({ + val stack = it.build(level = level + random.nextRange(levelVariance), random = random, seed = random.nextLong()) - for (entry in pool) { - if (chosen <= entry.weight) { - entry.treasure.map({ - val stack = it.makeStack() - if (stack.isNotEmpty) result.add(stack) - }, { - it.value?.evaluate(random, actualLevel) - }) - - break - } else { - chosen -= entry.weight + if (stack.isNotEmpty && (allowDuplication || previousDescriptors.add(stack.copy(count = 1L)))) { + result.add(stack) } - } + }, { + it.value?.evaluate(random, level + random.nextRange(levelVariance), visitedPools)?.forEach { + if (allowDuplication || previousDescriptors.add(it.copy(count = 1L))) + result.add(it) + } + }) } } @@ -121,139 +143,103 @@ class TreasurePoolDefinition(pieces: List) { } } - data class PoolRounds(val edges: ImmutableList) : IPoolRounds { - val maxWeight = edges.stream().mapToDouble { it.weight }.sum() - + data class WeightedRounds(val edges: WeightedList) : IPoolRounds { override fun evaluate(random: RandomGenerator): Int { - val result = random.nextDouble(maxWeight) - var lower = 0.0 + return edges.sample(random).orElse(1) + } + } - for (edge in edges) { - if (result in lower .. lower + edge.weight) { - return edge.rounds - } else { - lower += edge.weight + class Adapter(gson: Gson) : TypeAdapter() { + private val itemAdapter = gson.getAdapter(ItemDescriptor::class.java) + private val poolAdapter = gson.getAdapter(object : TypeToken>() {}) + private val vectors = gson.getAdapter(Vector2d::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 = Starbound.ELEMENTS_ADAPTER.objects.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 = WeightedRounds(WeightedList(Starbound.gson.getAdapter(object : TypeToken>>() {}).fromJsonTree(it))) + } else { + throw JsonSyntaxException("Expected either a number or an array for poolRounds, but got ${it::class.simpleName}") + } } - } - return edges.last().rounds - } - } + things["pool"]?.let { + it as? JsonArray ?: throw JsonSyntaxException("pool must be an array") - 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" } - } - } + 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") - 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(ItemDescriptor::class.java) - private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::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() + if (elem.has("item")) { + pool.add(weight to Either.left(itemAdapter.read(JsonTreeReader(elem["item"]!!)))) + } else if (elem.has("pool")) { + pool.add(weight to Either.right(poolAdapter.read(JsonTreeReader(elem["pool"]!!)))) } else { - TODO() + throw JsonSyntaxException("Pool element at $i is missing both 'item' and 'pool' entries") } } + } - override fun read(`in`: JsonReader): TreasurePoolDefinition? { - if (`in`.consumeNull()) { - return null + 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") } - - 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 + } + + pieces.add(Piece( + pool = WeightedList(pool.build()), + fill = fill.build(), + poolRounds = poolRounds, + startingLevel = level, + allowDuplication = allowDuplication, + levelVariance = vectors.fromJsonTree(things.get("levelVariance", jsonArrayOf(0.0, 0.0))) + )) + + `in`.endArray() } - return null + `in`.endArray() + + return TreasurePoolDefinition(pieces) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IArmorItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IArmorItemDefinition.kt deleted file mode 100644 index 6ea73ef5..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IArmorItemDefinition.kt +++ /dev/null @@ -1,71 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import com.google.gson.Gson -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.kommons.gson.consumeNull -import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation - -interface IArmorItemDefinition : ILeveledItemDefinition, IScriptableItemDefinition { - /** - * Варианты покраски (???) - */ - val colorOptions: List> - - /** - * Визуальные кадры анимации, когда надето на гуманоида мужского пола - */ - val maleFrames: IFrames - - /** - * Визуальные кадры анимации, когда надето на гуманоида женского пола - */ - val femaleFrames: IFrames - - @JsonImplementation(Frames::class) - interface IFrames { - val body: SpriteReference - val backSleeve: SpriteReference? - val frontSleeve: SpriteReference? - } - - data class Frames( - override val body: SpriteReference, - override val backSleeve: SpriteReference? = null, - override val frontSleeve: SpriteReference? = null, - ) : IFrames { - object Factory : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (type.rawType == Frames::class.java) { - return object : TypeAdapter() { - private val adapter = FactoryAdapter.createFor(Frames::class, gson) - - private val frames = gson.getAdapter(SpriteReference::class.java) - - override fun write(out: JsonWriter, value: Frames?) { - adapter.write(out, value) - } - - override fun read(`in`: JsonReader): Frames? { - if (`in`.consumeNull()) - return null - else if (`in`.peek() == JsonToken.STRING) - return Frames(frames.read(`in`), null, null) - else - return adapter.read(`in`) - } - } as TypeAdapter - } - - return null - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ICurrencyItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ICurrencyItemDefinition.kt deleted file mode 100644 index 633751e5..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ICurrencyItemDefinition.kt +++ /dev/null @@ -1,16 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -interface ICurrencyItemDefinition : IItemDefinition { - /** - * ID валюты - */ - val currency: String - - /** - * Ценность одного предмета в [currency] - */ - val value: Long - - override val itemType: String - get() = "currency" -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IFlashlightDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IFlashlightDefinition.kt deleted file mode 100644 index 8b837872..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IFlashlightDefinition.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d - -interface IFlashlightDefinition : IItemDefinition, IItemInHandDefinition { - /** - * Смещение в пикселях - */ - val lightPosition: Vector2d - - val lightColor: RGBAColor - - val beamLevel: Int - - val beamAmbience: Double -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IHarvestingToolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IHarvestingToolDefinition.kt deleted file mode 100644 index a2ba6e3f..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IHarvestingToolDefinition.kt +++ /dev/null @@ -1,30 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kstarbound.defs.animation.IAnimated - -interface IHarvestingToolDefinition : IItemDefinition, IAnimated, IItemInHandDefinition { - /** - * Радиус в тайлах, на какое расстояние действует данный инструмент для сбора - */ - val blockRadius: Int - - /** - * Радиус в тайлах, на какое расстояние действует данный инструмент для сбора - */ - val altBlockRadius: Int - - /** - * Звуки в бездействии - */ - val idleSound: List - - /** - * Звуки при работе - */ - val strikeSounds: List - - /** - * Время атаки - */ - val fireTime: Double -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt deleted file mode 100644 index f13161f5..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemDefinition.kt +++ /dev/null @@ -1,130 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.AssetPath -import ru.dbotthepony.kstarbound.defs.IThingWithDescription -import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon -import ru.dbotthepony.kstarbound.defs.item.ItemRarity -import ru.dbotthepony.kstarbound.defs.item.impl.ItemDefinition -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation - -@JsonImplementation(ItemDefinition::class) -interface IItemDefinition : IThingWithDescription { - /** - * Внутреннее имя предмета (ID). - * Не путать с именем предмета! - * - * @see shortdescription - * @see description - */ - val itemName: String - - /** - * Цена в пикселях - */ - val price: Long - - /** - * Редкость предмета - */ - val rarity: ItemRarity - - /** - * Категория предмета, определяет, в какую вкладку инвентаря оно попадает - */ - val category: String? - - /** - * Иконка в инвентаре, относительный и абсолютный пути - */ - val inventoryIcon: Either>? - - /** - * Теги предмета - */ - val itemTags: List - - /** - * При подборе предмета мгновенно заставляет игрока изучить эти рецепты крафта - */ - val learnBlueprintsOnPickup: List> - - /** - * Максимальное количество предмета в стопке - */ - val maxStack: Long - - /** - * snip - */ - val eventCategory: String? - - /** - * Топливо корабля - */ - val fuelAmount: Long - - /** - * Заставляет предмет "использовать" сразу же при подборе - */ - val consumeOnPickup: Boolean - - /** - * Запускает следующие квест(ы) при подборе - */ - val pickupQuestTemplates: List - - /** - * тип tooltip'а - * - * ссылается на конфиг окон - */ - val tooltipKind: String - - /** - * Занимает ли предмет обе руки - */ - val twoHanded: Boolean - - /** - * Заставляет SAIL/прочих болтать при подборе предмета в первый раз - */ - val radioMessagesOnPickup: List - - /** - * Звуки при поднятии "малого" количества предметов. Не имеет никакого смысла без [smallStackLimit] - */ - val pickupSoundsSmall: List? - - /** - * Звуки при поднятии "среднего" количества предметов. Не имеет никакого смысла без [mediumStackLimit] - */ - val pickupSoundsMedium: List? - - /** - * Звуки при поднятии "большого" количества предметов. Не имеет никакого смысла без [smallStackLimit] и без [mediumStackLimit] - */ - val pickupSoundsLarge: List? - - /** - * Количество предметов ниже или равному данному значению проиграет звук [pickupSoundsSmall] - */ - val smallStackLimit: Long? - - /** - * Количество предметов ниже или равному данному значению (но не меньше [smallStackLimit]) проиграет звук [pickupSoundsMedium] - */ - val mediumStackLimit: Long? - - /** - * Lua script path for item config builder - */ - val builder: AssetPath? - - /** - * Тип предмета, используется Lua скриптами - */ - val itemType: String - get() = "item" -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemInHandDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemInHandDefinition.kt deleted file mode 100644 index d2ea4d37..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IItemInHandDefinition.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kommons.vector.Vector2d - -interface IItemInHandDefinition : IItemDefinition { - /** - * Позиция инструмента в руке (смещение в пикселях) - */ - val handPosition: Vector2d -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILeveledItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILeveledItemDefinition.kt deleted file mode 100644 index 8b9f69e8..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILeveledItemDefinition.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kstarbound.defs.item.ILeveledStatusEffect - -interface ILeveledItemDefinition : IItemDefinition { - /** - * Изначальный уровень предмета, может быть изменён позднее чем угодно - */ - val level: Double - - /** - * Эффекты предмета, растущие с уровнем - */ - val leveledStatusEffects: List -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILiquidItem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILiquidItem.kt deleted file mode 100644 index 9948b4ea..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/ILiquidItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kommons.util.Either - -interface ILiquidItem : IItemDefinition { - /** - * То, какую жидкость из себя представляет данный предмет - */ - val liquid: Either - - override val itemType: String - get() = "liquid" -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IMaterialItem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IMaterialItem.kt deleted file mode 100644 index e06a9688..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IMaterialItem.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kommons.util.Either - -interface IMaterialItem : IItemDefinition { - /** - * То, какой материал (блок) из себя представляет данный предмет - */ - val material: Either - - override val itemType: String - get() = "material" -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IScriptableItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IScriptableItemDefinition.kt deleted file mode 100644 index cde5c4c8..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IScriptableItemDefinition.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.api - -import ru.dbotthepony.kstarbound.defs.IScriptable -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition - -interface IScriptableItemDefinition : IItemDefinition, IScriptable diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ArmorItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ArmorItemDefinition.kt deleted file mode 100644 index 1f2eee10..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ArmorItemDefinition.kt +++ /dev/null @@ -1,49 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kstarbound.defs.IScriptable -import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition -import ru.dbotthepony.kstarbound.defs.item.LeveledStatusEffect -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory(logMisses = false) -data class ArmorItemDefinition( - @JsonFlat - val parent: IItemDefinition, - @JsonFlat - val script: IScriptable.Impl, - - override val maxStack: Long = 1L, - - override val colorOptions: ImmutableList> = ImmutableList.of(), - override val maleFrames: IArmorItemDefinition.Frames, - override val femaleFrames: IArmorItemDefinition.Frames, - override val level: Double = 1.0, - override val leveledStatusEffects: ImmutableList = ImmutableList.of(), -) : IArmorItemDefinition, IItemDefinition by parent, IScriptable by script - -@JsonFactory(logMisses = false) -class HeadArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent { - override val itemType: String - get() = "headarmor" -} - -@JsonFactory(logMisses = false) -class ChestArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent { - override val itemType: String - get() = "chestarmor" -} - -@JsonFactory(logMisses = false) -class LegsArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent { - override val itemType: String - get() = "legsarmor" -} - -@JsonFactory(logMisses = false) -class BackArmorItemDefinition(@JsonFlat val parent: ArmorItemDefinition) : IArmorItemDefinition by parent { - override val itemType: String - get() = "backarmor" -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/CurrencyItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/CurrencyItemDefinition.kt deleted file mode 100644 index fe5a4e6e..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/CurrencyItemDefinition.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import ru.dbotthepony.kstarbound.defs.item.api.ICurrencyItemDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.builder.JsonBuilder -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory -data class CurrencyItemDefinition( - @JsonFlat - val parent: IItemDefinition, - override val maxStack: Long = 16777216L, - override var currency: String, - override var value: Long, -) : ICurrencyItemDefinition, IItemDefinition by parent { - override val itemType: String - get() = super.itemType -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/FlashlightDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/FlashlightDefinition.kt deleted file mode 100644 index f0e4adef..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/FlashlightDefinition.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kstarbound.defs.item.api.IFlashlightDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory(logMisses = false) -class FlashlightDefinition( - @JsonFlat - val parent: IItemDefinition, - - override val lightPosition: Vector2d, - override val lightColor: RGBAColor, - override val beamLevel: Int, - override val beamAmbience: Double, - override val handPosition: Vector2d, - override val maxStack: Long = 1L, -) : IItemDefinition by parent, IFlashlightDefinition diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/HarvestingToolPrototype.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/HarvestingToolPrototype.kt deleted file mode 100644 index 91fe2f1f..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/HarvestingToolPrototype.kt +++ /dev/null @@ -1,23 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kstarbound.defs.item.api.IHarvestingToolDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory(logMisses = false) -class HarvestingToolPrototype( - @JsonFlat - val parent: IItemDefinition, - override val frames: Int, - override val animationCycle: Double, - override val blockRadius: Int, - override val altBlockRadius: Int = 0, - override val idleSound: ImmutableList = ImmutableList.of(), - override val strikeSounds: ImmutableList = ImmutableList.of(), - override val handPosition: Vector2d, - override val fireTime: Double, - override val maxStack: Long = 1L, -) : IItemDefinition by parent, IHarvestingToolDefinition diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt deleted file mode 100644 index 937a160d..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/ItemDefinition.kt +++ /dev/null @@ -1,42 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.AssetPath -import ru.dbotthepony.kstarbound.defs.IThingWithDescription -import ru.dbotthepony.kstarbound.defs.ThingDescription -import ru.dbotthepony.kstarbound.defs.item.IInventoryIcon -import ru.dbotthepony.kstarbound.defs.item.InventoryIcon -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.defs.item.ItemRarity -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory(logMisses = false) -data class ItemDefinition( - @JsonFlat - val descriptionData: ThingDescription, - - override val itemName: String, - override val price: Long = 0, - override val rarity: ItemRarity = ItemRarity.COMMON, - override val category: String? = null, - override val inventoryIcon: Either>? = null, - override val itemTags: ImmutableList = ImmutableList.of(), - override val learnBlueprintsOnPickup: ImmutableList> = ImmutableList.of(), - override val maxStack: Long = 9999L, - override val eventCategory: String? = null, - override val consumeOnPickup: Boolean = false, - override val pickupQuestTemplates: ImmutableList = ImmutableList.of(), - override val tooltipKind: String = "normal", - override val twoHanded: Boolean = false, - override val radioMessagesOnPickup: ImmutableList = ImmutableList.of(), - override val fuelAmount: Long = 0, - override val pickupSoundsSmall: ImmutableList = ImmutableList.of(), - override val pickupSoundsMedium: ImmutableList = ImmutableList.of(), - override val pickupSoundsLarge: ImmutableList = ImmutableList.of(), - override val smallStackLimit: Long? = null, - override val mediumStackLimit: Long? = null, - override val builder: AssetPath? = null, -) : IItemDefinition, IThingWithDescription by descriptionData diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/LiquidItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/LiquidItemDefinition.kt deleted file mode 100644 index 418760cc..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/LiquidItemDefinition.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.defs.item.api.ILiquidItem -import ru.dbotthepony.kstarbound.json.builder.JsonAlias -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory -data class LiquidItemDefinition( - @JsonFlat - val parent: IItemDefinition, - @JsonAlias("liquidId", "liquidName") - override val liquid: Either, -) : IItemDefinition by parent, ILiquidItem { - override val itemType: String - get() = super.itemType -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/MaterialItemDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/MaterialItemDefinition.kt deleted file mode 100644 index 359901a0..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/impl/MaterialItemDefinition.kt +++ /dev/null @@ -1,19 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.item.impl - -import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.defs.item.api.IMaterialItem -import ru.dbotthepony.kstarbound.json.builder.JsonAlias -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.json.builder.JsonFlat - -@JsonFactory -data class MaterialItemDefinition( - @JsonFlat - val parent: IItemDefinition, - @JsonAlias("materialId", "materialName") - override val material: Either -) : IMaterialItem, IItemDefinition by parent { - override val itemType: String - get() = super.itemType -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index 2a4225b2..c6d21bca 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -30,6 +30,7 @@ import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor @@ -153,7 +154,6 @@ data class ObjectDefinition( val biomePlaced: Boolean = false, ) - private val objects = gson.getAdapter(JsonObject::class.java) private val objectRef = gson.getAdapter(JsonReference.Object::class.java) private val basic = gson.getAdapter(PlainData::class.java) private val damageConfig = gson.getAdapter(TileDamageConfig::class.java) @@ -174,7 +174,7 @@ data class ObjectDefinition( if (`in`.consumeNull()) return null - val read = objects.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val basic = basic.fromJsonTree(read) val printable = basic.hasObjectItem && read.get("printable", basic.scannable) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 86010ec6..914d42a7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -11,6 +11,7 @@ import com.google.gson.annotations.JsonAdapter import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.clear import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.AABBi @@ -30,6 +31,7 @@ import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile @@ -94,6 +96,8 @@ data class ObjectOrientation( } companion object { + private val LOGGER = LogManager.getLogger() + fun preprocess(json: JsonArray): JsonArray { val actual = ArrayList() @@ -136,7 +140,6 @@ data class ObjectOrientation( } class Adapter(gson: Gson) : TypeAdapter() { - private val objects = gson.getAdapter(JsonObject::class.java) private val vectors = gson.getAdapter(Vector2f::class.java) private val vectorsi = gson.getAdapter(Vector2i::class.java) private val vectorsd = gson.getAdapter(Vector2d::class.java) @@ -161,7 +164,7 @@ data class ObjectOrientation( if (`in`.consumeNull()) return null - val obj = objects.read(`in`) + val obj = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val drawables = ArrayList() val flipImages = obj.get("flipImages", false) val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object")) @@ -199,12 +202,16 @@ data class ObjectOrientation( for (drawable in drawables) { if (drawable is Drawable.Image) { val bound = drawable.path.with { "default" } - val sprite = bound.sprite ?: throw IllegalStateException("Not a valid sprite reference: ${bound.raw} (${bound.imagePath} / ${bound.spritePath})") + val sprite = bound.sprite - val new = ImmutableSet.Builder() - new.addAll(occupySpaces) - new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) - occupySpaces = new.build() + if (sprite != null) { + val new = ImmutableSet.Builder() + new.addAll(occupySpaces) + new.addAll(sprite.worldSpaces(imagePositionI, obj["spaceScan"].asDouble, flipImages)) + occupySpaces = new.build() + } else { + LOGGER.error("Unable to space scan image, not a valid sprite reference: $bound") + } } } } catch (err: Throwable) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt index f49a9a38..a10ec41c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestParameter.kt @@ -20,6 +20,7 @@ import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.actor.Gender import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.io.readInternedString @@ -264,7 +265,6 @@ class QuestParameter( class Adapter(gson: Gson) : TypeAdapter() { private val details = DETAIL_ADAPTER.create(gson, TypeToken.get(Detail::class.java))!! - private val objects = gson.getAdapter(JsonObject::class.java) override fun write(out: JsonWriter, value: QuestParameter?) { if (value == null) @@ -287,7 +287,7 @@ class QuestParameter( if (`in`.consumeNull()) return null - val read = objects.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val detail = if (read["type"]?.asString == "json") { JsonData(read) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestTemplate.kt index 0167a342..6fc1ff74 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/quest/QuestTemplate.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.quest import com.google.common.collect.ImmutableSet import com.google.gson.JsonObject +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.json.builder.JsonFactory @@ -10,8 +11,8 @@ data class QuestTemplate( val id: String, val prerequisites: ImmutableSet = ImmutableSet.of(), val requiredItems: ImmutableSet = ImmutableSet.of(), - val script: AssetPath, + val script: AssetPath?, val updateDelta: Int = 10, - val moneyRange: LongRange, + val moneyRange: Vector2i = Vector2i(0, 0), val scriptConfig: JsonObject = JsonObject() ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt index 89100a05..b6f93727 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderParameters.kt @@ -12,7 +12,7 @@ data class RenderParameters( val multiColored: Boolean = false, val occludesBelow: Boolean = false, val lightTransparent: Boolean = false, - val zLevel: Long, + val zLevel: Long = 0, ) { init { if (texture != null) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index 76e764be..704f9971 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -44,7 +44,7 @@ data class TileDefinition( val collisionKind: CollisionType = CollisionType.BLOCK, - override val renderTemplate: AssetReference, + override val renderTemplate: AssetReference = AssetReference(null), override val renderParameters: RenderParameters, val cascading: Boolean = false, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt index 5fd1bd32..2d30d667 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt @@ -77,7 +77,6 @@ data class BiomePlaceables( override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (Item::class.java.isAssignableFrom(type.rawType)) { return object : TypeAdapter() { - private val arrays = gson.getAdapter(JsonArray::class.java) private val grassVariant = gson.getAdapter(GrassVariant::class.java) private val bushVariant = gson.getAdapter(BushVariant::class.java) private val trees = gson.listAdapter() @@ -116,7 +115,7 @@ data class BiomePlaceables( // Truly our hero here. val obj = when (val type = `in`.nextString()) { "treasureBoxSet" -> TreasureBox(`in`.nextString()) - "microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet())) + "microDungeon" -> MicroDungeon(Starbound.ELEMENTS_ADAPTER.arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet())) "grass" -> Grass(grassVariant.read(`in`)) "bush" -> Bush(bushVariant.read(`in`)) "treePair" -> Tree(trees.read(`in`)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt index 80bafbce..aeb19d2c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt @@ -73,8 +73,6 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) { return object : TypeAdapter() { - private val objects = gson.getAdapter(JsonObject::class.java) - override fun write(out: JsonWriter, value: VisitableWorldParameters?) { if (value == null) out.nullValue() @@ -87,7 +85,7 @@ enum class VisitableWorldParametersType(override val jsonName: String, val token return null val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters - instance.fromJson(objects.read(`in`)) + instance.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`)) return instance } } as TypeAdapter diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index 3cf4eb30..035f2830 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -612,8 +612,6 @@ class WorldLayout { } companion object : TypeAdapter() { - private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } - override fun write(out: JsonWriter, value: WorldLayout?) { if (value == null) out.nullValue() @@ -626,7 +624,7 @@ class WorldLayout { return null val params = WorldLayout() - params.fromJson(objects.read(`in`)) + params.fromJson(Starbound.ELEMENTS_ADAPTER.objects.read(`in`)) return params } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt index 5c46fe44..701a5ecc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt @@ -2,12 +2,14 @@ package ru.dbotthepony.kstarbound.io import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap +import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarLong import ru.dbotthepony.kstarbound.IStarboundFile import ru.dbotthepony.kstarbound.getValue import ru.dbotthepony.kstarbound.json.readJsonObject +import ru.dbotthepony.kstarbound.util.sbIntern import java.io.BufferedInputStream import java.io.Closeable import java.io.DataInputStream @@ -64,12 +66,22 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) } } + override fun equals(other: Any?): Boolean { + return other is IStarboundFile && computeFullPath() == other.computeFullPath() + } + + private val hash = computeFullPath().hashCode() + + override fun hashCode(): Int { + return hash + } + override fun open(): InputStream { throw IllegalStateException("${computeFullPath()} is a directory") } override fun toString(): String { - return "SBDirectory[${computeFullPath()} @ $path]" + return "SBDirectory[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]" } } @@ -91,6 +103,16 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) override val children: Map? get() = null + override fun equals(other: Any?): Boolean { + return other is IStarboundFile && computeFullPath() == other.computeFullPath() + } + + private val hash = computeFullPath().hashCode() + + override fun hashCode(): Int { + return hash + } + override fun open(): InputStream { return object : InputStream() { private var innerOffset = 0L @@ -132,22 +154,20 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) if (readMax <= 0) return -1 - synchronized(reader) { - reader.seek(innerOffset + offset) - val readBytes = reader.read(b, off, readMax) + reader.seek(innerOffset + offset) + val readBytes = reader.read(b, off, readMax) - if (readBytes == -1) - throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path") + if (readBytes == -1) + throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path") - innerOffset += readBytes - return readBytes - } + innerOffset += readBytes + return readBytes } } } override fun toString(): String { - return "SBFile[${computeFullPath()} @ $path]" + return "SBFile[${computeFullPath()} @ ${metadata.get("friendlyName", "")} $path]" } } @@ -215,13 +235,13 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) try { callback(false, "Reading index node $i") - name = stream.readBinaryString() + name = stream.readInternedString() require(name[0] == '/') { "index node at $i with '$name' appears to be not an absolute filename" } val offset = stream.readLong() val length = stream.readLong() if (offset > flength) { - throw IndexOutOfBoundsException("Garbage offset at index $i: ${offset}") + throw IndexOutOfBoundsException("Garbage offset at index $i: $offset") } if (length + offset > flength) { @@ -233,10 +253,10 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) var parent = root as SBDirectory for (splitIndex in 1 until split.size - 1) { - parent = parent.subdir(split[splitIndex]) + parent = parent.subdir(split[splitIndex].sbIntern()) } - parent.put(SBFile(split.last(), parent, offset, length)) + parent.put(SBFile(split.last().sbIntern(), parent, offset, length)) } catch (err: Throwable) { if (name == null) { throw IOException("Reading index node at $i", err) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt new file mode 100644 index 00000000..fcd239fd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt @@ -0,0 +1,159 @@ +package ru.dbotthepony.kstarbound.item + +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.IStarboundFile +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.item.ItemType +import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.JsonPatch +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Future + +object ItemRegistry { + class Entry( + val name: String, + val type: ItemType, + val json: JsonObject, + val isEmpty: Boolean, + val friendlyName: String, + val itemTags: ImmutableSet, + val itemAgingScripts: ImmutableSet, + val file: IStarboundFile? + ) { + val isNotEmpty: Boolean + get() = !isEmpty + + val directory = file?.computeDirectory() ?: "/" + } + + private val LOGGER = LogManager.getLogger() + private val entries = HashMap() + private val tasks = ConcurrentLinkedQueue() + + val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null) + + init { + entries[""] = AIR + } + + operator fun get(name: String): Entry { + return entries[name] ?: AIR + } + + @JsonFactory + data class ReadData( + val itemName: String, + val shortdescription: String = "...", + val itemTags: ImmutableSet = ImmutableSet.of(), + val itemAgingScripts: ImmutableSet = ImmutableSet.of(), + ) + + @JsonFactory + data class ReadDataObj( + val shortdescription: String = "...", + val itemTags: ImmutableSet = ImmutableSet.of(), + val itemAgingScripts: ImmutableSet = ImmutableSet.of(), + ) + + private fun addObjectItem(obj: Registry.Entry) { + if (obj.key in entries) { + LOGGER.error("Refusing to overwrite item ${obj.key} with generated item for object (old def originate from ${entries[obj.key]!!.file}; new from ${obj.file})") + return + } + + val config = obj.jsonObject.deepCopy() + + if ("inventoryIcon" !in config) { + config["inventoryIcon"] = Globals.itemParameters.missingIcon.fullPath + LOGGER.warn("inventoryIcon is missing for object item ${obj.key}, using default ${Globals.itemParameters.missingIcon.fullPath}") + } + + config["itemName"] = obj.key + + if ("tooltipKind" !in config) + config["tooltipKind"] = "object" + + // this is required for Lua call to "get item config" + config["printable"] = obj.value.printable + + // Don't inherit object scripts. this is kind of a crappy solution to prevent + // ObjectItems (which are firable and therefore scripted) from trying to + // execute scripts intended for objects + config.remove("scripts") + + val readData = Starbound.gson.fromJson(config, ReadDataObj::class.java) + + entries[obj.key] = Entry( + name = obj.key, + type = ItemType.OBJECT, + json = config, + isEmpty = false, + friendlyName = readData.shortdescription, + itemTags = readData.itemTags, + itemAgingScripts = readData.itemAgingScripts, + file = obj.file, + ) + } + + fun finishLoad() { + tasks.forEach { it.run() } + tasks.clear() + + for (obj in Registries.worldObjects.keys.values) { + if (obj.value.hasObjectItem) { + addObjectItem(obj) + } + } + } + + fun load(fileTree: Map>, patches: Map>): Collection> { + val futures = ArrayList>() + val data by lazy { Starbound.gson.getAdapter(ReadData::class.java) } + + for (type in ItemType.entries) { + val files = fileTree[type.extension ?: continue] ?: continue + + for (file in files) { + futures.add(Starbound.EXECUTOR.submit { + try { + val read = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(file.jsonReader()), patches[file.computeFullPath()]) as JsonObject + val readData = data.fromJsonTree(read) + + tasks.add { + if (readData.itemName in entries) { + LOGGER.warn("Overwriting item definition at ${readData.itemName} (old def originate from ${entries[readData.itemName]!!.file}; new from $file)") + } + + entries[readData.itemName] = Entry( + name = readData.itemName, + type = type, + json = read, + isEmpty = false, + friendlyName = readData.shortdescription, + itemTags = readData.itemTags, + itemAgingScripts = readData.itemAgingScripts, + file = file, + ) + } + } catch (err: Throwable) { + LOGGER.error("Reading item definition $file", err) + } + }) + } + } + + return futures + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index 747dab9d..9533efff 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -1,43 +1,58 @@ package ru.dbotthepony.kstarbound.item +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.TypeAdapter +import com.google.gson.annotations.JsonAdapter import com.google.gson.internal.bind.TypeAdapters +import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter +import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.classdump.luna.Table import org.classdump.luna.TableFactory -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeVarLong -import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2f +import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.Drawable +import ru.dbotthepony.kstarbound.defs.image.SpriteReference +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.defs.item.ItemRarity +import ru.dbotthepony.kstarbound.json.mergeJson +import ru.dbotthepony.kstarbound.json.stream import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.from +import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.ManualLazy +import ru.dbotthepony.kstarbound.util.valueOf import java.io.DataOutputStream +import java.lang.Math.toRadians import java.util.concurrent.atomic.AtomicLong +import kotlin.math.max +import kotlin.properties.Delegates /** * Base class for instanced items in game */ -open class ItemStack { - constructor() { - this.config = Registries.items.emptyRef - this.parameters = JsonObject() - } - - constructor(descriptor: ItemDescriptor) { - this.config = descriptor.ref - this.size = descriptor.count - this.parameters = descriptor.parameters.deepCopy() - } - +@JsonAdapter(ItemStack.Adapter::class) +open class ItemStack(descriptor: ItemDescriptor) { /** * unique number utilized to determine whenever stack has changed */ @@ -52,7 +67,7 @@ open class ItemStack { changeset = CHANGESET.incrementAndGet() } - var size: Long = 0L + var size: Long = descriptor.count set(value) { val newValue = value.coerceAtLeast(0L) @@ -62,18 +77,21 @@ open class ItemStack { } } - val config: Registry.Ref - var parameters: JsonObject + val config: ItemRegistry.Entry = descriptor.ref + var parameters: JsonObject = descriptor.parameters.deepCopy() protected set + protected val parametersLazies = ObjectArrayList>() // no CME checks + + protected val mergedJson = ManualLazy { + mergeJson(config.json.deepCopy(), parameters) + }.also { parametersLazies.add(it) } + val isEmpty: Boolean get() = size <= 0 || config.isEmpty val isNotEmpty: Boolean - get() = size > 0 && config.isPresent - - val maxStackSize: Long - get() = config.value?.maxStack ?: 0L + get() = size > 0 && !config.isEmpty fun grow(amount: Long) { size += amount @@ -83,12 +101,135 @@ open class ItemStack { size -= amount } + protected fun lookupProperty(name: String): JsonElement { + return mergedJson.value[name] ?: JsonNull.INSTANCE + } + + protected fun lookupProperty(name: String, orElse: JsonElement): JsonElement { + return mergedJson.value[name] ?: orElse + } + + protected fun lookupProperty(name: String, orElse: () -> JsonElement): JsonElement { + return mergedJson.value[name] ?: orElse() + } + + protected fun lazyProperty(name: String, default: JsonElement, transform: JsonElement.() -> T): ManualLazy { + return ManualLazy { + transform(lookupProperty(name, default)) + }.also { parametersLazies.add(it) } + } + + protected fun lazyProperty(name: String, transform: JsonElement.() -> T): ManualLazy { + return ManualLazy { + transform(lookupProperty(name)) + }.also { parametersLazies.add(it) } + } + + protected fun lazyProperty(name: String, default: () -> JsonElement, transform: JsonElement.() -> T): ManualLazy { + return ManualLazy { + transform(lookupProperty(name, default)) + }.also { parametersLazies.add(it) } + } + + val maxStackSize: Long by lazyProperty("maxStack", JsonPrimitive(Globals.itemParameters.defaultMaxStack)) { asLong } + val shortDescription: String by lazyProperty("shortdescription", JsonPrimitive("")) { asString } + val description: String by lazyProperty("description", JsonPrimitive("")) { asString } + val rarity: ItemRarity by lazyProperty("rarity") { ItemRarity.entries.valueOf(asString) } + val twoHanded: Boolean by lazyProperty("twoHanded", JsonPrimitive(false)) { asBoolean } + val price: Long by lazyProperty("price", JsonPrimitive(Globals.itemParameters.defaultPrice)) { asLong } + val tooltipKind: String by lazyProperty("tooltipKind", JsonPrimitive("")) { asString } + val category: String by lazyProperty("category", JsonPrimitive("")) { asString } + val pickupSounds: Set by lazyProperty("pickupSounds", { JsonArray() }) { if (asJsonArray.isEmpty) Globals.itemParameters.pickupSoundsAbsolute else asJsonArray.stream().map { it.asString }.collect(ImmutableSet.toImmutableSet()) } + val timeToLive: Double by lazyProperty("timeToLive", JsonPrimitive(Globals.itemParameters.defaultTimeToLive)) { asDouble } + val learnBlueprintsOnPickup: Set by lazyProperty("learnBlueprintsOnPickup", { JsonArray() }) { asJsonArray.stream().map { ItemDescriptor(it) }.collect(ImmutableSet.toImmutableSet()) } + + // collection -> entry + val collectablesOnPickup: Map by lazyProperty("collectablesOnPickup", { JsonObject() }) { asJsonObject.entrySet().stream().map { it.key to it.value.asString }.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) } + + var drawables: ImmutableList by Delegates.notNull() + private set + + protected fun setIconDrawables(drawables: Collection) { + val drawablesList = ArrayList(drawables) + + if (drawablesList.isNotEmpty()) { + var boundingBox = drawablesList.first().boundingBox(true) + + for (i in 1 until drawablesList.size) { + boundingBox = boundingBox.combine(drawablesList[i].boundingBox(true)) + } + + for (drawable in drawablesList.indices) { + drawablesList[drawable] = drawablesList[drawable].translate(boundingBox.centre.toFloatVector()) + } + + // as original engine puts it: + // TODO: Why 16? Is this the size of the icon container? Shouldn't this be configurable? + + // in other news, + // TODO: scaling is always centered on 0.0, 0.0; this also should be configurable. + + val zoom = 16.0 / max(boundingBox.width, boundingBox.height) + + if (zoom < 1.0) { + for (drawable in drawablesList.indices) { + drawablesList[drawable] = drawablesList[drawable].scale(zoom.toFloat()) + } + } + } + + this.drawables = ImmutableList.copyOf(drawablesList) + } + + init { + val inventoryIcon = lookupProperty("inventoryIcon", JsonPrimitive(Globals.itemParameters.missingIcon.fullPath)) + + if (inventoryIcon.isJsonArray) { + val drawables = ArrayList() + + for (drawable in inventoryIcon.asJsonArray) { + drawable as JsonObject + + val image = SpriteReference.create(AssetPathStack.relativeTo(config.directory, drawable["image"].asString)) + val position: Vector2f + + if ("position" in drawable) { + position = Starbound.gson.fromJson(drawable["position"], Vector2f::class.java) + } else { + position = Vector2f.ZERO + } + + val scale = if ("scale" in drawable) { + scales.fromJsonTree(drawable["scale"]) + } else { + Either.left(1f) + } + + val color = if ("color" in drawable) { + colors.fromJsonTree(drawable["color"]) + } else { + RGBAColor.WHITE + } + + val rotation = toRadians(drawable.get("rotation", 0.0)).toFloat() + val transforms = Drawable.Transformations(drawable.get("centered", true), rotation, drawable.get("mirrored", false), scale) + + drawables.add(Drawable.Image(image, Either.right(transforms), position, color, drawable.get("fullbright", false))) + } + + setIconDrawables(drawables) + } else { + val image = SpriteReference.create(AssetPathStack.relativeTo(config.directory, inventoryIcon.asString)) + val transforms = Drawable.CENTERED + val drawable = Drawable.Image(image, Either.right(transforms)) + setIconDrawables(listOf(drawable)) + } + } + data class AgingResult(val new: ItemStack?, val ageUpdated: Boolean) - private fun config() = config - private val agingScripts: LuaEnvironment? by lazy { - val config = config().value ?: return@lazy null + //val config = config.value ?: return@lazy null //if (config.itemTags) null } @@ -119,7 +260,7 @@ open class ItemStack { if (isEmpty) return ItemDescriptor.EMPTY - return ItemDescriptor(config.key.left(), size, parameters.deepCopy()) + return ItemDescriptor(config.name, size, parameters.deepCopy()) } // faster than creating an item descriptor and writing it (because it avoids copying and allocation) @@ -129,7 +270,7 @@ open class ItemStack { stream.writeVarLong(0L) stream.writeJsonElement(JsonNull.INSTANCE) } else { - stream.writeBinaryString(config.key.left()) + stream.writeBinaryString(config.name) stream.writeVarLong(size) stream.writeJsonElement(parameters) } @@ -182,14 +323,14 @@ open class ItemStack { if (isEmpty) return "ItemStack.EMPTY" - return "ItemDescriptor[${config.value?.itemName}, count = $size, params = $parameters]" + return "ItemStack[${config.name}, count = $size, params = $parameters]" } - fun copy(size: Long = this.size): ItemStack { + open fun copy(size: Long = this.size): ItemStack { if (isEmpty) return this - return ItemStack(ItemDescriptor(config, size, parameters.deepCopy())) + return ItemStack(ItemDescriptor(config.name, size, parameters.deepCopy())) } fun toJson(): JsonObject? { @@ -197,7 +338,7 @@ open class ItemStack { return null return JsonObject().also { - it.add("name", JsonPrimitive(config.key.left())) + it.add("name", JsonPrimitive(config.name)) it.add("count", JsonPrimitive(size)) it.add("parameters", parameters.deepCopy()) } @@ -209,27 +350,27 @@ open class ItemStack { } return allocator.newTable(0, 3).also { - it.rawset("name", config.key.left()) + it.rawset("name", config.name) it.rawset("count", size) it.rawset("parameters", allocator.from(parameters)) } } - class Adapter(val starbound: Starbound) : TypeAdapter() { + class Adapter(gson: Gson) : TypeAdapter() { override fun write(out: JsonWriter, value: ItemStack?) { val json = value?.toJson() if (json == null) out.nullValue() else - TypeAdapters.JSON_ELEMENT.write(out, json) + out.value(json) } override fun read(`in`: JsonReader): ItemStack { if (`in`.consumeNull()) return EMPTY - return starbound.item(TypeAdapters.JSON_ELEMENT.read(`in`)) + return create(ItemDescriptor(Starbound.ELEMENTS_ADAPTER.read(`in`))) } } @@ -237,7 +378,19 @@ open class ItemStack { private val CHANGESET = AtomicLong() @JvmField - val EMPTY = ItemStack() + val EMPTY = ItemStack(ItemDescriptor.EMPTY) + + private val vectors by lazy { + Starbound.gson.getAdapter(object : TypeToken() {}) + } + + private val scales by lazy { + Starbound.gson.getAdapter(object : TypeToken>() {}) + } + + private val colors by lazy { + Starbound.gson.getAdapter(object : TypeToken() {}) + } fun create(descriptor: ItemDescriptor): ItemStack { if (descriptor.isEmpty) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt similarity index 80% rename from src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt index e69a920b..ff2799e5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/RecipeRegistry.kt @@ -1,13 +1,15 @@ -package ru.dbotthepony.kstarbound +package ru.dbotthepony.kstarbound.item import com.google.gson.JsonElement import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kstarbound.IStarboundFile +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.actor.player.RecipeDefinition +import ru.dbotthepony.kstarbound.json.JsonPatch import java.util.* import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.ExecutorService import java.util.concurrent.Future import kotlin.collections.ArrayList @@ -29,7 +31,7 @@ object RecipeRegistry { val output2recipes: Map> = Collections.unmodifiableMap(output2recipesBacking) val input2recipes: Map> = Collections.unmodifiableMap(input2recipesBacking) - private val backlog = ConcurrentLinkedQueue() + private val tasks = ConcurrentLinkedQueue() private fun add(recipe: Entry) { val value = recipe.value @@ -59,26 +61,22 @@ object RecipeRegistry { } fun finishLoad() { - var next = backlog.poll() - - while (next != null) { - add(next) - next = backlog.poll() - } + tasks.forEach { add(it) } + tasks.clear() } - fun load(fileTree: Map>): List> { + fun load(fileTree: Map>, patchTree: Map>): List> { val files = fileTree["recipe"] ?: return emptyList() - val elements = Starbound.gson.getAdapter(JsonElement::class.java) - val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java) + val recipes by lazy { Starbound.gson.getAdapter(RecipeDefinition::class.java) } return files.map { listedFile -> Starbound.EXECUTOR.submit { try { - val json = elements.read(listedFile.jsonReader()) + val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patchTree[listedFile.computeFullPath()]) + val value = recipes.fromJsonTree(json) - backlog.add(Entry(value, json, listedFile)) + tasks.add(Entry(value, json, listedFile)) } catch (err: Throwable) { LOGGER.error("Loading recipe definition file $listedFile", err) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt index 8afec70f..d75cbe11 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt @@ -19,12 +19,17 @@ class InternedJsonElementAdapter(val stringInterner: Interner) : TypeAda return TypeAdapters.JSON_ELEMENT.write(out, value) } - override fun read(`in`: JsonReader): JsonElement? { + override fun read(`in`: JsonReader): JsonElement { return when (val p = `in`.peek()) { JsonToken.STRING -> JsonPrimitive(stringInterner.intern(`in`.nextString())) - JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(`in`.nextString())) + JsonToken.NUMBER -> JsonPrimitive(LazilyParsedNumber(stringInterner.intern(`in`.nextString()))) JsonToken.BOOLEAN -> if (`in`.nextBoolean()) TRUE else FALSE - JsonToken.NULL -> JsonNull.INSTANCE + + JsonToken.NULL -> { + `in`.nextNull() + JsonNull.INSTANCE + } + JsonToken.BEGIN_ARRAY -> arrays.read(`in`) JsonToken.BEGIN_OBJECT -> objects.read(`in`) else -> throw IllegalArgumentException(p.toString()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt new file mode 100644 index 00000000..b79f99e3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPatch.kt @@ -0,0 +1,133 @@ +package ru.dbotthepony.kstarbound.json + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kstarbound.IStarboundFile +import ru.dbotthepony.kstarbound.Starbound + +enum class JsonPatch(val key: String) { + TEST("test") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + val path = JsonPath.pointer(data.get("path").asString) + val value = data["value"] ?: JsonNull.INSTANCE + val inverse = data.get("inverse", false) + + val get = path.find(base) ?: JsonNull.INSTANCE + + if (inverse && get == value) { + throw IllegalStateException("Expected $path to not contain $value") + } else if (!inverse && get != value && value != JsonNull.INSTANCE) { + var text = get.toString() + + if (text.length > 40) { + text = text.substring(0, 40) + "..." + } + + throw IllegalStateException("Expected $path to contain $value, but found $text") + } + + return base + } + }, + REMOVE("remove") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + return JsonPath.pointer(data.get("path").asString).remove(base).first + } + }, + ADD("add") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation") + return JsonPath.pointer(data.get("path").asString).add(base, value) + } + }, + REPLACE("replace") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + val value = data["value"] ?: throw JsonSyntaxException("Missing 'value' to 'add' operation") + val path = JsonPath.pointer(data.get("path").asString) + return path.add(path.remove(base).first, value) + } + }, + MOVE("move") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + val from = JsonPath.pointer(data.get("from").asString) + val path = JsonPath.pointer(data.get("path").asString) + val (newBase, removed) = from.remove(base) + return path.add(newBase, removed) + } + }, + COPY("copy") { + override fun apply(base: JsonElement, data: JsonObject): JsonElement { + val from = JsonPath.pointer(data.get("from").asString) + val path = JsonPath.pointer(data.get("path").asString) + return path.add(base, from.get(base)) + } + }; + + abstract fun apply(base: JsonElement, data: JsonObject): JsonElement + + companion object { + private val LOGGER = LogManager.getLogger() + + fun apply(base: JsonElement, data: JsonObject): JsonElement { + val op = data["op"]?.asString ?: throw JsonSyntaxException("Missing 'op' in json patch operation") + val operation = entries.firstOrNull { it.key == op } ?: throw JsonSyntaxException("Unknown patch operation $op!") + return operation.apply(base, data) + } + + @Suppress("NAME_SHADOWING") + fun apply(base: JsonElement, data: JsonArray, source: IStarboundFile?): JsonElement { + // original engine decides what to do based on first element encountered + // in patch array... let's not. + var base = base + + for ((i, element) in data.withIndex()) { + if (element is JsonObject) { + try { + base = apply(base, element) + } catch (err: Throwable) { + LOGGER.error("Error while applying JSON patch from $source at index $i: ${err.message}") + } + } else if (element is JsonArray) { + for ((i2, sub) in element.withIndex()) { + try { + if (sub !is JsonObject) + throw JsonSyntaxException("Expected JSON Object as patch data, got $sub") + + base = apply(base, sub) + } catch (err: Throwable) { + LOGGER.error("Error while applying JSON patch from $source at index $i -> $i2: ${err.message}") + break + } + } + } else { + LOGGER.error("Unknown data in $source at index $i") + } + } + + return base + } + + @Suppress("NAME_SHADOWING") + fun apply(base: JsonElement, source: Collection?): JsonElement { + source ?: return base + var base = base + + for (patch in source) { + val read = Starbound.ELEMENTS_ADAPTER.read(patch.jsonReader()) + + if (read !is JsonArray) { + LOGGER.error("$patch root element is not an array") + } else { + base = apply(base, read, patch) + } + } + + return base + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt index 96ea3940..b1ad96ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonPath.kt @@ -7,11 +7,9 @@ import com.google.gson.JsonNull import com.google.gson.JsonObject import it.unimi.dsi.fastutil.objects.ObjectArrayList import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.util.KOptional -fun jsonQuery(path: String) = JsonPath.query(path) -fun jsonPointer(path: String) = JsonPath.pointer(path) - class JsonPath private constructor(val pieces: ImmutableList) { constructor(path: String) : this(ImmutableList.of(Piece(path))) @@ -66,62 +64,26 @@ class JsonPath private constructor(val pieces: ImmutableList) { return JsonPath(ImmutableList.copyOf(build)) } - private fun reconstructPath(at: Int): String { - if (at == 0) - return "" - else - return "/${pieces.joinToString("/", limit = at, truncated = "") { it.value }}" + fun reconstructPath(at: Int = pieces.size - 1): String { + if (at == pieces.size - 1) + return toString() + + return "/${pieces.joinToString("/", limit = at + 1, truncated = "") { it.value }}" } /** * Attempts to find given element along path, if can't, throws [TraversalException] */ fun get(element: JsonElement): JsonElement { - if (isEmpty) { - return element - } - - var current = element - - for ((i, piece) in pieces.withIndex()) { - if (current is JsonObject) { - if (piece.value !in current) { - throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element") - } - - current = current[piece.value]!! - - if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) { - throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current") - } - } else if (current is JsonArray) { - if (piece.value == "-") { - throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index") - } - - val key = piece.int - - if (key == null) { - throw TraversalException("Path at ${reconstructPath(i)} can not index an array") - } else if (key < 0) { - throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index") - } - - try { - current = current[key] - } catch (err: IndexOutOfBoundsException) { - throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index") - } - - if (i != pieces.size - 1 && current !is JsonObject && current !is JsonArray) { - throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current") - } - } else { - throw TraversalException("Path at ${reconstructPath(i)} can not index $current") + return traverse(element, { self, index -> + if (index == null || index !in 0 until self.size()) { + throw TraversalException("Path at ${reconstructPath()} points at non-existing index") } - } - return current + self[index] + }, { self, index -> + self[index] ?: throw TraversalException("Path at ${reconstructPath()} points at non-existing element") + }).orElse(element) } /** @@ -169,6 +131,121 @@ class JsonPath private constructor(val pieces: ImmutableList) { return find(element) ?: orElse } + fun traverse(element: JsonElement, arrayOperator: (JsonArray, Int?) -> T, objectOperator: (JsonObject, String) -> T): KOptional { + var current = element + + for ((i, piece) in pieces.withIndex()) { + if (current is JsonObject) { + if (i == pieces.size - 1) { + return KOptional(objectOperator(current, piece.value)) + } else { + if (piece.value !in current) { + throw TraversalException("Path at ${reconstructPath(i)} points at non-existing element") + } + + current = current[piece.value]!! + + if (current !is JsonObject && current !is JsonArray) { + throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying object (for ${pieces[i + 1]}), but there is $current") + } + } + } else if (current is JsonArray) { + if (piece.value == "-") { + if (i == pieces.size - 1) { + return KOptional(arrayOperator(current, null)) + } + + throw TraversalException("Path at ${reconstructPath(i)} points at non-existent index") + } + + var key = piece.int + + if (key == null) { + throw TraversalException("Path at ${reconstructPath(i)} can not index an array") + } else if (key < 0) { + key += current.size() + + if (key < 0) + throw TraversalException("Path at ${reconstructPath(i)} points at pseudo index, which is out of range (array size: ${current.size()})") + + if (i == pieces.size - 1) { + return KOptional(arrayOperator(current, key)) + } + } else if (i == pieces.size - 1) { + return KOptional(arrayOperator(current, key)) + } else if (key !in 0 until current.size()) { + throw TraversalException("Path at ${reconstructPath(i)} points at non-existing index") + } + + current = current[key] + + if (current !is JsonObject && current !is JsonArray) { + throw TraversalException("Path at ${reconstructPath(i)} expects to get index-able element from underlying array (for ${pieces[i + 1]}), but there is $current") + } + } else { + throw TraversalException("Path at ${reconstructPath(i)} can not index $current") + } + } + + return KOptional() + } + + fun add(base: JsonElement, value: JsonElement): JsonElement { + if (isEmpty) { + check(base.isJsonNull) { "Cannot add a value to the entire document, it is not empty." } + return value + } + + traverse(base, { self, index -> + if (index == null) + self.add(value) + else if (index !in 0 .. self.size()) + throw IndexOutOfBoundsException("Element at ${reconstructPath()} trying to insert new value into non-existing index (array size: ${self.size()})") + else if (index == self.size()) + self.add(value) + else { + // jsonarray does not support insertion + val values = ArrayList(self.size() - index) + + while (self.size() > index) { + values.add(self.remove(self.size() - 1)) + } + + self.add(value) + + while (values.isNotEmpty()) { + self.add(values.removeLast()) + } + } + }, { self, index -> + self[index] = value + }) + + return base + } + + fun remove(base: JsonElement): Pair { + if (isEmpty) { + return JsonNull.INSTANCE to base + } + + val removed = traverse(base, { self, index -> + if (index == null) + throw NoSuchElementException("Николай протоген") + else if (index !in 0 until self.size()) + throw NoSuchElementException("Element at ${reconstructPath()} does not exist (array size: ${self.size()})") + else + self.remove(index) + }, { self, index -> + if (index !in self) + throw NoSuchElementException("Element at ${reconstructPath()} does not exist") + + self.remove(index) + }).orThrow { RuntimeException() } + + return base to removed + } + companion object { val EMPTY = JsonPath(ImmutableList.of()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt index 520a9a61..3adfcd3f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt @@ -11,6 +11,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kstarbound.Starbound inline fun DispatchingAdapter( key: String, @@ -40,7 +41,6 @@ class DispatchingAdapter( private inner class Impl(gson: Gson) : TypeAdapter() { private val typeAdapter = gson.getAdapter(typeClass) private val adapters = types.associateWith { gson.getAdapter(type2value.invoke(it)) } as Map> - private val obj = gson.getAdapter(JsonObject::class.java) override fun write(out: JsonWriter, value: ELEMENT?) { if (value == null) @@ -75,7 +75,7 @@ class DispatchingAdapter( if (`in`.consumeNull()) return null - val read = obj.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val type = typeAdapter.fromJsonTree(read[key] ?: throw JsonSyntaxException("Missing '$key'")) val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type (${read[key]})") return adapter.fromJsonTree(read) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt index 796d8491..7c2399c3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -432,7 +432,7 @@ class FactoryAdapter private constructor( stringInterner = stringInterner, aliases = aliases, logMisses = logMisses, - elements = gson.getAdapter(JsonElement::class.java) + elements = Starbound.ELEMENTS_ADAPTER ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Properties.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Properties.kt index 81f477b3..2178d2dc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Properties.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Properties.kt @@ -1,9 +1,13 @@ package ru.dbotthepony.kstarbound.json.builder import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.Starbound import kotlin.properties.Delegates import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KProperty1 @@ -49,6 +53,12 @@ open class ReferencedProperty( } else { adapter = gson.getAdapter(TypeToken.getParameterized(KOptional.NotNull::class.java, subtype.type!!.javaType)) as TypeAdapter } + } else if (token.rawType === JsonElement::class.java) { + adapter = Starbound.ELEMENTS_ADAPTER as TypeAdapter + } else if (token.rawType === JsonArray::class.java) { + adapter = Starbound.ELEMENTS_ADAPTER.arrays as TypeAdapter + } else if (token.rawType === JsonObject::class.java) { + adapter = Starbound.ELEMENTS_ADAPTER.objects as TypeAdapter } else { adapter = gson.getAdapter(token) as TypeAdapter } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt index 9166b56b..cbeb03fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -4,6 +4,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet import org.apache.logging.log4j.LogManager import org.classdump.luna.ByteString import org.classdump.luna.LuaObject +import org.classdump.luna.LuaRuntimeException import org.classdump.luna.LuaType import org.classdump.luna.StateContext import org.classdump.luna.Table @@ -27,6 +28,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings +import ru.dbotthepony.kstarbound.util.random.random class LuaEnvironment : StateContext { private var nilMeta: Table? = null @@ -126,6 +128,7 @@ class LuaEnvironment : StateContext { val globals: Table = newTable() val executor: DirectCallExecutor = DirectCallExecutor.newExecutor() + var random = random() init { globals["_G"] = globals @@ -163,6 +166,39 @@ class LuaEnvironment : StateContext { StringLib.installInto(this, globals) OsLib.installInto(this, globals, RuntimeEnvironments.system()) + val math = globals["math"] as Table + + math["random"] = luaFunction { origin: Number?, bound: Number? -> + if (origin == null && bound == null) { + returnBuffer.setTo(random.nextDouble()) + } else if (bound == null) { + val origin = origin!!.toLong() + + if (origin > 1L) { + returnBuffer.setTo(random.nextLong(1L, origin)) + } else if (origin == 1L) { + returnBuffer.setTo(1L) + } else { + throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)") + } + } else { + val origin = origin!!.toLong() + val bound = bound.toLong() + + if (bound > origin) { + returnBuffer.setTo(random.nextLong(origin, bound)) + } else if (bound == origin) { + returnBuffer.setTo(origin) + } else { + throw LuaRuntimeException("bad argument #1 to 'random' (interval is empty)") + } + } + } + + math["randomseed"] = luaFunction { seed: Number -> + random = random(seed.toLong()) + } + // TODO: NYI, maybe polyfill? Utf8Lib.installInto(this, globals) @@ -210,7 +246,7 @@ class LuaEnvironment : StateContext { errorState = true } - fun init(): Boolean { + fun init(callInit: Boolean = true): Boolean { check(!initCalled) { "Already called init()" } initCalled = true @@ -227,15 +263,17 @@ class LuaEnvironment : StateContext { scripts.clear() - val init = globals["init"] + if (callInit) { + val init = globals["init"] - if (init is LuaFunction<*, *, *, *, *>) { - try { - executor.call(this, init) - } catch (err: Throwable) { - errorState = true - LOGGER.error("Exception on init()", err) - return false + if (init is LuaFunction<*, *, *, *, *>) { + try { + executor.call(this, init) + } catch (err: Throwable) { + errorState = true + LOGGER.error("Exception on init()", err) + return false + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt index d40b2359..ee620025 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/RootBindings.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.lua.bindings -import it.unimi.dsi.fastutil.longs.LongArrayList import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException @@ -9,7 +8,7 @@ import org.classdump.luna.lib.ArgumentIterator import org.classdump.luna.runtime.ExecutionContext import org.classdump.luna.runtime.LuaFunction import ru.dbotthepony.kstarbound.Globals -import ru.dbotthepony.kstarbound.RecipeRegistry +import ru.dbotthepony.kstarbound.item.RecipeRegistry import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound @@ -143,44 +142,6 @@ private fun recipesForItem(context: ExecutionContext, name: ByteString) { } } -private fun itemType(context: ExecutionContext, name: ByteString) { - context.returnBuffer.setTo(Registries.items[name.decode()]?.value?.itemType ?: throw NoSuchElementException("No such item $name")) -} - -private fun itemTags(context: ExecutionContext, name: ByteString) { - context.returnBuffer.setTo(context.from(Registries.items[name.decode()]?.value?.itemTags ?: throw NoSuchElementException("No such item $name"))) -} - -private fun itemHasTag(context: ExecutionContext, name: ByteString, tag: ByteString) { - context.returnBuffer.setTo(Registries.items[name.decode()]?.value?.itemTags?.contains(tag.decode()) ?: throw NoSuchElementException("No such item $name")) -} - -private fun itemConfig(context: ExecutionContext, args: ArgumentIterator): StateMachine { - val name = args.nextString().decode() - val state = StateMachine() - - if (name !in Registries.items) { - context.returnBuffer.setTo() - return state - } - - val desc = ItemDescriptor(args.nextTable(), state) - val level = args.nextOptionalFloat() - val seed = args.nextOptionalInteger() - - state.add { - val build = Starbound.itemConfig(desc.get().name, desc.get().parameters, level, seed) - - context.returnBuffer.setTo(context.newTable(0, 3).also { - it["directory"] = build.directory - it["config"] = context.from(build.config) - it["parameters"] = context.from(build.parameters) - }) - } - - return state -} - private fun getMatchingTenants(context: ExecutionContext, tags: Table) { val actualTags = Object2IntOpenHashMap() @@ -400,11 +361,11 @@ fun provideRootBindings(lua: LuaEnvironment) { table["projectileConfig"] = registryDef(Registries.projectiles) table["recipesForItem"] = luaFunction(::recipesForItem) - table["itemType"] = luaFunction(::itemType) - table["itemTags"] = luaFunction(::itemTags) - table["itemHasTag"] = luaFunction(::itemHasTag) + table["itemType"] = luaStub("itemType") + table["itemTags"] = luaStub("itemTags") + table["itemHasTag"] = luaStub("itemHasTag") - table["itemConfig"] = luaFunctionNS("itemConfig", ::itemConfig) + table["itemConfig"] = luaStub("itemConfig") table["createItem"] = luaStub("createItem") table["tenantConfig"] = registryDef(Registries.tenants) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 084e981f..52f3f930 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -1,9 +1,11 @@ package ru.dbotthepony.kstarbound.lua.bindings +import org.classdump.luna.Table import ru.dbotthepony.kstarbound.lua.LuaEnvironment import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.luaFunction import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.toVector2d import ru.dbotthepony.kstarbound.world.World fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { @@ -13,4 +15,12 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { callbacks["flyingType"] = luaFunction { returnBuffer.setTo(self.sky.flyingType.jsonName) } + + callbacks["magnitude"] = luaFunction { arg1: Table, arg2: Table? -> + if (arg2 == null) { + returnBuffer.setTo(toVector2d(arg1).length) + } else { + returnBuffer.setTo(self.geometry.diff(toVector2d(arg1), toVector2d(arg2)).length) + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt index b18de125..f67ea194 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/userdata/LuaRandomGenerator.kt @@ -67,7 +67,7 @@ class LuaRandomGenerator(var random: RandomGenerator) : Userdata - throw UnsupportedOperationException("Adding entropy is not supported on new engine. If you have legitimate usecase for this, please let us know at issue tracker or discord") + throw UnsupportedOperationException("Adding entropy is not supported on new engine. If you have legitimate usecase for this, please let us know on issue tracker or discord") }) // TODO: Lua, by definition, has no unsigned numbers, // and before 5.3, there were only doubles, longs (integers) were added diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 6c948018..f0b5d217 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -79,9 +79,14 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : private val legacyValidator = PacketRegistry.LEGACY.Validator(side) private val legacySerializer = PacketRegistry.LEGACY.Serializer(side) + // whenever protocol was negotiated var isConnected = false private set + // whenever successfully joined the game + var isReady = false + protected set + open fun setupLegacy() { isConnected = true LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") @@ -150,7 +155,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : } - abstract fun inGame() + open fun inGame() { + isReady = true + } fun send(packet: IPacket) { if (channel.isOpen && isConnected) { @@ -172,6 +179,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : abstract fun disconnect(reason: String = "") override fun close() { + LOGGER.info("Terminating $this") channel.close() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt index 1d0af071..5cc91dcd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerChannels.kt @@ -89,7 +89,9 @@ class ServerChannels(val server: StarboundServer) : Closeable { fun broadcast(packet: IPacket) { connections.forEach { - it.send(packet) + if (it.isReady) { + it.send(packet) + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 70847ff1..1f44b487 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -109,7 +109,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private var remoteVersion = 0L override fun flush() { - if (isConnected) { + if (isConnected && isReady) { val entries = rpc.write() if (entries != null || modifiedShipChunks.isNotEmpty() || server2clientGroup.upstream.hasChangedSince(remoteVersion)) { @@ -424,7 +424,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } fun tick(delta: Double) { - if (!isConnected || !channel.isOpen) + if (!isConnected || !channel.isOpen || !isReady) return flush() @@ -452,6 +452,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn flush() } + isReady = false tracker?.remove() tracker = null @@ -487,6 +488,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn private var countedTowardsPlayerCount = false override fun inGame() { + super.inGame() announcedDisconnect = false server.chat.systemMessage("Player '$nickname' connected") countedTowardsPlayerCount = true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index ac4c2f0d..63df76cc 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -238,6 +238,8 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk { - return eventLoop.scope.launch { prepare0() }.asCompletableFuture() + return eventLoop.scope.async { prepare0() }.asCompletableFuture() } private suspend fun findPlayerStart(hint: Vector2d? = null): Vector2d { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt index 7fe370d0..ac182729 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt @@ -16,6 +16,7 @@ import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector3i +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.pairAdapter @@ -59,7 +60,6 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { class Adapter(gson: Gson) : TypeAdapter() { private val vectors = gson.getAdapter(Vector2i::class.java) private val vectors3 = gson.getAdapter(Vector3i::class.java) - private val objects = gson.getAdapter(JsonObject::class.java) private val systems = gson.pairListAdapter() private val params = gson.getAdapter(CelestialParameters::class.java) private val lines = gson.pairAdapter() @@ -141,7 +141,7 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { if (`in`.consumeNull()) return null - val read = objects.read(`in`) + val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`) val chunkPos = read.get("chunkIndex", vectors) val chunk = UniverseChunk(chunkPos) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt index e39e7dca..385b267d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt @@ -55,6 +55,6 @@ object AssetPathStack { if (path.isNotEmpty() && path[0] == '/') return path - return if (base.endsWith('/')) "$base$path" else "${base.substringBeforeLast('/')}/$path" + return if (base.endsWith('/')) "$base$path" else "$base/$path" } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt index 0fd29fb1..f03928c7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/SBPattern.kt @@ -230,14 +230,14 @@ class SBPattern private constructor( if (open == -1) { if (i == 0) - pieces.add(Piece(contents = raw)) + pieces.add(Piece(contents = raw.sbIntern())) else - pieces.add(Piece(contents = raw.substring(i))) + pieces.add(Piece(contents = raw.substring(i).sbIntern())) break } else { if (open != i) { - pieces.add(Piece(contents = raw.substring(i, open))) + pieces.add(Piece(contents = raw.substring(i, open).sbIntern())) } val closing = raw.indexOf('>', startIndex = open + 1) @@ -246,7 +246,7 @@ class SBPattern private constructor( throw IllegalArgumentException("Malformed pattern string: $raw") } - pieces.add(Piece(name = Starbound.STRINGS.intern(raw.substring(open + 1, closing)))) + pieces.add(Piece(name = raw.substring(open + 1, closing).sbIntern())) i = closing + 1 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index f5d856d0..a9d36006 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.util.random import com.google.gson.JsonArray import com.google.gson.JsonElement import it.unimi.dsi.fastutil.bytes.ByteConsumer +import org.classdump.luna.ByteString import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.XXHash32 @@ -61,6 +62,7 @@ fun staticRandom32(vararg values: Any?): Int { for (value in values) { when (value) { is String -> digest.update(value.toByteArray()) + is ByteString -> digest.update(value.bytes) is Byte -> digest.update(value) is Boolean -> digest.update(if (value) 1 else 0) is Short -> toBytes(digest::update, value) @@ -100,6 +102,7 @@ fun staticRandom64(vararg values: Any?): Long { for (value in values) { when (value) { is String -> digest.update(value.toByteArray()) + is ByteString -> digest.update(value.bytes) is Byte -> digest.update(value) is Boolean -> digest.update(if (value) 1 else 0) is Short -> toBytes(digest::update, value) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index e9d9e49d..db8b1755 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -166,10 +166,9 @@ abstract class Chunk, This : Chunk) { val ours = cells.value - source.checkSizeEquals(ours) - for (x in 0 until CHUNK_SIZE) { - for (y in 0 until CHUNK_SIZE) { + for (x in 0 until width) { + for (y in 0 until height) { ours[x, y] = source[x, y].immutable() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt index e5cf470e..993d5944 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/UniversePos.kt @@ -18,6 +18,7 @@ import ru.dbotthepony.kommons.io.readVector3i import ru.dbotthepony.kommons.io.writeSignedVarInt import ru.dbotthepony.kommons.io.writeStruct3i import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import java.io.DataInputStream @@ -122,7 +123,6 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: class Adapter(gson: Gson) : TypeAdapter() { private val vectors = gson.getAdapter(Vector3i::class.java) - private val objects = gson.getAdapter(JsonObject::class.java) override fun write(out: JsonWriter, value: UniversePos) { out.beginObject() @@ -141,7 +141,7 @@ data class UniversePos(val location: Vector3i = Vector3i.ZERO, val planetOrbit: override fun read(`in`: JsonReader): UniversePos { if (`in`.peek() == JsonToken.BEGIN_OBJECT) { - val values = objects.read(`in`)!! + val values = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)!! val location = values.get("location", vectors) val planet = values.get("planet", 0) val orbit = values.get("orbit", 0) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 7a8ef7ed..1a9b8f31 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -31,10 +31,12 @@ import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.TileView import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.DynamicEntity +import ru.dbotthepony.kstarbound.world.entities.MovementController import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly +import java.util.concurrent.CompletableFuture import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -285,11 +287,32 @@ abstract class World, ChunkType : Chunk>() + var batch = ArrayList() + + for (entity in dynamicEntities) { + batch.add(entity.movement) + + if (batch.size == 32) { + val b = batch + tasks.add(CompletableFuture.runAsync(Runnable { b.forEach { it.move(delta) } }, Starbound.EXECUTOR)) + batch = ArrayList() + } + } + + if (batch.isNotEmpty()) { + tasks.add(CompletableFuture.runAsync(Runnable { batch.forEach { it.move(delta) } }, Starbound.EXECUTOR)) + } + + CompletableFuture.allOf(*tasks.toTypedArray()).join() + } entityList.forEach { try { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt index 48a90d26..e7f1b6ab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt @@ -491,7 +491,7 @@ class Animator() { for ((part, tags) in config.partTagDefaults) { for ((k, v) in tags) { - setPartTag(part, k, v) + partTags.computeIfAbsent(part) { NetworkedMap(InternedStringCodec, InternedStringCodec) }.put(k, v) } } @@ -602,8 +602,10 @@ class Animator() { } fun setPartTag(partName: String, tagKey: String, tagValue: String) { - val tags = partTags[partName] ?: throw IllegalArgumentException("Unknown part $partName!") - tags[tagKey] = tagValue + partTags.computeIfAbsent(partName) { + LOGGER.warn("Creating part tags for $it after initialization, this can cause client-server desyncs") + NetworkedMap(InternedStringCodec, InternedStringCodec) + }.put(tagKey, tagValue) } private fun setupNetworkElements() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index af071b18..7acd6d8e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -28,6 +28,7 @@ import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.physics.CollisionPoly import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.Poly +import java.util.LinkedList import kotlin.math.PI import kotlin.math.absoluteValue import kotlin.math.acos diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt index 901171c7..f6969ee1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.entities import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.ObjectArraySet +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.collect.ListenableMap import ru.dbotthepony.kommons.io.IntValueCodec import ru.dbotthepony.kommons.io.KOptionalIntValueCodec @@ -173,8 +174,17 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf abstract val maxValue: Double? fun setAsPercentage(percent: Double) { - val maxValue = maxValue ?: throw IllegalArgumentException("$name does not have max value") - this.value = maxValue * percent + val maxValue = maxValue + + if (maxValue == null) { + if (percent == 0.0) { + LOGGER.warn("Preserving odd quirk of original engine: $name requires resource $max to be present to be set as 0% of that, however, $max is missing. Due to flawed `if` branch in original engine, `initialPercentage` specified as `0` falls through condition and is treated as `initialValue`=`0`") + } else { + throw IllegalArgumentException("$name does not have max value") + } + } else { + this.value = maxValue * percent + } } } @@ -375,5 +385,6 @@ class StatusController(val entity: ActorEntity, val config: StatusControllerConf companion object { private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList) private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList) + private val LOGGER = LogManager.getLogger() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index 0811efce..983130fb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -5,6 +5,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kommons.io.readByteArray @@ -13,6 +14,7 @@ import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.InteractAction import ru.dbotthepony.kstarbound.defs.InteractRequest @@ -28,6 +30,8 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.util.ManualLazy import ru.dbotthepony.kstarbound.util.RelativeClock +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import java.io.DataInputStream import java.io.DataOutputStream @@ -36,7 +40,12 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co var opened by networkedSignedInt().also { networkGroup.upstream.add(it) } var isCrafting by networkedBoolean().also { networkGroup.upstream.add(it) } var craftingProgress by networkedFloat().also { networkGroup.upstream.add(it); it.interpolator = Interpolator.Linear } - val items = Container(0).also { networkGroup.upstream.add(it) } + val items = Container(lookupProperty("slotCount").asInt).also { networkGroup.upstream.add(it) } + + override fun parametersUpdated() { + super.parametersUpdated() + items.size = lookupProperty("slotCount").asInt + } // whenever set loot seed, put initial items, etc. private var isInitialized = false @@ -100,6 +109,52 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co return data } + override fun onJoinWorld(world: World<*, *>) { + if (!isRemote) + isInteractive = true + + super.onJoinWorld(world) + + if (!isRemote) { + if (isInitialized) return + isInitialized = true + + var level = world.template.threatLevel + val seed = lookupProperty("treasureSeed") { JsonPrimitive(world.random.nextLong()) }.asLong + level = lookupProperty("level") { JsonPrimitive(level) }.asDouble + level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble + + val initialItems = lookupProperty("initialItems") + + if (!initialItems.isJsonNull) { + for (item in initialItems.asJsonArray) { + items.add(ItemStack.create(ItemDescriptor(item).build(level, seed))) + } + } + + val treasurePools = lookupProperty("treasurePools") + + if (!treasurePools.isJsonNull) { + val random = random(seed) + val get = treasurePools.asJsonArray.random(random).asString + val treasurePool = Registries.treasurePools[get] + + if (treasurePool == null) { + LOGGER.error("Unknown treasure pool $get! Can't generate container contents at $tilePosition.") + } else { + for (item in treasurePool.value.evaluate(random, level)) { + val leftover = items.add(ItemStack.create(item)) + + if (leftover.isNotEmpty) { + LOGGER.warn("Tried to overfill container at $tilePosition") + lostItems.add(leftover) + } + } + } + } + } + } + // Networking of this container to legacy clients is incredibly stupid, // and networks entire state each time something has changed. inner class Container(size: Int) : NetworkedElement(), IContainer { @@ -181,10 +236,7 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co val wrapper = FastByteArrayOutputStream() val stream = DataOutputStream(wrapper) stream.writeVarInt(size) - var setItemsSize = items.indexOfLast { it.isNotEmpty } - - if (setItemsSize == -1) - setItemsSize = 0 + val setItemsSize = items.indexOfLast { it.isNotEmpty } + 1 stream.writeVarInt(setItemsSize) @@ -256,4 +308,8 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co override fun disableInterpolation() {} override fun tickInterpolation(delta: Double) {} } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt index cb85c3b8..d5f076f8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/TerrainSelectorType.kt @@ -62,8 +62,6 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, * out.value(value.toJson()) } - private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } - fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { return Registries.terrainSelectors.getOrThrow(name).value.create(parameters) } @@ -72,7 +70,7 @@ enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, * if (`in`.consumeNull()) return null - return load(objects.read(`in`)) + return load(Starbound.ELEMENTS_ADAPTER.objects.read(`in`)) } fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> {