KStarbound/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt

286 lines
14 KiB
Kotlin

package ru.dbotthepony.kstarbound
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory
import com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction
import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
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
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.projectile.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
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.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.util.AssetPathStack
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<Registry<*>>()
val registries: List<Registry<*>> = Collections.unmodifiableList(registriesInternal)
private val adapters = ArrayList<TypeAdapterFactory>()
fun registerAdapters(gsonBuilder: GsonBuilder) {
adapters.forEach { gsonBuilder.registerTypeAdapterFactory(it) }
}
val tiles = Registry<TileDefinition>("tiles").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val tileModifiers = Registry<TileModifierDefinition>("tile modifiers").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val liquid = Registry<LiquidDefinition>("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val particles = Registry<ParticleConfig>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val items = Registry<IItemDefinition>("item").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val jsonFunctions = Registry<JsonFunction>("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val json2Functions = Registry<Json2Function>("json 2function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val jsonConfigFunctions = Registry<JsonConfigFunction>("json config function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val npcTypes = Registry<NpcTypeDefinition>("npc type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val projectiles = Registry<ProjectileDefinition>("projectile").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val tenants = Registry<TenantDefinition>("tenant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treasurePools = Registry<TreasurePoolDefinition>("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val biomes = Registry<BiomeDefinition>("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val terrainSelectors = Registry<TerrainSelectorType.Factory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val grassVariants = Registry<GrassVariant.Data>("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeFoliageVariants = Registry<TreeVariant.FoliageData>("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, KOptional<Int?>> {
return { mapper.invoke(it) to KOptional() }
}
private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair<String, KOptional<Int?>> {
return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) }
}
fun validate(): CompletableFuture<Boolean> {
val futures = ArrayList<CompletableFuture<Boolean>>()
for (registry in registriesInternal)
futures.add(CompletableFuture.supplyAsync { registry.validate() })
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } }
}
private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>,
files: List<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
): List<Future<*>> {
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 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)
}
}
}
}
fun finishLoad() {
registriesInternal.forEach { it.finishLoad() }
}
fun load(fileTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
tasks.addAll(loadItemDefinitions(fileTree))
tasks.addAll(loadTerrainSelectors(fileTree))
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(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(dungeons, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::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 })
return tasks
}
private fun loadItemDefinitions(files: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
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<Future<*>>()
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 <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
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 })
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)
}
}
}
}
private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?) {
try {
val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })
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<String, Collection<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, null)
}
})
// legacy files
for (type in TerrainSelectorType.entries) {
tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
Starbound.EXECUTOR.submit {
loadTerrainSelector(listedFile, type)
}
})
}
return tasks
}
}