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

293 lines
15 KiB
Kotlin

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<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 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 treasureChests = Registry<TreasureChestDefinition>("treasure chest").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()) }
val markovGenerators = Registry<MarkovTextGenerator>("markov text generator").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() }, Starbound.EXECUTOR))
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } }
}
private inline fun <reified T : Any> loadRegistry(
registry: Registry<T>,
patches: Map<String, Collection<IStarboundFile>>,
files: Collection<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) }
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<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
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 <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
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<String, Collection<IStarboundFile>>) {
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<String, Collection<IStarboundFile>>, patches: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>()
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<ImmutableList<MetaMaterialDef>>() {}).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()
}
}