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.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken 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.util.KOptional import ru.dbotthepony.kstarbound.defs.AssetReference 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.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.npc.NpcTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.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.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.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 import ru.dbotthepony.kstarbound.world.physics.CollisionType import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.Future import kotlin.collections.ArrayList 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 npcTypes = Registry("npc type").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 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()) } 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(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) AssetPathStack(listedFile.computeDirectory()) { val read = adapter.fromJsonTree(elem) val keys = keyProvider(read) after(read, listedFile) registry.add { registry.add( key = keys.first, value = read, id = keys.second, json = elem, file = listedFile ) } } } catch (err: Throwable) { LOGGER.error("Loading ${registry.name} definition file $listedFile", err) } }.asCompletableFuture() } } fun finishLoad() { registriesInternal.forEach { it.finishLoad() } } 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(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(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name))) 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 }) 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(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject for ((k, v) in json.entrySet()) { try { val value = adapter.fromJsonTree(v) transform(value, k) registry.add { registry.add(k, value, v, listedFile) } } 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 suspend fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map>) { try { val json = JsonPatch.applyAsync(Starbound.ELEMENTS_ADAPTER.read(listedFile.asyncJsonReader().await()), patches[listedFile.computeFullPath()]) as JsonObject val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field") val factory = TerrainSelectorType.factory(json, false, type) terrainSelectors.add { 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>() {}).fromJsonTree(read) for (def in read2) { tiles.add { 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() } }