package ru.dbotthepony.kstarbound import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.gson.GsonBuilder 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.JsonSyntaxException import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import kotlinx.coroutines.async import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.DamageKind import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonFunction import ru.dbotthepony.kstarbound.defs.MarkovTextGenerator import ru.dbotthepony.kstarbound.defs.actor.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.monster.MonsterSkillDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.actor.TenantDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition import ru.dbotthepony.kstarbound.defs.ProjectileDefinition import ru.dbotthepony.kstarbound.defs.actor.DanceDefinition import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorNodeDefinition import ru.dbotthepony.kstarbound.defs.monster.MonsterPaletteSwap import ru.dbotthepony.kstarbound.defs.monster.MonsterPartDefinition import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.RenderParameters import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition 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.defs.world.SpawnType import ru.dbotthepony.kstarbound.item.ItemRegistry import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.fromJsonTreeFast import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.world.physics.CollisionType import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.Future import java.util.random.RandomGenerator import kotlin.NoSuchElementException import kotlin.collections.ArrayList import kotlin.collections.HashMap object Registries { private val LOGGER = LogManager.getLogger() private val registriesInternal = ArrayList>() val registries: List> = Collections.unmodifiableList(registriesInternal) private val adapters = ArrayList() fun registerAdapters(gsonBuilder: GsonBuilder) { adapters.forEach { gsonBuilder.registerTypeAdapterFactory(it) } } val tiles = Registry("tiles").also(registriesInternal::add).also { adapters.add(it.adapter()) } val tileModifiers = Registry("tile modifiers").also(registriesInternal::add).also { adapters.add(it.adapter()) } val liquid = Registry("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) } 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 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()) } val json2Functions = Registry("json 2function").also(registriesInternal::add).also { adapters.add(it.adapter()) } val jsonConfigFunctions = Registry("json config function").also(registriesInternal::add).also { adapters.add(it.adapter()) } val projectiles = Registry("projectile").also(registriesInternal::add).also { adapters.add(it.adapter()) } val tenants = Registry("tenant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treasurePools = Registry("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treasureChests = Registry("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterSkills = Registry("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterTypes = Registry("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) } val spawnTypes = Registry("spawn type").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterPalettes = Registry("monster palette").also(registriesInternal::add).also { adapters.add(it.adapter()) } val behavior = Registry("behavior").also(registriesInternal::add).also { adapters.add(it.adapter()) } val behaviorNodes = Registry("behavior node").also(registriesInternal::add).also { adapters.add(it.adapter()) } val worldObjects = Registry("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) } val biomes = Registry("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) } val terrainSelectors = Registry>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) } val grassVariants = Registry("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeStemVariants = Registry("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treeFoliageVariants = Registry("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val bushVariants = Registry("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dungeons = Registry("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) } val markovGenerators = Registry("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) } val damageKinds = Registry("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) } val dance = Registry("dance").also(registriesInternal::add).also { adapters.add(it.adapter()) } private val monsterParts = HashMap, HashMap>>() private val loggedMonsterPartMisses = Collections.synchronizedSet(ObjectOpenHashSet>()) private val stagehands = HashMap>() fun makeStagehandConfig(type: String, overrides: JsonElement? = JsonNull.INSTANCE): JsonObject { val (data) = stagehands[type] ?: throw NoSuchElementException("No such stagehand: $type") return mergeJson(data.deepCopy(), overrides ?: JsonNull.INSTANCE) } private fun loadStagehands(files: Collection, patches: Map>): List> { return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]).asJsonObject val type = elem["type"].asString Starbound.submit { val existing = stagehands.put(type, elem to listedFile) if (existing != null) { LOGGER.warn("Redefining stagehand prototype $type (new def originate from $listedFile, existing originate from ${existing.second})") } } } catch (err: Throwable) { LOGGER.error("Loading stagehand definition file $listedFile", err) } }.asCompletableFuture() } } fun selectMonsterPart(category: String, type: String, random: RandomGenerator): MonsterPartDefinition? { val key = category to type val get = monsterParts[key] if (get.isNullOrEmpty()) { if (loggedMonsterPartMisses.add(key)) { LOGGER.error("No such monster part combination of category '$category' and type '$type'") } return null } return get.values.random(random, true).first } fun getMonsterPart(category: String, type: String, name: String): MonsterPartDefinition? { return monsterParts[category to type]?.get(name)?.first } private fun loadMonsterParts(files: Collection, patches: Map>): List> { val adapter by lazy { Starbound.gson.getAdapter(MonsterPartDefinition::class.java) } return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) val next = AssetPathStack(listedFile.computeFullPath()) { adapter.fromJsonTreeFast(elem) } Starbound.submit { val map = monsterParts.computeIfAbsent(next.category to next.type) { HashMap() } val old = map[next.name] if (old != null) { LOGGER.error("Duplicate monster part '${next.name}' of category '${next.category}' and type '${next.type}', originating from $listedFile (old originate from ${old.second})") } else { map[next.name] = next to listedFile } } } catch (err: Throwable) { LOGGER.error("Loading monster part definition file $listedFile", err) } }.asCompletableFuture() } } private val npcTypes = HashMap>() fun buildNPCConfig(type: String, overrides: JsonElement = JsonNull.INSTANCE): JsonObject { val baseConfig = npcTypes.getOrElse(type) { throw NoSuchElementException("No such NPC type $type") }.second val config = mergeJson(baseConfig.deepCopy(), overrides) if ("baseType" in baseConfig) { return buildNPCConfig(baseConfig["baseType"].asString, config) } else { return config } } private fun loadNpcTypes(files: Collection, patches: Map>): List> { return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject val type = elem.get("type").asString if ("scripts" in elem) { val fPath = listedFile.computeFullPath() val scripts = elem["scripts"].asJsonArray for (i in 0 until scripts.size()) { scripts[i] = JsonPrimitive(AssetPathStack.relativeTo(fPath, scripts[i].asString)) } } Starbound.submit { val existing = npcTypes.put(type, listedFile to elem) if (existing != null) { LOGGER.warn("Overwriting existing NPC definition '$type' (new originate from $listedFile, old originating from ${existing.first})") } } } catch (err: Throwable) { LOGGER.error("Loading NPC type definition file $listedFile", err) } }.asCompletableFuture() } } private fun key(mapper: (T) -> String): (T) -> Pair> { return { mapper.invoke(it) to KOptional() } } private fun key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair> { return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) } } fun validate(): CompletableFuture { val futures = ArrayList>() for (registry in registriesInternal) futures.add(CompletableFuture.supplyAsync({ registry.validate() }, Starbound.EXECUTOR)) return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } } } private inline fun loadRegistry( registry: Registry, patches: Map>, files: Collection, noinline keyProvider: (T) -> Pair>, noinline after: (T, IStarboundFile) -> Unit = { _, _ -> } ): List> { val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val elem = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) AssetPathStack(listedFile.computeFullPath()) { val read = adapter.fromJsonTreeFast(elem) val keys = keyProvider(read) after(read, listedFile) Starbound.submit { registry.add( key = keys.first, value = read, id = keys.second, json = elem, file = listedFile ) }.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition file $listedFile", err); null } } } catch (err: Throwable) { LOGGER.error("Loading ${registry.name} definition file $listedFile", err) } }.asCompletableFuture() } } fun load(fileTree: Map>, patchTree: Map>): List> { val tasks = ArrayList>() tasks.addAll(ItemRegistry.load(fileTree, patchTree)) tasks.addAll(loadTerrainSelectors(fileTree, patchTree)) 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, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name))) tasks.addAll(loadMonsterParts(fileTree["monsterpart"] ?: listOf(), patchTree)) tasks.addAll(loadStagehands(fileTree["stagehand"] ?: listOf(), patchTree)) tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) tasks.addAll(loadRegistry(monsterTypes, patchTree, fileTree["monstertype"] ?: listOf(), key(MonsterTypeDefinition::type))) tasks.addAll(loadRegistry(monsterPalettes, patchTree, fileTree["monstercolors"] ?: listOf(), key(MonsterPaletteSwap::name))) tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) 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(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(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name))) tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName))) tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name))) tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind))) tasks.addAll(loadRegistry(dance, patchTree, fileTree["dance"] ?: listOf(), key(DanceDefinition::name))) tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree)) 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(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it }) tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it }) // because someone couldn't handle their mushroom vine that day, and decided to make third way of // declaring game data tasks.addAll(loadMixed(spawnTypes, fileTree["spawntypes"] ?: listOf(), patchTree, SpawnType::name)) tasks.addAll(loadNpcTypes(fileTree["npctype"] ?: listOf(), patchTree)) return tasks } 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) } return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject for ((k, v) in json.entrySet()) { try { val value = adapter.fromJsonTreeFast(v) transform(value, k) Starbound.submit { registry.add(k, value, v, listedFile) }.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err); null } } catch (err: Exception) { LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err) } } } catch (err: Exception) { LOGGER.error("Loading ${registry.name} definition $listedFile", err) } }.asCompletableFuture() } } private inline fun loadMixed(registry: Registry, files: Collection, patches: Map>, noinline key: T.() -> String): List> { val adapter by lazy { Starbound.gson.getAdapter(T::class.java) } return files.map { listedFile -> Starbound.GLOBAL_SCOPE.launch { try { val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonArray for ((i, v) in json.withIndex()) { try { val value = adapter.fromJsonTreeFast(v) val getKey = key(value) Starbound.submit { registry.add(getKey, value, v, listedFile) }.exceptionally { err -> LOGGER.error("Loading ${registry.name} definition at name '$getKey' from file $listedFile", err); null } } catch (err: Exception) { LOGGER.error("Loading ${registry.name} definition at index $i from file $listedFile", err) } } } catch (err: Exception) { LOGGER.error("Loading ${registry.name} definition $listedFile", err) } }.asCompletableFuture() } } private suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map>) { try { val json = JsonPatch.applyAsync(listedFile.asyncJsonReader(), patches[listedFile.computeFullPath()]) as JsonObject val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") val factory = TerrainSelectorType.factory(json, false, type) Starbound.submit { terrainSelectors.add(name, factory) } } catch (err: Exception) { LOGGER.error("Loading terrain selector $listedFile", err) } } private fun loadTerrainSelectors(files: Map>, patches: Map>): List> { val tasks = ArrayList>() tasks.addAll((files["terrain"] ?: listOf()).map { listedFile -> Starbound.GLOBAL_SCOPE.async { loadTerrainSelector(listedFile, null, patches) }.asCompletableFuture() }) // legacy files for (type in TerrainSelectorType.entries) { tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile -> Starbound.GLOBAL_SCOPE.async { loadTerrainSelector(listedFile, type, patches) }.asCompletableFuture() }) } return tasks } @JsonFactory data class MetaMaterialDef( val materialId: Int, val name: String, val collisionKind: CollisionType, val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision, val isConnectable: Boolean = true, val supportsMods: Boolean = false, ) private fun loadMetaMaterials(): Future<*> { return Starbound.GLOBAL_SCOPE.async { val read = Starbound.loadJsonAsset("/metamaterials.config").await() ?: return@async val read2 = Starbound.gson.getAdapter(object : TypeToken>() {}).fromJsonTreeFast(read) for (def in read2) { Starbound.submit { tiles.add(key = "metamaterial:${def.name}", id = KOptional(def.materialId), value = TileDefinition( isMeta = true, materialId = def.materialId, materialName = "metamaterial:${def.name}", descriptionData = ThingDescription.EMPTY, category = "meta", renderTemplate = AssetReference.empty(), renderParameters = RenderParameters.META, isConnectable = def.isConnectable, supportsMods = def.supportsMods, damageTable = AssetReference(TileDamageParameters( damageFactors = ImmutableMap.of(), damageRecovery = Double.MAX_VALUE, maximumEffectTime = 0.0, totalHealth = Double.MAX_VALUE, harvestLevel = Int.MAX_VALUE, )) )) } } }.asCompletableFuture() } }