diff --git a/ADDITIONS.md b/ADDITIONS.md new file mode 100644 index 00000000..a9a1a926 --- /dev/null +++ b/ADDITIONS.md @@ -0,0 +1,20 @@ + +## JSON additions + +### Worldgen + * Where applicable, Perlin noise now can have custom seed specified + * Change above allows to explicitly specify universe seed (as celestial.config:systemTypePerlin:seed) + * Perlin noise now can be of arbitrary scale (defaults to 512, specified with 'scale' key, integer type, >=16) + +#### Terrain + * Nested terrain selectors now get their unique seeds (displacement selector can now properly be nested inside other displacement selector) + * Previously, all nested terrain selectors were based off the same seed + * displacement terrain selector has xClamp added, works like yClamp + +#### Biomes + * Tree biome placeables now have `variantsRange` (defaults to `[1, 1]`) and `subVariantsRange` (defaults to `[2, 2]`) + * `variantsRange` is responsible for "stem-foliage" combinations + * `subVariantsRange` is responsible for "stem-foliage" hue shift combinations + * Rolled per each "stem-foliage" combination + * Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination + * Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees. diff --git a/build.gradle.kts b/build.gradle.kts index e5a61cab..4212f965 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,8 @@ version = "0.1-SNAPSHOT" val lwjglVersion: String by project val lwjglNatives: String by project +val kotlinVersion: String by project +val kotlinCoroutinesVersion: String by project repositories { mavenCentral() @@ -39,8 +41,9 @@ tasks.compileKotlin { dependencies { val kommonsVersion: String by project - implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.10") - implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10") + implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion") implementation("org.apache.logging.log4j:log4j-api:2.17.1") implementation("org.apache.logging.log4j:log4j-core:2.17.1") diff --git a/gradle.properties b/gradle.properties index 0304721e..90133a14 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,9 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -kotlinVersion=1.9.0 -kommonsVersion=2.7.16 +kotlinVersion=1.9.10 +kotlinCoroutinesVersion=1.8.0 +kommonsVersion=2.9.20 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index ba5284c4..d84a166e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -1,13 +1,23 @@ package ru.dbotthepony.kstarbound +import com.google.common.collect.ImmutableMap +import com.google.gson.TypeAdapter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.MovementParameters +import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig +import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig +import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig +import ru.dbotthepony.kstarbound.defs.world.DungeonWorldsConfig +import ru.dbotthepony.kstarbound.defs.world.WorldTemplateConfig +import ru.dbotthepony.kstarbound.json.mapAdapter +import ru.dbotthepony.kstarbound.json.pairSetAdapter import ru.dbotthepony.kstarbound.util.AssetPathStack import java.util.concurrent.ExecutorService import java.util.concurrent.ForkJoinTask import java.util.concurrent.Future +import kotlin.properties.Delegates import kotlin.reflect.KMutableProperty0 object GlobalDefaults { @@ -22,6 +32,27 @@ object GlobalDefaults { var clientParameters = ClientConfigParameters() private set + var worldTemplate by Delegates.notNull() + private set + + var terrestrialWorlds by Delegates.notNull() + private set + + var asteroidWorlds by Delegates.notNull() + private set + + var dungeonWorlds by Delegates.notNull>() + private set + + var grassDamage by Delegates.notNull() + private set + + var treeDamage by Delegates.notNull() + private set + + var bushDamage by Delegates.notNull() + private set + private object EmptyTask : ForkJoinTask() { private fun readResolve(): Any = EmptyTask override fun getRawResult() { @@ -35,30 +66,44 @@ object GlobalDefaults { } } - private inline fun load(path: String, accept: KMutableProperty0, executor: ExecutorService): Future<*> { - val file = Starbound.locate(path) + private fun load(path: String, accept: KMutableProperty0, adapter: TypeAdapter): Future<*> { + val file = Starbound.loadJsonAsset(path) - if (!file.exists) { - LOGGER.fatal("$path does not exist, expect bad things to happen!") - return EmptyTask - } else if (!file.isFile) { - LOGGER.fatal("$path is not a file, expect bad things to happen!") + if (file == null) { + LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!") return EmptyTask } else { - return executor.submit { - AssetPathStack("/") { - accept.set(Starbound.gson.fromJson(file.jsonReader(), T::class.java)) + return Starbound.EXECUTOR.submit { + try { + AssetPathStack("/") { + accept.set(adapter.fromJsonTree(file)) + } + } catch (err: Throwable) { + LOGGER.fatal("Error while reading $path, expect bad things to happen!", err) + throw err } } } } - fun load(executor: ExecutorService): List> { + private inline fun load(path: String, accept: KMutableProperty0): Future<*> { + return load(path, accept, Starbound.gson.getAdapter(T::class.java)) + } + + fun load(): List> { val tasks = ArrayList>() - tasks.add(load("/default_actor_movement.config", ::actorMovementParameters, executor)) - tasks.add(load("/default_movement.config", ::movementParameters, executor)) - tasks.add(load("/client.config", ::clientParameters, executor)) + tasks.add(load("/default_actor_movement.config", ::actorMovementParameters)) + tasks.add(load("/default_movement.config", ::movementParameters)) + tasks.add(load("/client.config", ::clientParameters)) + tasks.add(load("/terrestrial_worlds.config", ::terrestrialWorlds)) + tasks.add(load("/asteroids_worlds.config", ::asteroidWorlds)) + tasks.add(load("/world_template.config", ::worldTemplate)) + tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) + + tasks.add(load("/plants/grassDamage.config", ::grassDamage)) + tasks.add(load("/plants/treeDamage.config", ::treeDamage)) + tasks.add(load("/plants/bushDamage.config", ::bushDamage)) return tasks } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt index b0b45234..41c3cd5d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Main.kt @@ -1,14 +1,17 @@ package ru.dbotthepony.kstarbound -import it.unimi.dsi.fastutil.floats.FloatArrayList +import kotlinx.coroutines.async +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.future.future +import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.LogManager import org.lwjgl.Version -import ru.dbotthepony.kommons.io.BTreeDB6 import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.client.StarboundClient +import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource @@ -23,7 +26,6 @@ import java.io.DataInputStream import java.io.File import java.net.InetSocketAddress import java.util.* -import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.zip.Inflater import java.util.zip.InflaterInputStream @@ -38,10 +40,28 @@ fun main() { val data = ServerUniverse() val t = System.nanoTime() - val result = data.scanConstellationLines(AABBi(Vector2i(-100, -100), Vector2i(100, 100))).get() + val result = Starbound.COROUTINES.future { + val systems = data.scanSystems(AABBi(Vector2i(-50, -50), Vector2i(50, 50)), setOf("whitestar")) + + for (system in systems) { + for (children in data.children(system)) { + if (children.isPlanet) { + val params = data.parameters(children)!! + + if (params.visitableParameters != null) { + //val write = params.visitableParameters!!.toJson(false) + //println(write) + //println(Starbound.gson.fromJson(write, VisitableWorldParameters::class.java)) + } + } + } + } + + systems + }.get() + println(System.nanoTime() - t) - println(result) data.close() return @@ -74,8 +94,8 @@ fun main() { val server = IntegratedStarboundServer(File("./")) val client = StarboundClient.create().get() //val client2 = StarboundClient.create().get() - val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false)) - world.addChunkSource(LegacyChunkSource(db)) + val world = ServerWorld(server, WorldGeometry(Vector2i(3000, 2000), true, false)) + world.addChunkSource(LegacyChunkSource.file(db)) world.thread.start() //Starbound.addFilePath(File("./unpacked_assets/")) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt index 2cc74fe7..72b9e670 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt @@ -67,14 +67,14 @@ object RecipeRegistry { } } - fun load(fileTree: Map>, executor: ExecutorService): List> { + fun load(fileTree: Map>): List> { val files = fileTree["recipe"] ?: return emptyList() val elements = Starbound.gson.getAdapter(JsonElement::class.java) val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java) return files.map { listedFile -> - executor.submit { + Starbound.EXECUTOR.submit { try { val json = elements.read(listedFile.jsonReader()) val value = recipes.fromJsonTree(json) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index b40b502b..b421ad49 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -1,11 +1,14 @@ package ru.dbotthepony.kstarbound +import com.google.gson.GsonBuilder import com.google.gson.JsonElement import com.google.gson.JsonObject -import com.google.gson.internal.bind.JsonTreeReader +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory import com.google.gson.stream.JsonReader import org.apache.logging.log4j.LogManager 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 @@ -33,33 +36,56 @@ import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier 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.terrain.BiomeDefinition +import ru.dbotthepony.kstarbound.defs.world.terrain.TerrainSelectorFactory +import ru.dbotthepony.kstarbound.defs.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.util.AssetPathStack +import java.util.* +import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutorService import java.util.concurrent.Future +import java.util.function.Supplier +import kotlin.collections.ArrayList object Registries { private val LOGGER = LogManager.getLogger() - private val registries = ArrayList>() + private val registriesInternal = ArrayList>() + val registries: List> = Collections.unmodifiableList(registriesInternal) + private val adapters = ArrayList() - val tiles = Registry("tiles").also(registries::add) - val tileModifiers = Registry("tile modifiers").also(registries::add) - val liquid = Registry("liquid").also(registries::add) - val species = Registry("species").also(registries::add) - val statusEffects = Registry("status effects").also(registries::add) - val particles = Registry("particles").also(registries::add) - val items = Registry("items").also(registries::add) - val questTemplates = Registry("quest templates").also(registries::add) - val techs = Registry("techs").also(registries::add) - val jsonFunctions = Registry("json functions").also(registries::add) - val json2Functions = Registry("json 2functions").also(registries::add) - val npcTypes = Registry("npc types").also(registries::add) - val projectiles = Registry("projectiles").also(registries::add) - val tenants = Registry("tenants").also(registries::add) - val treasurePools = Registry("treasure pools").also(registries::add) - val monsterSkills = Registry("monster skills").also(registries::add) - val monsterTypes = Registry("monster types").also(registries::add) - val worldObjects = Registry("objects").also(registries::add) + 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 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()) } + 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 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()) } private fun key(mapper: (T) -> String): (T) -> Pair { return { mapper.invoke(it) to null } @@ -69,33 +95,16 @@ object Registries { return { mapper.invoke(it) to mapperInt.invoke(it) } } - fun validate(): Boolean { - var any = false + fun validate(): CompletableFuture { + val futures = ArrayList>() - any = !tiles.validate() || any - any = !tileModifiers.validate() || any - any = !liquid.validate() || any - any = !species.validate() || any - any = !statusEffects.validate() || any - any = !particles.validate() || any - any = !items.validate() || any - any = !questTemplates.validate() || any - any = !techs.validate() || any - any = !jsonFunctions.validate() || any - any = !json2Functions.validate() || any - any = !npcTypes.validate() || any - any = !projectiles.validate() || any - any = !tenants.validate() || any - any = !treasurePools.validate() || any - any = !monsterSkills.validate() || any - any = !monsterTypes.validate() || any - any = !worldObjects.validate() || any + for (registry in registriesInternal) + futures.add(CompletableFuture.supplyAsync { registry.validate() }) - return !any + return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } } } private inline fun loadRegistry( - executor: ExecutorService, registry: Registry, files: List, noinline keyProvider: (T) -> Pair @@ -104,9 +113,10 @@ object Registries { val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) } return files.map { listedFile -> - executor.submit { + 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) @@ -126,35 +136,43 @@ object Registries { } fun finishLoad() { - registries.forEach { it.finishLoad() } + registriesInternal.forEach { it.finishLoad() } } - fun load(fileTree: Map>, executor: ExecutorService): List> { + fun load(fileTree: Map>): List> { val tasks = ArrayList>() - tasks.addAll(loadItemDefinitions(fileTree, executor)) + tasks.addAll(loadItemDefinitions(fileTree)) - tasks.addAll(loadJsonFunctions(fileTree["functions"] ?: listOf(), executor)) - tasks.addAll(loadJson2Functions(fileTree["2functions"] ?: listOf(), executor)) - tasks.addAll(loadTreasurePools(fileTree["treasurepools"] ?: listOf(), executor)) + tasks.addAll(loadTerrainSelectors(fileTree["terrain"] ?: listOf())) - tasks.addAll(loadRegistry(executor, tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) - tasks.addAll(loadRegistry(executor, tileModifiers, fileTree["matmod"] ?: listOf(), key(MaterialModifier::modName, MaterialModifier::modId))) - tasks.addAll(loadRegistry(executor, liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId))) + tasks.addAll(loadRegistry(tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) + tasks.addAll(loadRegistry(tileModifiers, fileTree["matmod"] ?: listOf(), key(MaterialModifier::modName, MaterialModifier::modId))) + tasks.addAll(loadRegistry(liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId))) - tasks.addAll(loadRegistry(executor, worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) - tasks.addAll(loadRegistry(executor, statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) - tasks.addAll(loadRegistry(executor, species, fileTree["species"] ?: listOf(), key(Species::kind))) - tasks.addAll(loadRegistry(executor, particles, fileTree["particle"] ?: listOf(), key(ParticleDefinition::kind))) - tasks.addAll(loadRegistry(executor, questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) - tasks.addAll(loadRegistry(executor, techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) - tasks.addAll(loadRegistry(executor, npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) - tasks.addAll(loadRegistry(executor, monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::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(), key(ParticleDefinition::kind))) + 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(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>, executor: ExecutorService): List> { + private fun loadItemDefinitions(files: Map>): List> { val fileMap = mapOf( "item" to ItemDefinition::class.java, "currency" to CurrencyItemDefinition::class.java, @@ -176,7 +194,7 @@ object Registries { val adapter by lazy { Starbound.gson.getAdapter(clazz) } for (listedFile in fileList) { - tasks.add(executor.submit { + tasks.add(Starbound.EXECUTOR.submit { try { val json = objects.read(listedFile.jsonReader()) val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) } @@ -194,65 +212,46 @@ object Registries { return tasks } - private fun loadJsonFunctions(files: Collection, executor: ExecutorService): List> { + private inline fun loadCombined(registry: Registry, files: Collection, 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 -> - executor.submit { + Starbound.EXECUTOR.submit { try { - val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) + // TODO: json patch support + val json = elementAdapter.read(JsonReader(listedFile.reader()).also { it.isLenient = true }) for ((k, v) in json.entrySet()) { try { - val fn = Starbound.gson.fromJson(JsonTreeReader(v), JsonFunction::class.java) - jsonFunctions.add(k, fn, v, listedFile) + val value = adapter.fromJsonTree(v) + transform(value, k) + + registry.add { + registry.add(k, value, v, listedFile) + } } catch (err: Exception) { - LOGGER.error("Loading json function definition $k from file $listedFile", err) + LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err) } } } catch (err: Exception) { - LOGGER.error("Loading json function definition $listedFile", err) + LOGGER.error("Loading ${registry.name} definition $listedFile", err) } } } } - private fun loadJson2Functions(files: Collection, executor: ExecutorService): List> { + private fun loadTerrainSelectors(files: Collection): List> { return files.map { listedFile -> - executor.submit { + Starbound.EXECUTOR.submit { try { - val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) + val factory = TerrainSelectorType.createFactory(Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true })) - for ((k, v) in json.entrySet()) { - try { - val fn = Starbound.gson.fromJson(JsonTreeReader(v), Json2Function::class.java) - json2Functions.add(k, fn, v, listedFile) - } catch (err: Throwable) { - LOGGER.error("Loading json 2function definition $k from file $listedFile", err) - } + terrainSelectors.add { + terrainSelectors.add(factory.name, factory) } } catch (err: Exception) { - LOGGER.error("Loading json 2function definition $listedFile", err) - } - } - } - } - - private fun loadTreasurePools(files: Collection, executor: ExecutorService): List> { - return files.map { listedFile -> - executor.submit { - try { - val json = Starbound.gson.getAdapter(JsonObject::class.java).read(JsonReader(listedFile.reader()).also { it.isLenient = true }) - - for ((k, v) in json.entrySet()) { - try { - val result = Starbound.gson.fromJson(JsonTreeReader(v), TreasurePoolDefinition::class.java) - result.name = k - treasurePools.add(result.name, result, v, listedFile) - } catch (err: Throwable) { - LOGGER.error("Loading treasure pool definition $k from file $listedFile", err) - } - } - } catch (err: Exception) { - LOGGER.error("Loading treasure pool definition $listedFile", err) + LOGGER.error("Loading terrain selector $listedFile", err) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index 20b115a1..fa82c3e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -1,16 +1,8 @@ package ru.dbotthepony.kstarbound -import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject -import com.google.gson.JsonSyntaxException -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 it.unimi.dsi.fastutil.ints.Int2ObjectFunction import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMaps @@ -21,76 +13,12 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMaps import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kommons.gson.consumeNull -import java.lang.reflect.ParameterizedType -import java.util.concurrent.locks.ReentrantLock -import java.util.function.Supplier -import kotlin.collections.contains -import kotlin.collections.set -import kotlin.concurrent.withLock import ru.dbotthepony.kstarbound.util.traverseJsonPath import java.util.concurrent.ConcurrentLinkedQueue - -inline fun Registry.adapter(): TypeAdapterFactory { - return object : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - val subtype = type.type as? ParameterizedType ?: return null - if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != S::class.java) return null - - return when (type.rawType) { - Registry.Entry::class.java -> { - object : TypeAdapter>() { - override fun write(out: JsonWriter, value: Registry.Entry?) { - if (value != null) { - out.value(value.key) - } else { - out.nullValue() - } - } - - override fun read(`in`: JsonReader): Registry.Entry? { - if (`in`.consumeNull()) { - return null - } else if (`in`.peek() == JsonToken.STRING) { - return this@adapter[`in`.nextString()] - } else if (`in`.peek() == JsonToken.NUMBER) { - return this@adapter[`in`.nextInt()] - } else { - throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") - } - } - } - } - - Registry.Ref::class.java -> { - object : TypeAdapter>() { - override fun write(out: JsonWriter, value: Registry.Ref?) { - if (value != null) { - value.key.map(out::value, out::value) - } else { - out.nullValue() - } - } - - override fun read(`in`: JsonReader): Registry.Ref? { - if (`in`.consumeNull()) { - return null - } else if (`in`.peek() == JsonToken.STRING) { - return this@adapter.ref(`in`.nextString()) - } else if (`in`.peek() == JsonToken.NUMBER) { - return this@adapter.ref(`in`.nextInt()) - } else { - throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") - } - } - } - } - - else -> null - } as TypeAdapter? - } - } -} +import java.util.concurrent.locks.ReentrantLock +import java.util.function.Supplier +import kotlin.collections.set +import kotlin.concurrent.withLock class Registry(val name: String) { private val keysInternal = Object2ObjectOpenHashMap() @@ -105,11 +33,13 @@ class Registry(val name: String) { } fun finishLoad() { - var next = backlog.poll() + lock.withLock { + var next = backlog.poll() - while (next != null) { - next.run() - next = backlog.poll() + while (next != null) { + next.run() + next = backlog.poll() + } } } @@ -193,7 +123,7 @@ class Registry(val name: String) { } override fun toString(): String { - return "Registry.Ref[key=$key, bound=${entry != null}, registry=$name]" + return "Registry.Ref[key=$key, bound to value=${entry != null}, registry=$name]" } override val registry: Registry @@ -203,6 +133,14 @@ class Registry(val name: String) { operator fun get(index: String): Entry? = lock.withLock { keysInternal[index] } operator fun get(index: Int): Entry? = lock.withLock { idsInternal[index] } + fun getOrThrow(index: String): Entry { + return get(index) ?: throw NoSuchElementException("No such $name: $index") + } + + fun getOrThrow(index: Int): Entry { + return get(index) ?: throw NoSuchElementException("No such $name: $index") + } + fun ref(index: String): Ref = lock.withLock { keyRefs.computeIfAbsent(index, Object2ObjectFunction { val ref = RefImpl(Either.left(it as String)) @@ -227,29 +165,29 @@ class Registry(val name: String) { operator fun contains(index: Int) = lock.withLock { index in idsInternal } fun validate(): Boolean { - var any = true + var valid = true keyRefs.values.forEach { if (!it.isPresent) { LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") - any = false + valid = false } } idRefs.values.forEach { if (!it.isPresent) { LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") - any = false + valid = false } } - return any + return valid } fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry { lock.withLock { if (key in keysInternal) { - LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") + LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) @@ -275,11 +213,11 @@ class Registry(val name: String) { fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry { lock.withLock { if (key in keysInternal) { - LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") + LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { - LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: ""})") + LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) @@ -307,7 +245,7 @@ class Registry(val name: String) { fun add(key: String, value: T, isBuiltin: Boolean = false): Entry { lock.withLock { if (key in keysInternal) { - LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") + LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) @@ -334,11 +272,11 @@ class Registry(val name: String) { fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry { lock.withLock { if (key in keysInternal) { - LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") + LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { - LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from ; old def originate from ${idsInternal[id]?.file ?: ""})") + LOGGER.warn("Overwriting $name with ID '$id' (new def originate from ; old def originate from ${idsInternal[id]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt new file mode 100644 index 00000000..2dc4bb5a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt @@ -0,0 +1,76 @@ +package ru.dbotthepony.kstarbound + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +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 java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +inline fun Registry.adapter(): TypeAdapterFactory { + return RegistryTypeAdapterFactory(this, S::class) +} + +class RegistryTypeAdapterFactory(private val registry: Registry, private val clazz: KClass) : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val subtype = type.type as? ParameterizedType ?: return null + if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != clazz.java) return null + + if (type.rawType == Registry.Entry::class.java) { + return EntryImpl(gson) as TypeAdapter + } else if (type.rawType == Registry.Ref::class.java) { + return RefImpl(gson) as TypeAdapter + } + + return null + } + + private inner class EntryImpl(gson: Gson) : TypeAdapter>() { + override fun write(out: JsonWriter, value: Registry.Entry?) { + if (value != null) { + out.value(value.key) + } else { + out.nullValue() + } + } + + override fun read(`in`: JsonReader): Registry.Entry? { + if (`in`.consumeNull()) { + return null + } else if (`in`.peek() == JsonToken.STRING) { + return registry[`in`.nextString()] + } else if (`in`.peek() == JsonToken.NUMBER) { + return registry[`in`.nextInt()] + } else { + throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") + } + } + } + + private inner class RefImpl(gson: Gson) : TypeAdapter>() { + override fun write(out: JsonWriter, value: Registry.Ref?) { + if (value != null) { + value.key.map(out::value, out::value) + } else { + out.nullValue() + } + } + + override fun read(`in`: JsonReader): Registry.Ref? { + if (`in`.consumeNull()) { + return null + } else if (`in`.peek() == JsonToken.STRING) { + return registry.ref(`in`.nextString()) + } else if (`in`.peek() == JsonToken.NUMBER) { + return registry.ref(`in`.nextInt()) + } else { + throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 08cb8884..2f9709bf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -4,10 +4,11 @@ import com.github.benmanes.caffeine.cache.Interner import com.google.gson.* import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.gson.AABBTypeAdapter import ru.dbotthepony.kommons.gson.AABBiTypeAdapter -import ru.dbotthepony.kommons.gson.ColorTypeAdapter import ru.dbotthepony.kommons.gson.EitherTypeAdapter import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter import ru.dbotthepony.kommons.gson.NothingAdapter @@ -27,8 +28,14 @@ import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters +import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType +import ru.dbotthepony.kstarbound.defs.world.terrain.BiomePlaceables +import ru.dbotthepony.kstarbound.defs.world.terrain.BiomePlacementDistributionType +import ru.dbotthepony.kstarbound.defs.world.terrain.BiomePlacementItemType +import ru.dbotthepony.kstarbound.defs.world.terrain.TerrainSelectorType import ru.dbotthepony.kstarbound.io.* -import ru.dbotthepony.kstarbound.json.FastutilTypeAdapterFactory +import ru.dbotthepony.kstarbound.json.factory.MapsTypeAdapterFactory import ru.dbotthepony.kstarbound.json.InternedJsonElementAdapter import ru.dbotthepony.kstarbound.json.InternedStringAdapter import ru.dbotthepony.kstarbound.json.LongRangeAdapter @@ -36,9 +43,11 @@ import ru.dbotthepony.kstarbound.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.json.builder.BuilderAdapter import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.JsonImplementationTypeFactory -import ru.dbotthepony.kstarbound.json.factory.ArrayListAdapterFactory +import ru.dbotthepony.kstarbound.json.factory.CollectionAdapterFactory import ru.dbotthepony.kstarbound.json.factory.ImmutableCollectionAdapterFactory 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.math.* import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.util.ItemStack @@ -52,10 +61,10 @@ import java.io.* import java.lang.ref.Cleaner import java.text.DateFormat import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors +import java.util.concurrent.ExecutorService import java.util.concurrent.ForkJoinPool import java.util.concurrent.Future -import java.util.concurrent.SynchronousQueue +import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit @@ -100,13 +109,22 @@ object Starbound : ISBFileLocator { private val ioPoolCounter = AtomicInteger() - val STORAGE_IO_POOL = ThreadPoolExecutor(0, Int.MAX_VALUE, 30L, TimeUnit.SECONDS, SynchronousQueue(), ThreadFactory { + @JvmField + val IO_EXECUTOR: ExecutorService = ThreadPoolExecutor(0, 64, 30L, TimeUnit.SECONDS, LinkedBlockingQueue(), ThreadFactory { val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}") thread.isDaemon = true thread.priority = Thread.MIN_PRIORITY return@ThreadFactory thread }) + @JvmField + val EXECUTOR: ExecutorService = ForkJoinPool.commonPool() + @JvmField + val COROUTINE_EXECUTOR = EXECUTOR.asCoroutineDispatcher() + @JvmField + val COROUTINES = CoroutineScope(COROUTINE_EXECUTOR) + + @JvmField val CLEANER: Cleaner = Cleaner.create { val t = Thread(it, "Starbound Global Cleaner") t.isDaemon = true @@ -118,6 +136,7 @@ object Starbound : ISBFileLocator { // Hrm. // val strings: Interner = Interner.newWeakInterner() // val strings: Interner = Interner { it } + @JvmField val STRINGS: Interner = interner(5) private val LOGGER = LogManager.getLogger() @@ -141,14 +160,14 @@ object Starbound : ISBFileLocator { // Обработчик @JsonImplementation registerTypeAdapterFactory(JsonImplementationTypeFactory) + // списки, наборы, т.п. + registerTypeAdapterFactory(CollectionAdapterFactory) + // ImmutableList, ImmutableSet, ImmutableMap registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS)) // fastutil collections - registerTypeAdapterFactory(FastutilTypeAdapterFactory(STRINGS)) - - // ArrayList - registerTypeAdapterFactory(ArrayListAdapterFactory) + registerTypeAdapterFactory(MapsTypeAdapterFactory(STRINGS)) // все enum'ы без особых настроек registerTypeAdapterFactory(EnumAdapter.Companion) @@ -164,6 +183,8 @@ object Starbound : ISBFileLocator { // KOptional<> registerTypeAdapterFactory(KOptionalTypeAdapter) + registerTypeAdapterFactory(SingletonTypeAdapterFactory) + // Pair<> registerTypeAdapterFactory(PairAdapterFactory) registerTypeAdapterFactory(SBPattern.Companion) @@ -173,7 +194,7 @@ object Starbound : ISBFileLocator { registerTypeAdapter(ColorReplacements.Companion) registerTypeAdapterFactory(BlueprintLearnList.Companion) - registerTypeAdapter(ColorTypeAdapter.nullSafe()) + registerTypeAdapter(RGBAColorTypeAdapter) registerTypeAdapter(Drawable::Adapter) registerTypeAdapter(ObjectOrientation::Adapter) @@ -197,10 +218,12 @@ object Starbound : ISBFileLocator { registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER) registerTypeAdapter(JsonFunction.Companion) + registerTypeAdapter(JsonConfigFunction::Adapter) registerTypeAdapterFactory(Json2Function.Companion) // Общее registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) + registerTypeAdapterFactory(TerrainSelectorType.Companion) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) @@ -223,24 +246,19 @@ object Starbound : ISBFileLocator { registerTypeAdapterFactory(Poly.Companion) - registerTypeAdapterFactory(Registries.tiles.adapter()) - registerTypeAdapterFactory(Registries.tileModifiers.adapter()) - registerTypeAdapterFactory(Registries.liquid.adapter()) - registerTypeAdapterFactory(Registries.items.adapter()) - registerTypeAdapterFactory(Registries.species.adapter()) - registerTypeAdapterFactory(Registries.statusEffects.adapter()) - registerTypeAdapterFactory(Registries.particles.adapter()) - registerTypeAdapterFactory(Registries.questTemplates.adapter()) - registerTypeAdapterFactory(Registries.techs.adapter()) - registerTypeAdapterFactory(Registries.jsonFunctions.adapter()) - registerTypeAdapterFactory(Registries.json2Functions.adapter()) - registerTypeAdapterFactory(Registries.npcTypes.adapter()) - registerTypeAdapterFactory(Registries.projectiles.adapter()) - registerTypeAdapterFactory(Registries.tenants.adapter()) - registerTypeAdapterFactory(Registries.treasurePools.adapter()) - registerTypeAdapterFactory(Registries.monsterSkills.adapter()) - registerTypeAdapterFactory(Registries.monsterTypes.adapter()) - registerTypeAdapterFactory(Registries.worldObjects.adapter()) + registerTypeAdapter(CelestialParameters::Adapter) + + registerTypeAdapterFactory(BiomePlacementDistributionType.DATA_ADAPTER) + registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER) + registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER) + registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER) + registerTypeAdapterFactory(BiomePlaceables.Item.Companion) + + // register companion first, so it has lesser priority than dispatching adapter + registerTypeAdapterFactory(VisitableWorldParametersType.Companion) + registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER) + + Registries.registerAdapters(this) registerTypeAdapter(LongRangeAdapter) @@ -418,7 +436,7 @@ object Starbound : ISBFileLocator { checkMailbox() } - private fun doInitialize(parallel: Boolean) { + private fun doInitialize() { if (!initializing && !initialized) { initializing = true } else { @@ -464,11 +482,10 @@ object Starbound : ISBFileLocator { checkMailbox() val tasks = ArrayList>() - val pool = if (parallel) ForkJoinPool.commonPool() else Executors.newFixedThreadPool(1) - tasks.addAll(Registries.load(ext2files, pool)) - tasks.addAll(RecipeRegistry.load(ext2files, pool)) - tasks.addAll(GlobalDefaults.load(pool)) + tasks.addAll(Registries.load(ext2files)) + tasks.addAll(RecipeRegistry.load(ext2files)) + tasks.addAll(GlobalDefaults.load()) val total = tasks.size.toDouble() @@ -479,9 +496,6 @@ object Starbound : ISBFileLocator { LockSupport.parkNanos(5_000_000L) } - if (!parallel) - pool.shutdown() - Registries.finishLoad() RecipeRegistry.finishLoad() @@ -491,12 +505,12 @@ object Starbound : ISBFileLocator { initialized = true } - fun initializeGame(parallel: Boolean = true) { - mailbox.submit { doInitialize(parallel) } + fun initializeGame(): Future<*> { + return mailbox.submit { doInitialize() } } - fun bootstrapGame() { - mailbox.submit { doBootstrap() } + fun bootstrapGame(): Future<*> { + return mailbox.submit { doBootstrap() } } private fun checkMailbox() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt index 37a5157a..b6a53a93 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/StarboundFileSystem.kt @@ -44,6 +44,7 @@ fun interface ISBFileLocator { * @throws IllegalStateException if file is a directory * @throws FileNotFoundException if file does not exist */ + @Deprecated("This does not reflect json patches") fun jsonReader(path: String) = locate(path).jsonReader() } @@ -157,6 +158,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") fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true } /** diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt index 8416db3b..0d963564 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/ClientConnection.kt @@ -8,28 +8,33 @@ import io.netty.channel.local.LocalAddress import io.netty.channel.local.LocalChannel import io.netty.channel.socket.nio.NioSocketChannel import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import java.net.SocketAddress import java.util.* -// client -> server -class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: UUID) : Connection(ConnectionSide.CLIENT, type, uuid) { +// clientside part of connection +class ClientConnection(val client: StarboundClient, type: ConnectionType) : Connection(ConnectionSide.CLIENT, type) { private fun sendHello() { isLegacy = false //sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION)) sendAndFlush(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION)) } - var connectionID: Int = -1 - override fun inGame() { } + override fun toString(): String { + val channel = if (hasChannel) channel.remoteAddress().toString() else "" + return "ClientConnection[ID=$connectionID channel=$channel]" + } + override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { if (msg is IClientPacket) { try { @@ -44,12 +49,22 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: } } + override fun flush() { + val entries = rpc.write() + + if (entries != null) { + channel.write(ClientContextUpdatePacket(entries, KOptional(), KOptional())) + } + + super.flush() + } + companion object { private val LOGGER = LogManager.getLogger() fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID): ClientConnection { LOGGER.info("Trying to connect to local server at $address with Client UUID $uuid") - val connection = ClientConnection(client, ConnectionType.MEMORY, uuid) + val connection = ClientConnection(client, ConnectionType.MEMORY) Bootstrap() .group(NIO_POOL) @@ -69,7 +84,7 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID): ClientConnection { LOGGER.info("Trying to connect to remote server at $address with Client UUID $uuid") - val connection = ClientConnection(client, ConnectionType.NETWORK, uuid) + val connection = ClientConnection(client, ConnectionType.NETWORK) Bootstrap() .group(NIO_POOL) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt index caa85691..834ac41a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/JoinWorldPacket.kt @@ -11,19 +11,18 @@ import java.io.DataInputStream import java.io.DataOutputStream import java.util.* -data class JoinWorldPacket(val uuid: UUID, val seed: Long, val geometry: WorldGeometry) : IClientPacket { - constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID(), buff.readLong(), WorldGeometry(buff)) - constructor(world: World<*, *>) : this(UUID(0L, 0L), world.seed, world.geometry) +data class JoinWorldPacket(val uuid: UUID, val geometry: WorldGeometry) : IClientPacket { + constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID(), WorldGeometry(buff)) + constructor(world: World<*, *>) : this(UUID(0L, 0L), world.geometry) override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeUUID(uuid) - stream.writeLong(seed) geometry.write(stream) } override fun play(connection: ClientConnection) { connection.client.mailbox.execute { - connection.client.world = ClientWorld(connection.client, seed, geometry) + connection.client.world = ClientWorld(connection.client, geometry) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt index c984068b..290a41f7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/render/TileRenderer.kt @@ -14,11 +14,15 @@ import ru.dbotthepony.kstarbound.client.gl.* import ru.dbotthepony.kstarbound.client.gl.shader.UberShader import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.defs.tile.* +import ru.dbotthepony.kstarbound.util.random.staticRandom32 +import ru.dbotthepony.kstarbound.util.random.staticRandomFloat import ru.dbotthepony.kstarbound.world.api.ITileAccess import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.TileColor import java.time.Duration import java.util.concurrent.Callable +import kotlin.math.absoluteValue +import kotlin.math.roundToInt /** * Хранит в себе программы для отрисовки определённых [TileDefinition] @@ -150,7 +154,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) { var maxs = piece.texturePosition + piece.textureSize if (def.renderParameters.variants != 0 && piece.variantStride != null && piece.image == null) { - val variant = (getter.randomDoubleFor(pos) * def.renderParameters.variants).toInt() + val variant = (staticRandomFloat("TileVariant", pos.x, pos.y) * def.renderParameters.variants).roundToInt() mins += piece.variantStride * variant maxs += piece.variantStride * variant } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index 90b77ce6..9c0fbbf2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -36,9 +36,8 @@ import kotlin.concurrent.withLock class ClientWorld( val client: StarboundClient, - seed: Long, geometry: WorldGeometry, -) : World(seed, geometry) { +) : World(geometry) { private fun determineChunkSize(cells: Int): Int { for (i in 64 downTo 1) { if (cells % i == 0) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt index 2f7d6ed5..6f6b0f3a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/WeightedList.kt @@ -7,6 +7,7 @@ import com.google.gson.TypeAdapterFactory 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 ru.dbotthepony.kommons.util.KOptional import java.lang.reflect.ParameterizedType import java.util.random.RandomGenerator @@ -46,6 +47,39 @@ class WeightedList(val parent: ImmutableList>) { return sample(random.nextDouble(sum)) } + fun sample(amount: Int, random: RandomGenerator): List { + require(amount >= 0) { "Negative amount to choose: $amount" } + if (amount == 0 || isEmpty) return listOf() + if (amount == parent.size) return parent.stream().map { it.second }.toList() + + // Original engine is rather lazy, because it creates a **set** of chosen indices + // and 'while' loops until it contains desired amount of indices + // which means the closer 'amount' is to size of weighted list, the more cpu cycles is burned + + val unseen = ObjectArrayList(parent) + val result = ArrayList() + var currentSum = sum + + while (unseen.isNotEmpty() && result.size < amount) { + val sampled = random.nextDouble(currentSum) + val itr = unseen.iterator() + var sum = 0.0 + + for ((v, e) in itr) { + sum += v + + if (sum >= sampled) { + result.add(e) + itr.remove() + currentSum -= v + break + } + } + } + + return result + } + companion object : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == WeightedList::class.java) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt index a70aa592..4b857ceb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt @@ -9,8 +9,10 @@ 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 it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack import java.lang.reflect.ParameterizedType @@ -27,23 +29,23 @@ data class AssetReference(val path: String?, val fullPath: String?, val value if (type.rawType == AssetReference::class.java) { val param = type.type as? ParameterizedType ?: return null - return object : TypeAdapter>() { - private val cache = ConcurrentHashMap>() - private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter + return object : TypeAdapter>() { + private val cache = Collections.synchronizedMap(Object2ObjectOpenHashMap>()) + 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() - override fun write(out: JsonWriter, value: AssetReference?) { + override fun write(out: JsonWriter, value: AssetReference?) { if (value == null) out.nullValue() else out.value(value.fullPath) } - override fun read(`in`: JsonReader): AssetReference? { - if (`in`.peek() == JsonToken.NULL) { + override fun read(`in`: JsonReader): AssetReference? { + if (`in`.consumeNull()) { return null } else if (`in`.peek() == JsonToken.STRING) { val path = strings.read(`in`)!! @@ -56,21 +58,16 @@ data class AssetReference(val path: String?, val fullPath: String?, val value if (fullPath in missing) return null - val file = Starbound.locate(fullPath) + val json = Starbound.loadJsonAsset(fullPath) - if (!file.exists) { - logger.error("File does not exist: ${file.computeFullPath()}") + if (json == null) { + logger.error("JSON asset does not exist: $fullPath") missing.add(fullPath) return AssetReference(path, fullPath, null, null) } - val reader = file.reader() - val json = jsons.read(JsonReader(reader).also { - it.isLenient = true - }) - val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) { - adapter.read(JsonTreeReader(json)) + adapter.fromJsonTree(json) } if (value == null) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt index c1505a80..e2c2d466 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ColorReplacements.kt @@ -6,6 +6,7 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap +import ru.dbotthepony.kommons.gson.consumeNull class ColorReplacements private constructor(private val mapping: Int2IntOpenHashMap) { constructor(mapping: Map) : this(Int2IntOpenHashMap(mapping)) @@ -33,7 +34,7 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash } override fun read(`in`: JsonReader): ColorReplacements? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null else if (`in`.peek() == JsonToken.STRING) { if (`in`.nextString() != "") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt index 4c0c19ea..28e8e5fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt @@ -9,6 +9,7 @@ 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.json.builder.JsonImplementation @JsonImplementation(ThingDescription::class) @@ -83,6 +84,13 @@ data class ThingDescription( val EMPTY = ThingDescription() } + fun fixDescription(newDescription: String): ThingDescription { + return copy( + shortdescription = if (shortdescription == "...") newDescription else shortdescription, + description = if (description == "...") newDescription else description, + ) + } + class Factory(val interner: Interner = Interner { it }) : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == ThingDescription::class.java) { @@ -114,13 +122,13 @@ data class ThingDescription( } override fun read(`in`: JsonReader): ThingDescription? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null `in`.beginObject() - var shortdescription = "..." - var description = "..." + var shortdescription: String? = null + var description: String? = null val racial = ImmutableMap.Builder() val racialShort = ImmutableMap.Builder() @@ -146,9 +154,18 @@ data class ThingDescription( `in`.endObject() + if (shortdescription == null && description == null) { + shortdescription = "..." + description = "..." + } else if (shortdescription == null) { + shortdescription = description + } else if (description == null) { + description = shortdescription + } + return ThingDescription( - shortdescription = shortdescription, - description = description, + shortdescription = shortdescription!!, + description = description!!, racialDescription = racial.build(), racialShortDescription = racialShort.build() ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt index 12d98913..50bf8a70 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt @@ -36,8 +36,8 @@ data class ItemReference( override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == ItemReference::class.java) { return object : TypeAdapter() { - private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = false), gson, stringInterner) - private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = true), gson, stringInterner) + private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = false), gson, stringInterner) + private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = true), gson, stringInterner) private val references = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, IItemDefinition::class.java)) as TypeAdapter> override fun write(out: JsonWriter, value: ItemReference?) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt index 43ff110d..f82c3d8d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.defs +import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.TypeAdapter @@ -219,10 +220,10 @@ abstract class JsonDriven(val path: String) { for ((k, v) in b.entrySet()) { val existing = a[k] - if (existing == null) { - a[k] = v.deepCopy() - } else if (existing is JsonObject && v is JsonObject) { + if (existing is JsonObject && v is JsonObject) { a[k] = mergeNoCopy(existing, v) + } else if (existing !is JsonObject) { + a[k] = v.deepCopy() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt index 73730d5d..5e2ac2e2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonFunction.kt @@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList import com.google.gson.Gson import com.google.gson.JsonArray +import com.google.gson.JsonElement import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory @@ -11,8 +12,11 @@ import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.math.linearInterpolation +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.json.builder.EnumAdapter enum class JsonFunctionInterpolation { @@ -110,7 +114,7 @@ class JsonFunction( } override fun read(reader: JsonReader): JsonFunction? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null reader.beginArray() @@ -232,7 +236,7 @@ class Json2Function( } override fun read(reader: JsonReader): Json2Function? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null reader.beginArray() @@ -323,3 +327,44 @@ class Json2Function( } } } + +class JsonConfigFunction(val data: ImmutableList>) { + fun evaluate(point: Double): JsonElement? { + return data.lastOrNull { it.first <= point }?.second + } + + fun evaluate(point: Double, adapter: TypeAdapter): KOptional { + val eval = data.lastOrNull { it.first <= point }?.second ?: return KOptional() + return KOptional(adapter.fromJsonTree(eval)) + } + + class Adapter(gson: Gson) : TypeAdapter() { + val parent = gson.getAdapter( + TypeToken.getParameterized( + ImmutableList::class.java, + TypeToken.getParameterized(Pair::class.java, + java.lang.Double::class.java, + JsonElement::class.java + ).type + ) + ) as TypeAdapter>> + + override fun write(out: JsonWriter, value: JsonConfigFunction?) { + if (value == null) + out.nullValue() + else + parent.write(out, value.data) + } + + override fun read(`in`: JsonReader): JsonConfigFunction? { + if (`in`.consumeNull()) + return null + + return JsonConfigFunction( + parent.read(`in`) + .stream() + .sorted { o1, o2 -> o1.first.compareTo(o2.first) } + .collect(ImmutableList.toImmutableList())) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt index d61c7dc5..5b6fec42 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonReference.kt @@ -17,6 +17,8 @@ import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kstarbound.util.AssetPathStack +@Deprecated("Don't use directly, use one of subtypes instead") +@Suppress("DEPRECATION") sealed class JsonReference(val path: String?, val fullPath: String?) { abstract val value: E diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt index 235b25c3..04e123db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt @@ -1,12 +1,14 @@ package ru.dbotthepony.kstarbound.defs +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory data class PerlinNoiseParameters( val type: Type = Type.PERLIN, val seed: Long? = null, - val scale: Int = 512, + val scale: Int = DEFAULT_SCALE, val octaves: Int = 1, val gain: Double = 2.0, val offset: Double = 1.0, @@ -20,9 +22,21 @@ data class PerlinNoiseParameters( require(scale >= 16) { "Too little perlin noise scale" } } - enum class Type { - PERLIN, - BILLOW, - RIDGED_MULTI; + enum class Type(val jsonName: String) : IStringSerializable { + PERLIN("perlin"), + BILLOW("billow"), + RIDGED_MULTI("ridgedmulti"); + + override fun match(name: String): Boolean { + return name.lowercase() == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } + } + + companion object { + const val DEFAULT_SCALE = 512 } } 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 08e609f3..7fd0888b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -302,7 +302,7 @@ class Image private constructor( 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 cache = ConcurrentHashMap>>() + private val configCache = ConcurrentHashMap>>() private val imageCache = ConcurrentHashMap>() private val logger = LogManager.getLogger() @@ -505,7 +505,7 @@ class Image private constructor( val name = path.substringBefore(':').substringAfterLast('/').substringBefore('.') while (true) { - val find = cache.computeIfAbsent("$folder/$name", ::compute).or { cache.computeIfAbsent("$folder/default", ::compute) } + val find = configCache.computeIfAbsent("$folder/$name", ::compute).or { configCache.computeIfAbsent("$folder/default", ::compute) } if (find.isPresent) return find.get() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt index 976cbd6c..45f58c27 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/InventoryIcon.kt @@ -6,6 +6,7 @@ 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 @@ -27,7 +28,7 @@ data class InventoryIcon( } override fun read(`in`: JsonReader): InventoryIcon? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null if (`in`.peek() == JsonToken.STRING) { 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 index 0be07030..32f41bda 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IArmorItemDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/api/IArmorItemDefinition.kt @@ -7,6 +7,7 @@ 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 @@ -53,7 +54,7 @@ interface IArmorItemDefinition : ILeveledItemDefinition, IScriptableItemDefiniti } override fun read(`in`: JsonReader): Frames? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null else if (`in`.peek() == JsonToken.STRING) return Frames(frames.read(`in`), null, null) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt index d0f298eb..f0cf7a8f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/player/BlueprintLearnList.kt @@ -10,6 +10,7 @@ import com.google.gson.stream.JsonReader 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.json.builder.JsonFactory @@ -48,7 +49,7 @@ class BlueprintLearnList private constructor(private val tiers: Int2ObjectArrayM } override fun read(`in`: JsonReader): BlueprintLearnList? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null val tiers = Int2ObjectArrayMap>() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index cd7c44b1..8af719db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -23,8 +23,8 @@ object BuiltinMetaMaterials { damageFactors = Object2DoubleMaps.emptyMap(), damageRecovery = 1.0, maximumEffectTime = 0.0, - health = null, - harvestLevel = null, + totalHealth = Double.MAX_VALUE, + harvestLevel = Int.MAX_VALUE, ) )) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt index a9e73482..70d1d4d9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt @@ -5,10 +5,10 @@ import it.unimi.dsi.fastutil.objects.Object2DoubleMaps import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory -class TileDamageConfig( +data class TileDamageConfig( val damageFactors: Object2DoubleMap = Object2DoubleMaps.emptyMap(), val damageRecovery: Double = 1.0, - val maximumEffectTime: Double = 0.0, - val health: Double? = null, - val harvestLevel: Int? = null + val maximumEffectTime: Double = 1.5, + val totalHealth: Double = 1.0, + val harvestLevel: Int = 1, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AmbientNoisesDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AmbientNoisesDefinition.kt new file mode 100644 index 00000000..dae17227 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AmbientNoisesDefinition.kt @@ -0,0 +1,11 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class AmbientNoisesDefinition(val day: Group? = null, val night: Group? = null) { + @JsonFactory + data class Group(val tracks: ImmutableList) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt new file mode 100644 index 00000000..928a3547 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidWorldsConfig.kt @@ -0,0 +1,46 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise + +@JsonFactory +data class AsteroidWorldsConfig( + val biome: String, + val gravityRange: Vector2d, + val worldSize: Vector2i, + val threatRange: Vector2d, + val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), + val overrideTech: ImmutableSet? = null, + val globalDirectives: ImmutableSet? = null, + val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in asteroid field. + val disableDeathDrops: Boolean = false, + val worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.TOP_AND_BOTTOM, + val asteroidsTop: Int, + val asteroidsBottom: Int, + val ambientLightLevel: RGBAColor, + val blendSize: Double, + + val blockNoise: BlockNoiseConfig? = null, + val terrains: ImmutableList, + val emptyTerrain: Terrain, +) { + @JsonFactory + data class Terrain( + val terrainSelector: String, + val caveSelector: String, + val bgCaveSelector: String, + val oreSelector: String, + val subBlockSelector: String, + ) + + init { + require(worldSize.x > 0) { "Invalid asteroid worlds width: ${worldSize.x}" } + require(worldSize.y > 0) { "Invalid asteroid worlds height: ${worldSize.y}" } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt new file mode 100644 index 00000000..b4c88974 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/AsteroidsWorldParameters.kt @@ -0,0 +1,147 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABBi +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import kotlin.properties.Delegates + +class AsteroidsWorldParameters : VisitableWorldParameters() { + var asteroidTopLevel: Int = 0 + private set + var asteroidBottomLevel: Int = 0 + private set + var blendSize: Double = 0.0 + private set + var asteroidBiome: String by Delegates.notNull() + private set + var ambientLightLevel: RGBAColor by Delegates.notNull() + private set + + init { + airless = true + } + + override val type: VisitableWorldParametersType + get() = VisitableWorldParametersType.ASTEROIDS + + @JsonFactory + data class StoreData( + val asteroidTopLevel: Int = 0, + val asteroidBottomLevel: Int = 0, + val blendSize: Double = 0.0, + val asteroidBiome: String, + val ambientLightLevel: RGBAColor, + ) + + override fun fromJson(data: JsonObject) { + super.fromJson(data) + + val store = Starbound.gson.fromJson(data, StoreData::class.java) + asteroidTopLevel = store.asteroidTopLevel + asteroidBottomLevel = store.asteroidBottomLevel + blendSize = store.blendSize + asteroidBiome = store.asteroidBiome + ambientLightLevel = store.ambientLightLevel + } + + override fun toJson(data: JsonObject, isLegacy: Boolean) { + super.toJson(data, isLegacy) + + val store = Starbound.gson.toJsonTree(StoreData( + asteroidTopLevel = asteroidTopLevel, + asteroidBottomLevel = asteroidBottomLevel, + blendSize = blendSize, + asteroidBiome = asteroidBiome, + ambientLightLevel = ambientLightLevel, + )) as JsonObject + + for ((k, v) in store.entrySet()) + data[k] = v + } + + override fun createLayout(seed: Long): WorldLayout { + val random = random(seed) + val terrain = GlobalDefaults.asteroidWorlds.terrains.random(random) + + val layout = WorldLayout() + layout.worldSize = worldSize + + val asteroidRegion = WorldLayout.RegionParameters( + worldSize.y / 2, + threatLevel, + asteroidBiome, + terrain.terrainSelector, + terrain.caveSelector, + terrain.bgCaveSelector, + terrain.oreSelector, + terrain.oreSelector, + terrain.subBlockSelector, + WorldLayout.RegionLiquids(), + ) + + val emptyRegion = WorldLayout.RegionParameters( + worldSize.y / 2, + threatLevel, + asteroidBiome, + GlobalDefaults.asteroidWorlds.emptyTerrain.terrainSelector, + GlobalDefaults.asteroidWorlds.emptyTerrain.caveSelector, + GlobalDefaults.asteroidWorlds.emptyTerrain.bgCaveSelector, + GlobalDefaults.asteroidWorlds.emptyTerrain.oreSelector, + GlobalDefaults.asteroidWorlds.emptyTerrain.oreSelector, + GlobalDefaults.asteroidWorlds.emptyTerrain.subBlockSelector, + WorldLayout.RegionLiquids(), + ) + + layout.addLayer(random, 0, emptyRegion) + layout.addLayer(random, asteroidBottomLevel, asteroidRegion) + layout.addLayer(random, asteroidTopLevel, emptyRegion) + + layout.regionBlending = blendSize + layout.blockNoise = GlobalDefaults.asteroidWorlds.blockNoise?.build(random) + + layout.playerStartSearchRegions.add( + AABBi( + Vector2i(0, asteroidBottomLevel), + Vector2i(worldSize.x, asteroidTopLevel), + ) + ) + + layout.finalize(RGBAColor.BLACK) + return layout + } + + companion object { + fun generate(seed: Long): AsteroidsWorldParameters { + val random = random(seed) + + val parameters = AsteroidsWorldParameters() + + parameters.threatLevel = random.nextRange(GlobalDefaults.asteroidWorlds.threatRange) + parameters.typeName = "asteroids" + parameters.worldSize = GlobalDefaults.asteroidWorlds.worldSize + parameters.gravity = Vector2d(y = random.nextRange(GlobalDefaults.asteroidWorlds.gravityRange)) + parameters.environmentStatusEffects = GlobalDefaults.asteroidWorlds.environmentStatusEffects + parameters.overrideTech = GlobalDefaults.asteroidWorlds.overrideTech + parameters.globalDirectives = GlobalDefaults.asteroidWorlds.globalDirectives + parameters.beamUpRule = GlobalDefaults.asteroidWorlds.beamUpRule + parameters.disableDeathDrops = GlobalDefaults.asteroidWorlds.disableDeathDrops + parameters.worldEdgeForceRegions = GlobalDefaults.asteroidWorlds.worldEdgeForceRegions + parameters.asteroidTopLevel = GlobalDefaults.asteroidWorlds.asteroidsTop + parameters.asteroidBottomLevel = GlobalDefaults.asteroidWorlds.asteroidsBottom + parameters.blendSize = GlobalDefaults.asteroidWorlds.blendSize + parameters.ambientLightLevel = GlobalDefaults.asteroidWorlds.ambientLightLevel + parameters.asteroidBiome = GlobalDefaults.asteroidWorlds.biome + + return parameters + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt new file mode 100644 index 00000000..a12c8468 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt @@ -0,0 +1,75 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonElement +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.ThingDescription +import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat + +@JsonFactory +class BushVariant( + val bushName: String, + val modName: String, + + // because shapes don't contain absolute paths. + // god damn it + val directory: String, + val shapes: ImmutableList, + + val baseHueShift: Double, + val modHueShift: Double, + + @JsonFlat + val descriptions: ThingDescription, + val ceiling: Boolean, + + val ephemeral: Boolean, + val tileDamageParameters: TileDamageConfig, +) { + @JsonFactory + data class Shape(val image: String, val mods: ImmutableList) + + @JsonFactory + data class DataShape(val base: String, val mods: ImmutableMap> = ImmutableMap.of()) + + @JsonFactory + data class Data( + val name: String, + @JsonFlat + val descriptions: ThingDescription, + val shapes: ImmutableList, + val mods: ImmutableSet = ImmutableSet.of(), + val ceiling: Boolean = false, + val ephemeral: Boolean = true, + val damageTable: AssetReference? = null, + val health: Double = 1.0, + ) + + companion object { + fun create(bushName: String, baseHueShift: Double, modName: String, modHueShift: Double): BushVariant { + return create(Registries.bushVariants.getOrThrow(bushName), baseHueShift, modName, modHueShift) + } + + fun create(data: Registry.Entry, baseHueShift: Double, modName: String, modHueShift: Double): BushVariant { + return BushVariant( + bushName = data.key, + baseHueShift = baseHueShift, + directory = data.file?.computeDirectory() ?: "/", + modHueShift = modHueShift, + ceiling = data.value.ceiling, + descriptions = data.value.descriptions.fixDescription("${data.key} with $modName"), + ephemeral = data.value.ephemeral, + tileDamageParameters = (data.value.damageTable?.value ?: GlobalDefaults.bushDamage).copy(totalHealth = data.value.health), + modName = modName, + shapes = data.value.shapes.stream().map { Shape(it.base, it.mods[modName] ?: ImmutableList.of()) }.collect(ImmutableList.toImmutableList()) + ) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialBaseInformation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialBaseInformation.kt new file mode 100644 index 00000000..f156da35 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialBaseInformation.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.defs.world + +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.io.DataInputStream +import java.io.DataOutputStream + +@JsonFactory +data class CelestialBaseInformation( + val planetOrbitalLevels: Int = 1, + val satelliteOrbitalLevels: Int = 1, + val chunkSize: Int = 1, + val xyCoordRange: Vector2i = Vector2i.ZERO, + val zCoordRange: Vector2i = Vector2i.ZERO, +) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this( + stream.readInt(), + stream.readInt(), + stream.readInt(), + stream.readVector2i(), + stream.readVector2i(), + ) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeInt(planetOrbitalLevels) + stream.writeInt(satelliteOrbitalLevels) + stream.writeInt(chunkSize) + stream.writeStruct2i(xyCoordRange) + stream.writeStruct2i(zCoordRange) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialGenerationInformation.kt similarity index 55% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialGenerationInformation.kt index c8102794..ff6a76f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Celestial.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialGenerationInformation.kt @@ -1,64 +1,11 @@ -package ru.dbotthepony.kstarbound.defs +package ru.dbotthepony.kstarbound.defs.world import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.gson.JsonObject -import it.unimi.dsi.fastutil.ints.Int2ObjectMap -import ru.dbotthepony.kommons.io.readVector2i -import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.vector.Vector2i -import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise -import ru.dbotthepony.kstarbound.world.UniversePos -import java.io.DataInputStream -import java.io.DataOutputStream - -@JsonFactory -data class CelestialNames( - val systemNames: WeightedList = WeightedList(), - val systemPrefixNames: WeightedList = WeightedList(), - val systemSuffixNames: WeightedList = WeightedList(), - val planetarySuffixes: ImmutableList = ImmutableList.of(), - val satelliteSuffixes: ImmutableList = ImmutableList.of(), -) - -@JsonFactory -data class CelestialBaseInformation( - val planetOrbitalLevels: Int = 1, - val satelliteOrbitalLevels: Int = 1, - val chunkSize: Int = 1, - val xyCoordRange: Vector2i = Vector2i.ZERO, - val zCoordRange: Vector2i = Vector2i.ZERO, -) { - constructor(stream: DataInputStream, isLegacy: Boolean) : this( - stream.readInt(), - stream.readInt(), - stream.readInt(), - stream.readVector2i(), - stream.readVector2i(), - ) - - fun write(stream: DataOutputStream, isLegacy: Boolean) { - stream.writeInt(planetOrbitalLevels) - stream.writeInt(satelliteOrbitalLevels) - stream.writeInt(chunkSize) - stream.writeStruct2i(xyCoordRange) - stream.writeStruct2i(zCoordRange) - } -} - -@JsonFactory -data class CelestialOrbitRegion( - val regionName: String, - val orbitRange: Vector2i, - val bodyProbability: Double, - val planetaryTypes: WeightedList = WeightedList(), - val satelliteTypes: WeightedList = WeightedList(), -) - -@JsonFactory -data class CelestialPlanet(val parameters: CelestialParameters, val satellites: Int2ObjectMap) @JsonFactory data class CelestialGenerationInformation( @@ -127,7 +74,4 @@ data class CelestialGenerationInformation( it.value.typeName = it.key } } -} - -@JsonFactory -data class CelestialParameters(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialNames.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialNames.kt new file mode 100644 index 00000000..39957763 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialNames.kt @@ -0,0 +1,14 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class CelestialNames( + val systemNames: WeightedList = WeightedList(), + val systemPrefixNames: WeightedList = WeightedList(), + val systemSuffixNames: WeightedList = WeightedList(), + val planetarySuffixes: ImmutableList = ImmutableList.of(), + val satelliteSuffixes: ImmutableList = ImmutableList.of(), +) \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialOrbitRegion.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialOrbitRegion.kt new file mode 100644 index 00000000..d0ad6a01 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialOrbitRegion.kt @@ -0,0 +1,14 @@ +package ru.dbotthepony.kstarbound.defs.world + +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class CelestialOrbitRegion( + val regionName: String, + val orbitRange: Vector2i, + val bodyProbability: Double, + val planetaryTypes: WeightedList = WeightedList(), + val satelliteTypes: WeightedList = WeightedList(), +) \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt new file mode 100644 index 00000000..269e0ff5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialParameters.kt @@ -0,0 +1,82 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSyntaxException +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.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.UniversePos + +class CelestialParameters private constructor(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject, marker: Nothing?) { + constructor(coordinate: UniversePos, seed: Long, name: String, parameters: JsonObject) : this(coordinate, seed, name, parameters, null) { + val worldType = parameters["worldType"]?.asString + + if (worldType != null) { + visitableParameters = when (worldType.lowercase()) { + "terrestrial" -> { + val random = random(seed) + val worldSize = parameters["worldSize"].asString + val type = parameters["terrestrialType"] + + if (type is JsonArray) { + TerrestrialWorldParameters.generate(type.random(random).asString, worldSize, random) + } else if (type is JsonPrimitive) { + TerrestrialWorldParameters.generate(type.asString, worldSize, random) + } else { + throw JsonSyntaxException("Invalid terrestrialType: $type") + } + } + + "asteroids" -> { + AsteroidsWorldParameters.generate(seed) + } + + "floatingdungeon" -> { + FloatingDungeonWorldParameters.generate(parameters["dungeonWorld"].asString) + } + + else -> null + } + } + } + + constructor(coordinate: UniversePos, seed: Long, name: String, parameters: JsonObject, visitableParameters: VisitableWorldParameters?) : this(coordinate, seed, name, parameters, null) { + this.visitableParameters = visitableParameters + } + + var visitableParameters: VisitableWorldParameters? = null + private set + + @JsonFactory + data class StoreData(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject, val visitableParameters: VisitableWorldParameters? = null) + + class Adapter(gson: Gson) : TypeAdapter() { + private val data = gson.getAdapter(StoreData::class.java) + + override fun write(out: JsonWriter, value: CelestialParameters?) { + if (value == null) + out.nullValue() + else { + data.write(out, StoreData(value.coordinate, value.seed, value.name, value.parameters, value.visitableParameters)) + } + } + + override fun read(`in`: JsonReader): CelestialParameters? { + if (`in`.consumeNull()) + return null + + val read = data.read(`in`) + return CelestialParameters(read.coordinate, read.seed, read.name, read.parameters, read.visitableParameters) + } + } +} + diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt new file mode 100644 index 00000000..75f654c3 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/CelestialPlanet.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.defs.world + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class CelestialPlanet(val parameters: CelestialParameters, val satellites: Int2ObjectMap) \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt new file mode 100644 index 00000000..7ce19a3b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/DungeonWorldsConfig.kt @@ -0,0 +1,44 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableSet +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class DungeonWorldsConfig( + val threatLevel: Double, + val worldSize: Vector2i, + val gravity: Either, + val airless: Boolean = false, + val environmentStatusEffects: ImmutableSet = ImmutableSet.of(), + val overrideTech: ImmutableSet? = null, + val globalDirectives: ImmutableSet? = null, + val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in floating dungeon. + val disableDeathDrops: Boolean = false, + val worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.TOP, + val weatherPool: WeightedList? = null, + val dungeonBaseHeight: Int, + val dungeonSurfaceHeight: Int = dungeonBaseHeight, + val dungeonUndergroundLevel: Int = 0, + val primaryDungeon: String, + val biome: String? = null, + val ambientLightLevel: RGBAColor, + + val musicTrack: AssetPath? = null, + val dayMusicTrack: AssetPath? = null, + val nightMusicTrack: AssetPath? = null, + + val ambientNoises: AssetPath? = null, + val dayAmbientNoises: AssetPath? = null, + val nightAmbientNoises: AssetPath? = null, +) { + init { + require(worldSize.x > 0) { "Invalid asteroid worlds width: ${worldSize.x}" } + require(worldSize.y > 0) { "Invalid asteroid worlds height: ${worldSize.y}" } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt new file mode 100644 index 00000000..d72778ad --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/FloatingDungeonWorldParameters.kt @@ -0,0 +1,103 @@ +package ru.dbotthepony.kstarbound.defs.world + +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.util.random.random +import kotlin.properties.Delegates + +class FloatingDungeonWorldParameters : VisitableWorldParameters() { + var dungeonBaseHeight: Int by Delegates.notNull() + private set + var dungeonSurfaceHeight: Int by Delegates.notNull() + private set + var dungeonUndergroundLevel: Int by Delegates.notNull() + private set + var primaryDungeon: String by Delegates.notNull() + private set + var ambientLightLevel: RGBAColor by Delegates.notNull() + private set + var biome: String? = null + private set + var dayMusicTrack: String? = null + private set + var nightMusicTrack: String? = null + private set + var dayAmbientNoises: String? = null + private set + var nightAmbientNoises: String? = null + private set + + override val type: VisitableWorldParametersType + get() = VisitableWorldParametersType.FLOATING_DUNGEON + + override fun createLayout(seed: Long): WorldLayout { + val random = random(seed) + + val layout = WorldLayout() + layout.worldSize = worldSize + + layout.addLayer(random, 0, WorldLayout.RegionParameters( + dungeonSurfaceHeight, threatLevel, biome, + null, null, null, null, null, null, + WorldLayout.RegionLiquids() + )) + + val color = if (biome != null) { + // TODO: Original game engine queries biome database but forgets to finalize world layout if biome is present + // TODO: Is that intentional? + // (: god damn it. + + Registries.biomes[biome!!]?.value?.skyColoring(random)?.mainColor ?: RGBAColor.BLACK + } else { + RGBAColor.BLACK + } + + layout.finalize(color) + return layout + } + + companion object { + fun generate(typeName: String): FloatingDungeonWorldParameters { + val config = GlobalDefaults.dungeonWorlds[typeName] ?: throw NoSuchElementException("Unknown dungeon world type $typeName!") + val parameters = FloatingDungeonWorldParameters() + + parameters.threatLevel = config.threatLevel + parameters.gravity = config.gravity.map({ Vector2d(y = it) }, { it }) + parameters.airless = config.airless + parameters.environmentStatusEffects = config.environmentStatusEffects + + parameters.overrideTech = config.overrideTech + parameters.globalDirectives = config.globalDirectives + parameters.beamUpRule = config.beamUpRule + parameters.disableDeathDrops = config.disableDeathDrops + parameters.worldEdgeForceRegions = config.worldEdgeForceRegions + parameters.weatherPool = config.weatherPool + parameters.dungeonBaseHeight = config.dungeonBaseHeight + parameters.dungeonSurfaceHeight = config.dungeonSurfaceHeight + parameters.dungeonUndergroundLevel = config.dungeonUndergroundLevel + parameters.primaryDungeon = config.primaryDungeon + parameters.biome = config.biome + parameters.ambientLightLevel = config.ambientLightLevel + + if (config.musicTrack != null) { + parameters.dayMusicTrack = config.musicTrack.fullPath + parameters.nightMusicTrack = config.musicTrack.fullPath + } else { + parameters.dayMusicTrack = config.dayMusicTrack?.fullPath + parameters.nightMusicTrack = config.nightMusicTrack?.fullPath + } + + if (config.ambientNoises != null) { + parameters.dayAmbientNoises = config.ambientNoises.fullPath + parameters.nightAmbientNoises = config.ambientNoises.fullPath + } else { + parameters.dayAmbientNoises = config.dayAmbientNoises?.fullPath + parameters.nightAmbientNoises = config.nightAmbientNoises?.fullPath + } + + return parameters + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt new file mode 100644 index 00000000..864f4bc7 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt @@ -0,0 +1,61 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonElement +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.ThingDescription +import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat + +@JsonFactory +data class GrassVariant( + val name: String, + val directory: String, + val images: ImmutableSet = ImmutableSet.of(), + val hueShift: Double, + @JsonFlat + val descriptions: ThingDescription, + val ceiling: Boolean, + val ephemeral: Boolean, + val tileDamageParameters: TileDamageConfig, +) { + @JsonFactory + data class Data( + val name: String, + @JsonFlat + val descriptions: ThingDescription, + val images: ImmutableSet, + val ceiling: Boolean = false, + val ephemeral: Boolean = true, + val description: String = name, + val damageTable: AssetReference? = null, + val health: Double = 1.0 + ) { + init { + require(damageTable == null || damageTable.value != null) { "damageTable must be either absent or point at existing json" } + } + } + + companion object { + fun create(name: String, hueShift: Double): GrassVariant { + return create(Registries.grassVariants.getOrThrow(name), hueShift) + } + + fun create(data: Registry.Entry, hueShift: Double): GrassVariant { + return GrassVariant( + name = data.value.name, + directory = data.file?.computeDirectory() ?: "/", + images = data.value.images, + ceiling = data.value.ceiling, + ephemeral = data.value.ephemeral, + hueShift = hueShift, + descriptions = data.value.descriptions.fixDescription(data.value.name), + tileDamageParameters = (data.value.damageTable?.value ?: GlobalDefaults.grassDamage).copy(totalHealth = data.value.health) + ) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt new file mode 100644 index 00000000..550c8a1e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt @@ -0,0 +1,161 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.image.Image +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.RenderDirectives +import ru.dbotthepony.kstarbound.util.random.nextRange +import java.util.random.RandomGenerator + +@JsonFactory +class Parallax( + // ah yes storing useless data once again + val seed: Long = 0L, + val verticalOrigin: Double, + val hueShift: Double, + val imageDirectory: String, + val parallaxTreeVariant: TreeVariant?, + val layers: ImmutableList = ImmutableList.of(), +) { + @JsonFactory + data class Data( + val verticalOrigin: Double = 0.0, + val layers: ImmutableList, + ) { + fun create(random: RandomGenerator, verticalOrigin: Double, hueShift: Double, treeVariant: TreeVariant?): Parallax { + return Parallax( + seed = random.nextLong(), + verticalOrigin = verticalOrigin + this.verticalOrigin, + hueShift = hueShift, + imageDirectory = "/parallax/images/", + parallaxTreeVariant = treeVariant, + layers = layers.stream() + .filter { it.frequency >= random.nextDouble() } + .map { it.create(random, treeVariant, verticalOrigin + this.verticalOrigin, hueShift) } + .filterNotNull() + .collect(ImmutableList.toImmutableList()) + ) + } + } + + fun fadeToSkyColor(color: RGBAColor) { + layers.forEach { it.fadeToSkyColor(color) } + } + + @JsonFactory + data class DataLayer( + val kind: String, + val frequency: Double = 1.0, + val baseCount: Int = 1, + val modCount: Int = 0, + val parallax: Either, + val repeatY: Boolean = true, + val repeatX: Boolean = true, + val tileLimitTop: Double? = null, + val tileLimitBottom: Double? = null, + val offset: Vector2d = Vector2d.ZERO, // shift from bottom left to horizon level in the image + val noRandomOffset: Boolean = false, + val timeOfDayCorrelation: String = "", + val minSpeed: Double = 0.0, + val maxSpeed: Double = 0.0, + val unlit: Boolean = false, + val lightMapped: Boolean = false, + val directives: String = "", + val fadePercent: Double = 0.0, + val nohueshift: Boolean = false, + ) { + init { + require(baseCount >= 1) { "non positive baseCount: $baseCount" } + } + + fun create(random: RandomGenerator, treeVariant: TreeVariant?, verticalOrigin: Double, hueShift: Double): Layer? { + val isFoliage = kind.startsWith("foliage/") + val isStem = kind.startsWith("stem/") + + var texPath = "/parallax/images/$kind/" + + if (isFoliage) { + if (treeVariant == null) return null + if (!treeVariant.foliageSettings.parallaxFoliage) return null + texPath = "${treeVariant.foliageDirectory}/parallax/${kind.replace("foliage/", "")}/" + } else if (isStem) { + if (treeVariant == null) return null + texPath = "${treeVariant.stemDirectory}/parallax/${kind.replace("stem/", "")}/" + } + + val textures = ArrayList() + val base = if (baseCount <= 1) 1 else random.nextInt(baseCount - 1) + 1 + textures.add("${texPath}base/$base.png") + + val modCount = if (modCount == 0) 0 else random.nextInt(modCount) + 1 + if (modCount != 0) + textures.add("${texPath}mod/${modCount + 1}.png") + + var directives = directives + + if (isFoliage) + directives += "?hueshift=${treeVariant!!.foliageHueShift}" + else if (isStem) + directives += "?hueshift=${treeVariant!!.stemHueShift}" + else if (!nohueshift) + directives += "?hueshift=$hueShift" + + var offset = offset + + if (!noRandomOffset) { + val width = Image.get(textures.first())?.width ?: 0 + + if (width > 0) { + offset += Vector2d(x = random.nextInt(width).toDouble()) + } + } + + return Layer( + parallaxValue = parallax.map({ Vector2d(it, it) }, { it }), + repeat = repeatX to repeatY, + tileLimitTop = tileLimitTop, + tileLimitBottom = tileLimitBottom, + verticalOrigin = verticalOrigin, + zLevel = parallax.map({ it * 2.0 }, { it.x + it.y }), + parallaxOffset = offset, + timeOfDayCorrelation = timeOfDayCorrelation, + speed = random.nextRange(Vector2d(minSpeed, maxSpeed)), + unlit = unlit, + lightMapped = lightMapped, + fadePercent = fadePercent, + alpha = 1.0, + directives = RenderDirectives(directives), + textures = ImmutableList.copyOf(textures), + ) + } + } + + @JsonFactory + data class Layer( + var directives: RenderDirectives, + val textures: ImmutableList, + val alpha: Double, + val parallaxValue: Vector2d, + val repeat: Pair, + val tileLimitTop: Double? = null, + val tileLimitBottom: Double? = null, + val verticalOrigin: Double, + val zLevel: Double, + val parallaxOffset: Vector2d, + val timeOfDayCorrelation: String, + val speed: Double, + val unlit: Boolean, + val lightMapped: Boolean, + val fadePercent: Double, + ) { + fun fadeToSkyColor(color: RGBAColor) { + if (fadePercent > 0.0) { + directives = directives.add("fade", color.toHexStringRGB() + "=$fadePercent") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt new file mode 100644 index 00000000..641ac61f --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt @@ -0,0 +1,165 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.io.readColor +import ru.dbotthepony.kstarbound.io.writeColor +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.random +import ru.dbotthepony.kstarbound.world.Universe +import ru.dbotthepony.kstarbound.world.UniversePos +import java.io.DataInputStream +import java.io.DataOutputStream + +enum class SkyType { + BARREN, + ATMOSPHERIC, + ATMOSPHERELESS, + ORBITAL, + WARP, + SPACE +} + +enum class FlyingType { + NONE, + DISEMBARKING, + WARP, + ARRIVING +} + +enum class WarpPhase(val stupidassbitch: Int) { + SLOWING_DOWN(-1), + MAINTAIN(0), + SPEEDING_UP(1) +} + +enum class SkyOrbiterType(val sname: String) : IStringSerializable { + SUN("sun"), + MOON("moon"), + HORIZON_CLOUD("horizoncloud"), + SPACE_DEBRIS("scapedebris"); + + override fun match(name: String): Boolean { + return name.lowercase() == sname + } + + override fun write(out: JsonWriter) { + out.value(sname) + } +} + +@JsonFactory +data class SkyOrbiter(val type: SkyOrbiterType, val scale: Double, val angle: Double, val image: String, val position: Vector2d) + +@JsonFactory +data class SkyColoring( + val mainColor: RGBAColor = RGBAColor.BLACK, + + val morningColors: Pair = RGBAColor.BLACK to RGBAColor.BLACK, + val dayColors: Pair = RGBAColor.BLACK to RGBAColor.BLACK, + val eveningColors: Pair = RGBAColor.BLACK to RGBAColor.BLACK, + val nightColors: Pair = RGBAColor.BLACK to RGBAColor.BLACK, + + val morningLightColor: RGBAColor = RGBAColor.BLACK, + val dayLightColor: RGBAColor = RGBAColor.BLACK, + val eveningLightColor: RGBAColor = RGBAColor.BLACK, + val nightLightColor: RGBAColor = RGBAColor.BLACK, +) { + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeColor(mainColor) + + stream.writeColor(morningColors.first) + stream.writeColor(morningColors.second) + stream.writeColor(dayColors.first) + stream.writeColor(dayColors.second) + stream.writeColor(eveningColors.first) + stream.writeColor(eveningColors.second) + stream.writeColor(nightColors.first) + stream.writeColor(nightColors.second) + + stream.writeColor(morningLightColor) + stream.writeColor(dayLightColor) + stream.writeColor(eveningLightColor) + stream.writeColor(nightLightColor) + } + + companion object { + val BLACK = SkyColoring() + + fun read(stream: DataInputStream, isLegacy: Boolean): SkyColoring { + val mainColor = stream.readColor() + + val morningColors = stream.readColor() to stream.readColor() + val dayColors = stream.readColor() to stream.readColor() + val eveningColors = stream.readColor() to stream.readColor() + val nightColors = stream.readColor() to stream.readColor() + + val morningLightColor = stream.readColor() + val dayLightColor = stream.readColor() + val eveningLightColor = stream.readColor() + val nightLightColor = stream.readColor() + + return SkyColoring( + mainColor = mainColor, + morningColors = morningColors, + dayColors = dayColors, + eveningColors = eveningColors, + nightColors = nightColors, + morningLightColor = morningLightColor, + dayLightColor = dayLightColor, + eveningLightColor = eveningLightColor, + nightLightColor = nightLightColor, + ) + } + } +} + +data class SkyWorldHorizon(val center: Vector2d, val scale: Double, val rotation: Double, val layers: List>) + +class SkyParameters() { + var skyType = SkyType.BARREN + var seed = 0L + var dayLength: Double? = null + var horizonClouds = false + var skyColoring: Either = Either.right(RGBAColor.BLACK) + var spaceLevel: Double? = null + var surfaceLevel: Double? = null + var nearbyPlanet: Pair>, Vector2d>? = null + + companion object { + suspend fun create(coordinate: UniversePos, universe: Universe): SkyParameters { + if (coordinate.isSystem) + throw IllegalArgumentException("$coordinate is system location") + + val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!") + + val random = random(params.seed) + val selfPos = params.coordinate + + val sky = SkyParameters() + + if (selfPos.isSatellite) { + val planet = universe.parameters(selfPos.parent()) + + if (planet != null) { + val pos = Vector2d(random.nextDouble(), random.nextDouble()) + } + } + + for (satellitePos in universe.children(selfPos.planet())) { + if (satellitePos != selfPos) { + val satellite = universe.parameters(satellitePos) + + if (satellite != null) { + val pos = Vector2d(random.nextDouble(), random.nextDouble()) + } + } + } + + return sky + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt new file mode 100644 index 00000000..5ba95caf --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldParameters.kt @@ -0,0 +1,484 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonArray +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.reflect.TypeToken +import it.unimi.dsi.fastutil.ints.IntArrayList +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.gson.getObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.defs.JsonDriven +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.pairAdapter +import ru.dbotthepony.kstarbound.json.stream +import ru.dbotthepony.kstarbound.util.binnedChoice +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.PerlinNoise +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator +import kotlin.properties.Delegates + +class TerrestrialWorldParameters : VisitableWorldParameters() { + @JsonFactory + data class Generic( + // TODO: while engine supports arbitrary gravity direction, + // TODO: json files don't have the same support level + val gravityRange: Vector2d, + val dayLengthRange: Vector2d, + val threatRange: Vector2d = Vector2d(1.0, 1.0), + val size: Vector2i, + val overrideTech: ImmutableSet? = null, + val globalDirectives: ImmutableSet? = null, + val disableDeathDrops: Boolean = false, + val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, + val worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.TOP, + + val blockNoise: BlockNoiseConfig? = null, + val blendNoise: PerlinNoiseParameters? = null, + val blendSize: Double, + ) + + @JsonFactory + data class Region( + val biome: String, + + val blockSelector: String, + val fgCaveSelector: String, + val bgCaveSelector: String, + val fgOreSelector: String, + val bgOreSelector: String, + val subBlockSelector: String, + + val caveLiquid: Either?, + val caveLiquidSeedDensity: Double, + val oceanLiquid: Either?, + val oceanLiquidLevel: Int, + + val encloseLiquids: Boolean, + val fillMicrodungeons: Boolean, + ) + + @JsonFactory + data class Layer( + val layerMinHeight: Int, + val layerBaseHeight: Int, + + val dungeons: ImmutableSet, + val dungeonXVariance: Int, + + val primaryRegion: Region, + val primarySubRegion: Region, + + val secondaryRegions: ImmutableList, + val secondarySubRegions: ImmutableList, + + val secondaryRegionSizeRange: Vector2d, + val subRegionSizeRange: Vector2d, + ) + + override fun fromJson(data: JsonObject) { + super.fromJson(data) + + val read = Starbound.gson.fromJson(data, StoreData::class.java) + + primaryBiome = read.primaryBiome + primarySurfaceLiquid = read.primarySurfaceLiquid + sizeName = read.sizeName + hueShift = read.hueShift + skyColoring = read.skyColoring + dayLength = read.dayLength + blockNoiseConfig = read.blockNoiseConfig + blendNoiseConfig = read.blendNoiseConfig + blendSize = read.blendSize + spaceLayer = read.spaceLayer + atmosphereLayer = read.atmosphereLayer + surfaceLayer = read.surfaceLayer + subsurfaceLayer = read.subsurfaceLayer + undergroundLayers = read.undergroundLayers + coreLayer = read.coreLayer + } + + override fun toJson(data: JsonObject, isLegacy: Boolean) { + super.toJson(data, isLegacy) + + val primarySurfaceLiquid: Either? + + // original engine operate on liquids solely with IDs + // and we also need to network this json to legacy clients. + // what a shame :JC:. + if (this.primarySurfaceLiquid == null) { + primarySurfaceLiquid = if (isLegacy) Either.left(0) else null + } else if (isLegacy) { + primarySurfaceLiquid = this.primarySurfaceLiquid!!.map({ it }, { Registries.liquid.get(it)!!.id })?.let { Either.left(it) } + } else { + primarySurfaceLiquid = this.primarySurfaceLiquid + } + + val store = StoreData( + primaryBiome = primaryBiome, + primarySurfaceLiquid = primarySurfaceLiquid, + sizeName = sizeName, + hueShift = hueShift, + skyColoring = skyColoring, + dayLength = dayLength, + blockNoiseConfig = blockNoiseConfig, + blendNoiseConfig = blendNoiseConfig, + blendSize = blendSize, + spaceLayer = spaceLayer, + atmosphereLayer = atmosphereLayer, + surfaceLayer = surfaceLayer, + subsurfaceLayer = subsurfaceLayer, + undergroundLayers = undergroundLayers, + coreLayer = coreLayer, + ) + + for ((k, v) in (Starbound.gson.toJsonTree(store) as JsonObject).entrySet()) { + data[k] = v + } + } + + @JsonFactory + data class StoreData( + val primaryBiome: String, + val primarySurfaceLiquid: Either?, + val sizeName: String, + val hueShift: Double, + val skyColoring: SkyColoring, + val dayLength: Double, + val blockNoiseConfig: BlockNoiseConfig? = null, + val blendNoiseConfig: PerlinNoiseParameters? = null, + val blendSize: Double, + val spaceLayer: Layer, + val atmosphereLayer: Layer, + val surfaceLayer: Layer, + val subsurfaceLayer: Layer, + val undergroundLayers: List, + val coreLayer: Layer, + ) + + var primaryBiome: String by Delegates.notNull() + private set + var primarySurfaceLiquid: Either? = null + private set + var sizeName: String by Delegates.notNull() + private set + var hueShift: Double by Delegates.notNull() + private set + var skyColoring: SkyColoring by Delegates.notNull() + private set + var dayLength: Double by Delegates.notNull() + private set + var blockNoiseConfig: BlockNoiseConfig? = null + private set + var blendNoiseConfig: PerlinNoiseParameters? = null + private set + var blendSize: Double by Delegates.notNull() + private set + + var spaceLayer: Layer by Delegates.notNull() + private set + var atmosphereLayer: Layer by Delegates.notNull() + private set + var surfaceLayer: Layer by Delegates.notNull() + private set + var subsurfaceLayer: Layer by Delegates.notNull() + private set + var undergroundLayers: List by Delegates.notNull() + private set + var coreLayer: Layer by Delegates.notNull() + private set + + override val type: VisitableWorldParametersType + get() = VisitableWorldParametersType.TERRESTRIAL + + // why + override fun createLayout(seed: Long): WorldLayout { + val layout = WorldLayout() + layout.worldSize = worldSize + + val random = random(seed) + + fun addLayer(layer: Layer) { + fun bake(region: Region): WorldLayout.RegionParameters { + return WorldLayout.RegionParameters( + layer.layerBaseHeight, + threatLevel, + region.biome, + region.blockSelector, + region.fgCaveSelector, + region.bgCaveSelector, + region.fgOreSelector, + region.bgOreSelector, + region.subBlockSelector, + WorldLayout.RegionLiquids( + region.caveLiquid, + region.caveLiquidSeedDensity, + region.oceanLiquid, + region.oceanLiquidLevel, + region.encloseLiquids, + region.fillMicrodungeons, + ) + ) + } + + val primaryRegionParams = bake(layer.primaryRegion) + val primarySubRegionParams = bake(layer.primarySubRegion) + val secondary = layer.secondaryRegions.map { bake(it) } + val secondarySubRegions = layer.secondarySubRegions.map { bake(it) } + + layout.addLayer( + random, layer.layerMinHeight, layer.layerBaseHeight, + primaryBiome, + primaryRegionParams, primarySubRegionParams, + secondary, secondarySubRegions, + layer.secondaryRegionSizeRange, layer.subRegionSizeRange) + } + + addLayer(coreLayer) + undergroundLayers.asReversed().forEach { addLayer(it) } + addLayer(subsurfaceLayer) + addLayer(surfaceLayer) + addLayer(atmosphereLayer) + addLayer(spaceLayer) + + layout.regionBlending = blendSize + layout.blockNoise = blockNoiseConfig?.build(random) + layout.blendNoise = blendNoiseConfig?.let { AbstractPerlinNoise.of(it).also { it.init(seed) } } + + layout.finalize(skyColoring.mainColor) + + return layout + } + + companion object { + private val biomePairs by lazy { Starbound.gson.pairAdapter() } + private val vectors2d by lazy { Starbound.gson.getAdapter(Vector2d::class.java) } + private val vectors2i by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } + private val dungeonPools by lazy { Starbound.gson.getAdapter(TypeToken.getParameterized(WeightedList::class.java, String::class.java)) as TypeAdapter> } + + fun generate(typeName: String, sizeName: String, seed: Long): TerrestrialWorldParameters { + return generate(typeName, sizeName, random(seed)) + } + + fun generate(typeName: String, sizeName: String, random: RandomGenerator): TerrestrialWorldParameters { + val config = GlobalDefaults.terrestrialWorlds.planetDefaults.deepCopy() + JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetSizes[sizeName] ?: throw NoSuchElementException("Unknown world size name $sizeName")) + JsonDriven.mergeNoCopy(config, GlobalDefaults.terrestrialWorlds.planetTypes[typeName] ?: throw NoSuchElementException("Unknown world type name $typeName")) + + val params = Starbound.gson.fromJson(config, Generic::class.java) + + val threadLevel = random.nextRange(params.threatRange) + val layers = config.getObject("layers") + + fun readRegion(regionConfig: JsonObject, layerBaseHeight: Int): Region { + val biome = regionConfig["biome"] + .asJsonArray + .stream() + .map { biomePairs.fromJsonTree(it) } + .binnedChoice(threadLevel) + .map { it.stream().map { it.asString }.toList() } + .orElse(listOf()) + .random(random) + + val blockSelector = regionConfig.getArray("blockSelector").random(random).asString + val fgCaveSelector = regionConfig.getArray("fgCaveSelector").random(random).asString + val bgCaveSelector = regionConfig.getArray("bgCaveSelector").random(random).asString + val fgOreSelector = regionConfig.getArray("fgOreSelector").random(random).asString + val bgOreSelector = regionConfig.getArray("bgOreSelector").random(random).asString + val subBlockSelector = regionConfig.getArray("subBlockSelector").random(random).asString + + // original engine here translates liquid name into liquid id + // we, however, do not, since we prefer to operate on registry names and not registry ids + val caveLiquid: String? + val caveLiquidSeedDensity: Double + val oceanLiquid: String? + val oceanLiquidLevel: Int + + // "can be empty for no liquid" + // yeah thanks fucking fuck + // I hope whoever designed original loading code will have + // their software blown up by C++'s "feature" to implicitly construct classes/structs/values + // out of thin air + if ("caveLiquid" in regionConfig) { + caveLiquid = regionConfig.getArray("caveLiquid").random(random) { JsonPrimitive("") }.asString.let { if (it.isBlank()) null else it } + val range = Starbound.gson.fromJson(regionConfig["caveLiquidSeedDensityRange"], Vector2d::class.java) + caveLiquidSeedDensity = if (range.x == range.y) range.x else random.nextDouble(range.x, range.y) + } else { + caveLiquid = null + caveLiquidSeedDensity = 0.0 + } + + if ("oceanLiquid" in regionConfig) { + oceanLiquid = regionConfig.getArray("oceanLiquid").random(random) { JsonPrimitive("") }.asString.let { if (it.isBlank()) null else it } + oceanLiquidLevel = regionConfig.get("oceanLevelOffset", 0) + layerBaseHeight + } else { + oceanLiquid = null + oceanLiquidLevel = 0 + } + + val encloseLiquids = regionConfig.get("encloseLiquids", false) + val fillMicrodungeons = regionConfig.get("fillMicrodungeons", false) + + return Region( + biome = biome, + blockSelector = blockSelector, + fgCaveSelector = fgCaveSelector, + bgCaveSelector = bgCaveSelector, + fgOreSelector = fgOreSelector, + bgOreSelector = bgOreSelector, + subBlockSelector = subBlockSelector, + caveLiquid = caveLiquid?.let { Either.right(it) }, + caveLiquidSeedDensity = caveLiquidSeedDensity, + oceanLiquid = oceanLiquid?.let { Either.right(it) }, + oceanLiquidLevel = oceanLiquidLevel, + encloseLiquids = encloseLiquids, + fillMicrodungeons = fillMicrodungeons, + ) + } + + fun makeRegion(name: String, baseHeight: Int): Pair { + val primaryRegionJson = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() + JsonDriven.mergeNoCopy(primaryRegionJson, GlobalDefaults.terrestrialWorlds.regionTypes[name]!!) + + val region = readRegion(primaryRegionJson, baseHeight) + val subRegionList = primaryRegionJson.getArray("subRegion") + + val subRegion = readRegion(if (subRegionList.isEmpty) { + primaryRegionJson + } else { + val result = GlobalDefaults.terrestrialWorlds.regionDefaults.deepCopy() + JsonDriven.mergeNoCopy(result, GlobalDefaults.terrestrialWorlds.regionTypes[subRegionList.random(random).asString]!!) + result + }, baseHeight) + + return region to subRegion + } + + fun readLayer(layerName: String): Layer? { + if (layerName !in layers) + return null + + val layerConfig = config.getObject("layerDefaults").deepCopy() + JsonDriven.mergeNoCopy(layerConfig, layers.getObject(layerName)) + + if (!layerConfig.get("enabled", false)) + return null + + val layerLevel = layerConfig["layerLevel"].asInt + val baseHeight = layerConfig["baseHeight"].asInt + + val (primary, subPrimary) = makeRegion(layerConfig.getArray("primaryRegion").random(random).asString, baseHeight) + + val secondaryRegionCountRange = vectors2i.fromJsonTree(layerConfig["secondaryRegionCount"]) + var secondaryRegionCount = random.nextRange(secondaryRegionCountRange) + + val secondaryRegionList = layerConfig.getArray("secondaryRegions") + val secondary = ArrayList>() + + if (!secondaryRegionList.isEmpty && secondaryRegionCount > 0) { + val indices = IntArrayList(secondaryRegionList.size()) + indices.addAll(0 until secondaryRegionList.size()) + val shuffled = IntArrayList(secondaryRegionList.size()) + + while (indices.isNotEmpty()) { + val v = indices.random(random) + shuffled.add(v) + indices.removeInt(indices.indexOf(v)) + } + + while (secondaryRegionCount-- > 0 && shuffled.isNotEmpty()) { + val regionName = secondaryRegionList[shuffled.removeInt(0)].asString + secondary.add(makeRegion(regionName, baseHeight)) + } + } + + val secondaryRegionSizeRange = vectors2d.fromJsonTree(layerConfig["secondaryRegionSize"]) + val subRegionSizeRange = vectors2d.fromJsonTree(layerConfig["subRegionSize"]) + + val dungeonPool = dungeonPools.fromJsonTree(layerConfig.get("dungeons")) + val dungeonCountRange = if ("dungeonCountRange" in layerConfig) vectors2i.fromJsonTree(layerConfig["dungeonCountRange"]) else Vector2i.ZERO + val dungeonCount = random.nextRange(dungeonCountRange) + val dungeons = dungeonPool.sample(dungeonCount, random) + val dungeonXVariance = layerConfig.get("dungeonXVariance", 0) + + return Layer( + layerLevel, + baseHeight, + ImmutableSet.copyOf(dungeons), + dungeonXVariance, + primary, + subPrimary, + secondary.stream().map { it.first }.collect(ImmutableList.toImmutableList()), + secondary.stream().map { it.second }.collect(ImmutableList.toImmutableList()), + secondaryRegionSizeRange, + subRegionSizeRange + ) + } + + val surfaceLayer = readLayer("surface") ?: throw NoSuchElementException("No such layer 'surface', this should never happen") + val primaryBiome = Registries.biomes.getOrThrow(surfaceLayer.primaryRegion.biome) + + val parameters = TerrestrialWorldParameters() + + parameters.threatLevel = threadLevel + parameters.typeName = typeName + parameters.worldSize = params.size + parameters.gravity = Vector2d(y = -random.nextRange(params.gravityRange)) + parameters.airless = primaryBiome.value.airless + parameters.environmentStatusEffects = primaryBiome.value.statusEffects + parameters.overrideTech = params.overrideTech + parameters.globalDirectives = params.globalDirectives + parameters.beamUpRule = params.beamUpRule + parameters.disableDeathDrops = params.disableDeathDrops + parameters.worldEdgeForceRegions = params.worldEdgeForceRegions + parameters.weatherPool = primaryBiome.value.weather.stream().binnedChoice(threadLevel).get().random(random).value ?: throw NullPointerException("No weather pool") + parameters.primaryBiome = primaryBiome.key + parameters.sizeName = sizeName + parameters.hueShift = primaryBiome.value.hueShift(random) + + parameters.primarySurfaceLiquid = surfaceLayer.primaryRegion.oceanLiquid ?: surfaceLayer.primaryRegion.caveLiquid + parameters.skyColoring = primaryBiome.value.skyColoring(random) + parameters.dayLength = random.nextRange(params.dayLengthRange) + + parameters.blockNoiseConfig = params.blockNoise + parameters.blendNoiseConfig = params.blendNoise + parameters.blendSize = params.blendSize + + parameters.spaceLayer = readLayer("space") ?: throw NoSuchElementException("No such terrain layer 'space'") + parameters.atmosphereLayer = readLayer("atmosphere") ?: throw NoSuchElementException("No such terrain layer 'atmosphere'") + parameters.surfaceLayer = surfaceLayer + parameters.subsurfaceLayer = readLayer("subsurface") ?: throw NoSuchElementException("No such terrain layer 'subsurface'") + parameters.coreLayer = readLayer("core") ?: throw NoSuchElementException("No such terrain layer 'core'") + + val undergroundLayers = ArrayList() + var ulayer = readLayer("underground${undergroundLayers.size + 1}") + + while (ulayer != null) { + undergroundLayers.add(ulayer) + ulayer = readLayer("underground${undergroundLayers.size + 1}") + } + + parameters.undergroundLayers = undergroundLayers + + return parameters + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldsConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldsConfig.kt new file mode 100644 index 00000000..cc788c94 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TerrestrialWorldsConfig.kt @@ -0,0 +1,16 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class TerrestrialWorldsConfig( + val useSecondaryEnvironmentBiomeIndex: Boolean = false, + val regionDefaults: JsonObject, + val regionTypes: ImmutableMap, + + val planetDefaults: JsonObject, + val planetSizes: ImmutableMap, + val planetTypes: ImmutableMap, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt new file mode 100644 index 00000000..47165105 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt @@ -0,0 +1,121 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.ThingDescription +import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat + +@JsonFactory +data class TreeVariant( + val stemName: String, + val foliageName: String, + + val stemDirectory: String, + // may i fucking ask you why do you embed ENTIRE FUCKING FILE in + // this struct, Chucklefuck??????? + val stemSettings: JsonElement, + val stemHueShift: Double, + + val foliageDirectory: String, + // AGAIN. + val foliageSettings: FoliageData, + val foliageHueShift: Double, + + @JsonFlat + val descriptions: ThingDescription, + val ceiling: Boolean, + + val ephemeral: Boolean, + + val stemDropConfig: JsonElement, + val foliageDropConfig: JsonElement, + + val tileDamageParameters: TileDamageConfig, +) { + @JsonFactory + data class StemData( + val name: String, + val ceiling: Boolean = false, + val dropConfig: JsonElement = JsonObject(), + val shape: String, + + // TODO: original engine uses 'ephemeral' when creating tree variant with stem only, + // TODO: and uses 'allowsBlockPlacement' when creating tree with stem and foliage + // TODO: Bro what the FUCK? + val ephemeral: Boolean = false, + // val allowsBlockPlacement: Boolean = false, + + @JsonFlat + val descriptions: ThingDescription, + + val damageTable: AssetReference? = null, + val health: Double = 1.0, + ) + + @JsonFactory + data class FoliageData( + val name: String, + val dropConfig: JsonElement = JsonObject(), + val parallaxFoliage: Boolean = false, + val shape: String, + ) + + companion object { + fun create(stemName: String, stemHueShift: Double): TreeVariant { + return create(Registries.treeStemVariants.getOrThrow(stemName), stemHueShift) + } + + fun create(data: Registry.Entry, stemHueShift: Double): TreeVariant { + return TreeVariant( + stemDirectory = data.file?.computeDirectory() ?: "/", + stemSettings = data.json.deepCopy(), + stemHueShift = stemHueShift, + ceiling = data.value.ceiling, + stemDropConfig = data.value.dropConfig.deepCopy(), + descriptions = data.value.descriptions.fixDescription(data.key), + ephemeral = data.value.ephemeral, + tileDamageParameters = (data.value.damageTable?.value ?: GlobalDefaults.treeDamage).copy(totalHealth = data.value.health), + + foliageSettings = FoliageData("", shape = ""), + foliageDropConfig = JsonObject(), + foliageName = "", + foliageDirectory = "/", + foliageHueShift = 0.0, + stemName = data.key, + ) + } + + fun create(stemName: String, stemHueShift: Double, foliageName: String, foliageHueShift: Double): TreeVariant { + val data = Registries.treeStemVariants.getOrThrow(stemName) + val fdata = Registries.treeFoliageVariants.getOrThrow(foliageName) + + return create(data, stemHueShift, fdata, foliageHueShift) + } + + fun create(data: Registry.Entry, stemHueShift: Double, fdata: Registry.Entry, foliageHueShift: Double): TreeVariant { + return TreeVariant( + stemDirectory = data.file?.computeDirectory() ?: "/", + stemSettings = data.json.deepCopy(), + stemHueShift = stemHueShift, + ceiling = data.value.ceiling, + stemDropConfig = data.value.dropConfig.deepCopy(), + descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}"), + ephemeral = data.value.ephemeral, + tileDamageParameters = (data.value.damageTable?.value ?: GlobalDefaults.treeDamage).copy(totalHealth = data.value.health), + + foliageSettings = fdata.value, + foliageDropConfig = fdata.value.dropConfig.deepCopy(), + foliageName = fdata.key, + foliageDirectory = fdata.file?.computeDirectory() ?: "/", + foliageHueShift = foliageHueShift, + stemName = data.key, + ) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt new file mode 100644 index 00000000..f6142472 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Utils.kt @@ -0,0 +1,33 @@ +package ru.dbotthepony.kstarbound.defs.world + +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator + +@JsonFactory +data class BlockNoiseConfig( + val horizontalNoise: PerlinNoiseParameters, + val verticalNoise: PerlinNoiseParameters, + val noise: PerlinNoiseParameters, +) { + fun build(random: RandomGenerator): BlockNoise { + return BlockNoise( + AbstractPerlinNoise.of(horizontalNoise).also { it.init(random.nextLong()) }, + AbstractPerlinNoise.of(verticalNoise).also { it.init(random.nextLong()) }, + AbstractPerlinNoise.of(noise).also { it.init(random.nextLong()) }, + AbstractPerlinNoise.of(noise).also { it.init(random.nextLong()) }, + ) + } + + fun build(seed: Long) = build(random(seed)) +} + +@JsonFactory +data class BlockNoise( + val horizontalNoise: AbstractPerlinNoise, + val verticalNoise: AbstractPerlinNoise, + val xNoise: AbstractPerlinNoise, + val yNoise: AbstractPerlinNoise, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt new file mode 100644 index 00000000..5dfb9a71 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/VisitableWorldParameters.kt @@ -0,0 +1,196 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.Gson +import com.google.gson.JsonObject +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.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import kotlin.properties.Delegates + +enum class BeamUpRule(val jsonName: String) : IStringSerializable { + NOWHERE("nowhere"), + SURFACE("surface"), + ANYWHERE("anywhere"), + ANYWHERE_WITH_WARNING("anywherewithwarning"); + + override fun match(name: String): Boolean { + return name.lowercase() == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } +} + +enum class WorldEdgeForceRegion(val sname: String) : IStringSerializable { + NONE("none"), + TOP("top"), + BOTTOM("bottom"), + TOP_AND_BOTTOM("topandbottom"); + + override fun match(name: String): Boolean { + return name.lowercase() == sname + } + + override fun write(out: JsonWriter) { + out.value(sname) + } +} + +enum class VisitableWorldParametersType(val jsonName: String, val token: TypeToken) : IStringSerializable { + TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)), + ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)), + FLOATING_DUNGEON("FloatingDungeonWorldParameters", TypeToken.get(FloatingDungeonWorldParameters::class.java)); + + override fun match(name: String): Boolean { + return name == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } + + companion object : TypeAdapterFactory { + val ADAPTER = DispatchingAdapter("type", { type }, { token }, entries) + + 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() + else + out.value(value.toJson(false)) + } + + override fun read(`in`: JsonReader): VisitableWorldParameters? { + if (`in`.consumeNull()) + return null + + val instance = type.rawType.getDeclaredConstructor().newInstance() as VisitableWorldParameters + instance.fromJson(objects.read(`in`)) + return instance + } + } as TypeAdapter + } + + return null + } + } +} + +// using notnull delegate because this will look very ugly +// if done as immutable class +abstract class VisitableWorldParameters { + var threatLevel: Double = 0.0 + protected set + var typeName: String by Delegates.notNull() + protected set + var worldSize: Vector2i by Delegates.notNull() + protected set + var gravity: Vector2d = Vector2d.ZERO + protected set + var airless: Boolean = false + protected set + var environmentStatusEffects: Set by Delegates.notNull() + protected set + var overrideTech: Set? = null + protected set + var globalDirectives: Set? = null + protected set + var beamUpRule: BeamUpRule by Delegates.notNull() + protected set + var disableDeathDrops: Boolean = false + protected set + var terraformed: Boolean = false + protected set + var worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.NONE + protected set + var weatherPool: WeightedList? = null + protected set + + abstract fun createLayout(seed: Long): WorldLayout + + abstract val type: VisitableWorldParametersType + + // lazy but effective, and less error prone + @JsonFactory + data class StoreData( + val threatLevel: Double, + val typeName: String, + val worldSize: Vector2i, + val gravity: Either, + val airless: Boolean, + val environmentStatusEffects: Set, + val overrideTech: Set?, + val globalDirectives: Set?, + val beamUpRule: BeamUpRule, + val disableDeathDrops: Boolean, + val terraformed: Boolean, + val worldEdgeForceRegions: WorldEdgeForceRegion, + val weatherPool: WeightedList?, + ) + + open fun fromJson(data: JsonObject) { + val read = Starbound.gson.fromJson(data, StoreData::class.java) + + this.threatLevel = read.threatLevel + this.typeName = read.typeName + this.worldSize = read.worldSize + this.gravity = read.gravity.map({ Vector2d(y = it) }, { it }) + this.airless = read.airless + this.environmentStatusEffects = read.environmentStatusEffects + this.overrideTech = read.overrideTech + this.globalDirectives = read.globalDirectives + this.beamUpRule = read.beamUpRule + this.disableDeathDrops = read.disableDeathDrops + this.terraformed = read.terraformed + this.worldEdgeForceRegions = read.worldEdgeForceRegions + this.weatherPool = read.weatherPool + } + + open fun toJson(data: JsonObject, isLegacy: Boolean) { + val store = StoreData( + threatLevel, + typeName, + worldSize, + if (isLegacy) Either.left(gravity.y) else Either.right(gravity), + airless, + environmentStatusEffects, + overrideTech, + globalDirectives, + beamUpRule, + disableDeathDrops, + terraformed, + worldEdgeForceRegions, + weatherPool, + ) + + for ((k, v) in (Starbound.gson.toJsonTree(store) as JsonObject).entrySet()) { + data[k] = v + } + + data["type"] = type.jsonName + } + + fun toJson(isLegacy: Boolean): JsonObject { + val data = JsonObject() + toJson(data, isLegacy) + return data + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt new file mode 100644 index 00000000..bf5005f2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -0,0 +1,417 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.reflect.TypeToken +import it.unimi.dsi.fastutil.doubles.DoubleArrayList +import it.unimi.dsi.fastutil.ints.IntArrayList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.gson.JsonArrayCollector +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.getArray +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABBi +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.world.terrain.AbstractTerrainSelector +import ru.dbotthepony.kstarbound.defs.world.terrain.Biome +import ru.dbotthepony.kstarbound.defs.world.terrain.TerrainSelectorParameters +import ru.dbotthepony.kstarbound.defs.world.terrain.createNamedTerrainSelector +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.ListInterner +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.nextRange +import java.util.random.RandomGenerator +import kotlin.math.roundToInt +import kotlin.properties.Delegates + +/** + * While this class seems redundant, it is not. + * + * Every world must be represented by layers and regions. + * And some worlds don't have visitable world parameters because they are custom-made, + * such as dungeon worlds. + * + * Layer is a "line" than spans from 0 to world width, and has determined height. + * Region, on other hand, is 1D section (slice) of layer, representing unique biome. + * This structure allows to have diverse worlds, but regions are limited to cover only one + * layer at time. + * + * While final region structure can be anything, generated terrestrial worlds + * regions are generated in following pattern (per layer): + * `[... [main region 0][sub region 0][main region 0] [main region 1][sub region 1][main region 1] ...]` + * + * Sucks that it has to virtually duplicate everything when creating from + * [TerrestrialWorldParameters], though. + * + * World regions terrain selectors and selectors themselves are stored separately, + * and hence create weird "index" structure in original code. This was done to + * greatly cut down serialized form size. We, however, don't do this at runtime, + * and reference biomes/terrain selectors directly. + */ +class WorldLayout { + var worldSize: Vector2i by Delegates.notNull() + var regionBlending: Double = 0.0 + var blendNoise: AbstractPerlinNoise? = null + var blockNoise: BlockNoise? = null + val terrainSelectors = ListInterner>() + val biomes = ListInterner() + + val playerStartSearchRegions = ArrayList() + val layers = ArrayList() + + private object StartingRegionsToken : TypeToken>() + + @JsonFactory + data class SerializedLayer( + val yStart: Int, + val boundaries: IntArrayList, + val cells: ImmutableList, + ) + + inner class Layer(val yStart: Int) : Comparable { + val boundaries = IntArrayList() + val cells = ArrayList() + + override fun compareTo(other: Layer): Int { + return yStart.compareTo(other.yStart) + } + + fun toJson(isLegacy: Boolean): JsonObject { + val data = JsonObject() + + data["yStart"] = yStart + data["boundaries"] = boundaries.stream().map { JsonPrimitive(it) }.collect(JsonArrayCollector) + data["cells"] = cells.stream().map { it.toJson(isLegacy) }.collect(JsonArrayCollector) + + return data + } + } + + @JsonFactory + data class RegionLiquids( + val caveLiquid: Either? = null, + val caveLiquidSeedDensity: Double = 0.0, + + val oceanLiquid: Either? = null, + val oceanLiquidLevel: Int = 0, + + val encloseLiquids: Boolean = false, + val fillMicrodungeons: Boolean = false, + ) { + fun toLegacy(): RegionLiquidsLegacy { + return RegionLiquidsLegacy( + caveLiquid = caveLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0, + caveLiquidSeedDensity = caveLiquidSeedDensity, + oceanLiquid = oceanLiquid?.map({ it }, { Registries.liquid[it]?.id }) ?: 0, + oceanLiquidLevel = oceanLiquidLevel, + encloseLiquids = encloseLiquids, + fillMicrodungeons = fillMicrodungeons, + ) + } + } + + @JsonFactory + data class RegionLiquidsLegacy( + val caveLiquid: Int = 0, + val caveLiquidSeedDensity: Double = 0.0, + + val oceanLiquid: Int = 0, + val oceanLiquidLevel: Int = 0, + + val encloseLiquids: Boolean = false, + val fillMicrodungeons: Boolean = false, + ) + + @JsonFactory + data class SerializedRegion( + val terrainSelectorIndex: Int, + val foregroundCaveSelectorIndex: Int, + val backgroundCaveSelectorIndex: Int, + + val blockBiomeIndex: Int, + val environmentBiomeIndex: Int, + + val subBlockSelectorIndexes: IntArrayList, + val foregroundOreSelectorIndexes: IntArrayList, + val backgroundOreSelectorIndexes: IntArrayList, + ) + + inner class Region( + val terrainSelector: AbstractTerrainSelector<*>?, + val foregroundCaveSelector: AbstractTerrainSelector<*>?, + val backgroundCaveSelector: AbstractTerrainSelector<*>?, + + val blockBiome: Biome?, + var environmentBiome: Biome?, + + val subBlockSelector: List>, + val foregroundOreSelector: List>, + val backgroundOreSelector: List>, + + val regionLiquids: RegionLiquids, + ) { + fun toJson(isLegacy: Boolean): JsonObject { + val data = SerializedRegion( + terrainSelectorIndex = terrainSelectors.list.indexOf(terrainSelector), + foregroundCaveSelectorIndex = terrainSelectors.list.indexOf(foregroundCaveSelector), + backgroundCaveSelectorIndex = terrainSelectors.list.indexOf(backgroundCaveSelector), + blockBiomeIndex = biomes.list.indexOf(blockBiome), + environmentBiomeIndex = biomes.list.indexOf(environmentBiome), + subBlockSelectorIndexes = subBlockSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), + foregroundOreSelectorIndexes = foregroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), + backgroundOreSelectorIndexes = backgroundOreSelector.stream().mapToInt { terrainSelectors.list.indexOf(it) }.filter { it != -1 }.collect(::IntArrayList, IntArrayList::add, IntArrayList::addAll), + ) + + val liquidData = (if (isLegacy) Starbound.gson.toJsonTree(regionLiquids.toLegacy()) else Starbound.gson.toJsonTree(regionLiquids)) as JsonObject + val data2 = Starbound.gson.toJsonTree(data) as JsonObject + + for ((k, v) in data2.entrySet()) { + liquidData[k] = v + } + + return liquidData + } + } + + data class RegionParameters( + val baseHeight: Int, + val threatLevel: Double, + val biomeName: String?, + val terrainSelector: String?, + val fgCaveSelector: String?, + val bgCaveSelector: String?, + val fgOreSelector: String?, + val bgOreSelector: String?, + val subBlockSelector: String?, + val regionLiquids: RegionLiquids, + ) + + @JsonFactory + data class SerializedForm( + val worldSize: Vector2i, + val regionBlending: Double, + val blockNoise: BlockNoise? = null, + val blendNoise: AbstractPerlinNoise? = null, + val playerStartSearchRegions: List, + val biomes: List, + val terrainSelectors: List>, + val layers: JsonArray, + ) + + fun toJson(isLegacy: Boolean): JsonObject { + return Starbound.gson.toJsonTree(SerializedForm( + worldSize, regionBlending, blockNoise, blendNoise, + playerStartSearchRegions, biomes.list, terrainSelectors.list, + layers = layers.stream().map { it.toJson(isLegacy) }.collect(JsonArrayCollector) + )) as JsonObject + } + + fun fromJson(data: JsonObject) { + val load = Starbound.gson.fromJson(data, SerializedForm::class.java) + + worldSize = load.worldSize + regionBlending = load.regionBlending + blockNoise = load.blockNoise + blendNoise = load.blendNoise + playerStartSearchRegions.addAll(load.playerStartSearchRegions) + + load.layers.forEach { + val datalayer = Starbound.gson.fromJson(it, SerializedLayer::class.java) + val layer = Layer(datalayer.yStart) + layer.boundaries.addAll(datalayer.boundaries) + + datalayer.cells.forEach { + val region = Starbound.gson.fromJson(it, SerializedRegion::class.java) + + layer.cells.add(Region( + terrainSelector = load.terrainSelectors.getOrNull(region.terrainSelectorIndex)?.let { terrainSelectors.intern(it) }, + foregroundCaveSelector = load.terrainSelectors.getOrNull(region.foregroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) }, + backgroundCaveSelector = load.terrainSelectors.getOrNull(region.backgroundCaveSelectorIndex)?.let { terrainSelectors.intern(it) }, + blockBiome = load.biomes.getOrNull(region.blockBiomeIndex)?.let { biomes.intern(it) }, + environmentBiome = load.biomes.getOrNull(region.environmentBiomeIndex)?.let { biomes.intern(it) }, + subBlockSelector = region.subBlockSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), + foregroundOreSelector = region.foregroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), + backgroundOreSelector = region.backgroundOreSelectorIndexes.map { load.terrainSelectors.getOrNull(it)?.let { terrainSelectors.intern(it) } }.filterNotNull(), + regionLiquids = Starbound.gson.fromJson(it, RegionLiquids::class.java), + )) + } + } + } + + private fun buildRegion(random: RandomGenerator, params: RegionParameters): Region { + var terrainSelector: AbstractTerrainSelector<*>? = null + var foregroundCaveSelector: AbstractTerrainSelector<*>? = null + var backgroundCaveSelector: AbstractTerrainSelector<*>? = null + + val terrainBase = TerrainSelectorParameters(worldSize.x, params.baseHeight.toDouble()) + val terrain = terrainBase.withSeed(random.nextLong()) + val fg = terrainBase.withSeed(random.nextLong()) + val bg = terrainBase.withSeed(random.nextLong()) + + if (params.terrainSelector != null) + terrainSelector = terrainSelectors.intern(createNamedTerrainSelector(params.terrainSelector, terrain)) + if (params.fgCaveSelector != null) + foregroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.fgCaveSelector, fg)) + if (params.bgCaveSelector != null) + backgroundCaveSelector = terrainSelectors.intern(createNamedTerrainSelector(params.bgCaveSelector, bg)) + + val subBlockSelector = ArrayList>() + val foregroundOreSelector = ArrayList>() + val backgroundOreSelector = ArrayList>() + + var biome: Biome? = null + + if (params.biomeName != null) { + biome = biomes.intern(Registries.biomes.getOrThrow(params.biomeName).value.create(random, params.baseHeight, params.threatLevel)) + + if (params.subBlockSelector != null) { + for (i in 0 until biome.subBlocks.size) { + subBlockSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.subBlockSelector, terrainBase.withSeed(random.nextLong())))) + } + + for ((ore, commonality) in biome.ores) { + val oreParams = terrainBase.withCommonality(commonality) + + if (params.fgOreSelector != null) { + foregroundOreSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.fgOreSelector, oreParams.withSeed(random.nextLong())))) + } + + if (params.bgOreSelector != null) { + backgroundOreSelector.add(terrainSelectors.intern(createNamedTerrainSelector(params.bgOreSelector, oreParams.withSeed(random.nextLong())))) + } + } + } + } + + return Region( + terrainSelector = terrainSelector, + foregroundCaveSelector = foregroundCaveSelector, + backgroundCaveSelector = backgroundCaveSelector, + + subBlockSelector = subBlockSelector, + foregroundOreSelector = foregroundOreSelector, + backgroundOreSelector = backgroundOreSelector, + + blockBiome = biome, + environmentBiome = biome, + + regionLiquids = params.regionLiquids + ) + } + + fun addLayer(random: RandomGenerator, yStart: Int, params: RegionParameters) { + val layer = Layer(yStart) + layer.cells.add(buildRegion(random, params)) + layers.add(layer) + } + + fun addLayer( + random: RandomGenerator, yStart: Int, yBase: Int, primaryBiome: String, + primaryRegionParams: RegionParameters, primarySubRegionParams: RegionParameters, + secondaryRegions: List, secondarySubRegions: List, + secondaryRegionSize: Vector2d, subRegionSize: Vector2d + ) { + require(secondaryRegions.size == secondarySubRegions.size) { "${secondaryRegions.size} != ${secondarySubRegions.size}" } + require(subRegionSize.y <= 1.0) { "subRegionSize.y > 1.0: ${subRegionSize.y}" } + + val layer = Layer(yStart) + val relativeRegionSizes = DoubleArrayList() + var totalRelativeSize = 0.0 + + val primaryEnvironment = buildRegion(random, primaryRegionParams) + val spawnBiomes = ObjectOpenHashSet() + + fun addRegion(params: RegionParameters, subParams: RegionParameters, regionSizeRange: Vector2d) { + val region = buildRegion(random, params) + val subRegion = buildRegion(random, params) + + if (!GlobalDefaults.terrestrialWorlds.useSecondaryEnvironmentBiomeIndex) { + region.environmentBiome = primaryEnvironment.environmentBiome + } + + subRegion.environmentBiome = region.environmentBiome + + if (params.biomeName == primaryBiome && region.blockBiome != null) + spawnBiomes.add(region.blockBiome) + + if (subParams.biomeName == primaryBiome && subRegion.blockBiome != null) + spawnBiomes.add(subRegion.blockBiome) + + layer.cells.add(region) + layer.cells.add(subRegion) + layer.cells.add(region) + + var regionRelativeSize = random.nextRange(regionSizeRange) + var subRegionRelativeSize = random.nextRange(subRegionSize) + + totalRelativeSize += regionRelativeSize + subRegionRelativeSize *= regionRelativeSize + regionRelativeSize -= subRegionRelativeSize + + relativeRegionSizes.add(regionRelativeSize / 2.0) + relativeRegionSizes.add(subRegionRelativeSize) + relativeRegionSizes.add(regionRelativeSize / 2.0) + } + + // construct list of region cells and relative sizes + addRegion(primaryRegionParams, primarySubRegionParams, Vector2d.POSITIVE_XY) + + for ((i, region) in secondaryRegions.withIndex()) { + addRegion(region, secondarySubRegions[i], secondaryRegionSize) + } + + // construct boundaries based on normalized sizes + var nextBoundary = random.nextInt(0, worldSize.x) + layer.boundaries.add(nextBoundary) + + for (v in relativeRegionSizes) { + nextBoundary += (worldSize.x * v / totalRelativeSize).roundToInt() + layer.boundaries.add(nextBoundary) + } + + // wrap cells + boundaries + while (layer.boundaries.last() > worldSize.x) { + layer.cells.add(0, layer.cells.removeLast()) + layer.boundaries.add(0, layer.boundaries.removeLast() - worldSize.x) + } + + layer.cells.add(0, layer.cells.last()) + val yRange = GlobalDefaults.worldTemplate.playerStartSearchYRange + var i = 0 + var lastBoundary = 0 + + for (region in layer.cells) { + nextBoundary = if (i < layer.boundaries.size) layer.boundaries.getInt(i) else worldSize.x + + if (region.blockBiome in spawnBiomes) { + playerStartSearchRegions.add(AABBi( + Vector2i(lastBoundary, (yBase - yRange).coerceAtLeast(0)), + Vector2i(nextBoundary, (yBase + yRange).coerceAtMost(worldSize.y)), + )) + } + + lastBoundary = nextBoundary + i++ + } + + layers.add(layer) + } + + fun finalize(skyColoring: RGBAColor) { + layers.sort() + + for (biome in biomes) { + biome.parallax?.fadeToSkyColor(skyColoring) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt new file mode 100644 index 00000000..3faef8ba --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -0,0 +1,100 @@ +package ru.dbotthepony.kstarbound.defs.world + +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.world.Universe +import ru.dbotthepony.kstarbound.world.UniversePos +import ru.dbotthepony.kstarbound.world.WorldGeometry +import kotlin.properties.Delegates + +class WorldTemplate(val geometry: WorldGeometry) { + var seed: Long = 0L + private set + var worldName: String = "" + private set + var worldParameters: VisitableWorldParameters? = null + private set + var worldLayout: WorldLayout? = null + private set + var skyParameters: SkyParameters = SkyParameters() + private set + var celestialParameters: CelestialParameters? = null + private set + + constructor(worldParameters: VisitableWorldParameters, skyParameters: SkyParameters, seed: Long) : this(WorldGeometry(worldParameters.worldSize, true, false)) { + this.seed = seed + this.skyParameters = skyParameters + this.worldLayout = worldParameters.createLayout(seed) + } + + fun determineName() { + if (celestialParameters != null) { + worldName = celestialParameters!!.name + } else if (worldParameters is FloatingDungeonWorldParameters) { + + } else { + worldName = "" + } + } + + @JsonFactory + data class SerializedForm( + val celestialParameters: CelestialParameters? = null, + val worldParameters: VisitableWorldParameters? = null, + val skyParameters: SkyParameters = SkyParameters(), + val seed: Long = 0L, + val size: Either, + val regionData: WorldLayout? = null, + //val customTerrainRegions: + ) + + fun toJson(isLegacy: Boolean): JsonObject { + val data = Starbound.gson.toJsonTree(SerializedForm( + celestialParameters, worldParameters, skyParameters, seed, + if (isLegacy) Either.right(geometry.size) else Either.left(geometry), + )) as JsonObject + + data["regionData"] = worldLayout?.toJson(isLegacy) ?: JsonNull.INSTANCE + return data + } + + companion object { + suspend fun create(coordinate: UniversePos, universe: Universe): WorldTemplate { + val params = universe.parameters(coordinate) ?: throw IllegalArgumentException("$universe has nothing at $coordinate!") + val visitable = params.visitableParameters ?: throw IllegalArgumentException("$coordinate of $universe is not visitable") + + val template = WorldTemplate(WorldGeometry(visitable.worldSize, true, false)) + + template.seed = params.seed + template.worldLayout = visitable.createLayout(params.seed) + template.skyParameters = SkyParameters.create(coordinate, universe) + template.celestialParameters = params + template.worldParameters = visitable + + template.determineName() + + return template + } + + fun fromJson(data: JsonObject): WorldTemplate { + val load = Starbound.gson.fromJson(data, SerializedForm::class.java) + val template = WorldTemplate(load.size.map({ it }, { WorldGeometry(it, true, false) })) + + template.celestialParameters = load.celestialParameters + template.worldParameters = load.worldParameters + template.skyParameters = load.skyParameters + template.seed = load.seed + template.worldLayout = load.regionData + + template.determineName() + + return template + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt new file mode 100644 index 00000000..126d4ce8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplateConfig.kt @@ -0,0 +1,8 @@ +package ru.dbotthepony.kstarbound.defs.world + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class WorldTemplateConfig( + val playerStartSearchYRange: Int, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt new file mode 100644 index 00000000..084eb41b --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/AbstractTerrainSelector.kt @@ -0,0 +1,47 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.Starbound + +abstract class AbstractTerrainSelector(val name: String, val config: D, val parameters: TerrainSelectorParameters) { + // Returns a float signifying the "solid-ness" of a block, >= 0.0 should be + // considered solid, < 0.0 should be considered open space. + abstract operator fun get(x: Int, y: Int): Double + + abstract val type: TerrainSelectorType + + fun toJson(): JsonObject { + val result = JsonObject() + result["name"] = name + result["config"] = Starbound.gson.toJsonTree(config) + result["parameters"] = Starbound.gson.toJsonTree(parameters) + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) + return true + + if (other == null || this::class.java != other::class.java) + return false + + other as AbstractTerrainSelector<*> + return name == other.name && config == other.config && parameters == other.parameters + } + + private val hash by lazy { + var h = name.hashCode() + h = h * 31 + config.hashCode() + h = h * 31 + parameters.hashCode() + h + } + + override fun hashCode(): Int { + return hash + } + + override fun toString(): String { + return "${this::class.simpleName}[$name, config=$config, parameters=$parameters]" + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/Biome.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/Biome.kt new file mode 100644 index 00000000..05982285 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/Biome.kt @@ -0,0 +1,241 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import com.google.common.collect.ImmutableList +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.JsonPrimitive +import com.google.gson.JsonSyntaxException +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.JsonWriter +import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.stream +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.defs.world.AmbientNoisesDefinition +import ru.dbotthepony.kstarbound.defs.world.BushVariant +import ru.dbotthepony.kstarbound.defs.world.GrassVariant +import ru.dbotthepony.kstarbound.defs.world.Parallax +import ru.dbotthepony.kstarbound.defs.world.TreeVariant +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat +import ru.dbotthepony.kstarbound.json.builder.JsonImplementation +import ru.dbotthepony.kstarbound.json.listAdapter +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import java.util.stream.Stream + +@JsonFactory +data class BiomePlaceables( + val grassMod: Registry.Entry? = null, + val ceilingGrassMod: Registry.Entry? = null, + val grassModDensity: Double = 0.0, + val ceilingGrassModDensity: Double = 0.0, + val items: ImmutableList = ImmutableList.of(), +) { + fun firstTreeVariant(): TreeVariant? { + return items.stream() + .flatMap { it.data.itemStream() } + .map { it as? Tree } + .filterNotNull() + .flatMap { it.trees.stream() } + .findAny() + .orElse(null) + } + + // ----------- ITEMS + @JsonFactory + data class DistributionItem( + val priority: Double = 0.0, + val variants: Int = 1, + val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR, + @JsonFlat + val data: DistributionData, + ) + + abstract class Item { + abstract val type: BiomePlacementItemType + + abstract fun toJson(): JsonElement + + companion object : TypeAdapterFactory { + 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() + private val objects = gson.getAdapter(PoolTypeToken) + + override fun write(out: JsonWriter, value: Item?) { + if (value == null) + out.nullValue() + else { + out.beginArray() + + when (value.type) { + BiomePlacementItemType.MICRO_DUNGEON -> out.value("microDungeon") + BiomePlacementItemType.TREASURE_BOX_SET -> out.value("treasureBoxSet") + BiomePlacementItemType.GRASS -> out.value("grass") + BiomePlacementItemType.BUSH -> out.value("bush") + BiomePlacementItemType.TREE -> out.value("treePair") + BiomePlacementItemType.OBJECT -> out.value("objectPool") + } + + out.value(value.toJson()) + out.endArray() + } + } + + override fun read(`in`: JsonReader): Item? { + if (`in`.consumeNull()) + return null + + `in`.beginArray() + + // sucks that this has to be done manually + // Also I salute to whoever decided to give different names + // and different comparison rules for data in *.biome files + // and world storage data at Chucklefish. + // Truly our hero here. + val obj = when (val type = `in`.nextString()) { + "treasureBoxSet" -> TreasureBox(`in`.nextString()) + "microDungeon" -> MicroDungeon(arrays.read(`in`).stream().map { it.asString }.collect(ImmutableSet.toImmutableSet())) + "grass" -> Grass(grassVariant.read(`in`)) + "bush" -> Bush(bushVariant.read(`in`)) + "tree" -> Tree(trees.read(`in`)) + "objectPool" -> Object(objects.read(`in`)) + else -> throw JsonSyntaxException("Unknown biome placement item $type") + } + + `in`.endArray() + return obj + } + } as TypeAdapter + } + + return null + } + } + } + + data class MicroDungeon(val microdungeons: ImmutableSet = ImmutableSet.of()) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.MICRO_DUNGEON + + override fun toJson(): JsonElement { + return JsonArray().also { j -> + microdungeons.forEach { j.add(JsonPrimitive(it)) } + } + } + } + + data class TreasureBox(val pool: String) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.TREASURE_BOX_SET + + override fun toJson(): JsonElement { + return JsonPrimitive(pool) + } + } + + data class Grass(val value: GrassVariant) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.GRASS + + override fun toJson(): JsonElement { + return Starbound.gson.toJsonTree(value) + } + } + + data class Bush(val value: BushVariant) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.BUSH + + override fun toJson(): JsonElement { + return Starbound.gson.toJsonTree(value) + } + } + + data class Tree(val trees: ImmutableList) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.TREE + + override fun toJson(): JsonElement { + return Starbound.gson.toJsonTree(trees, TypeToken.getParameterized(ImmutableList::class.java, TreeVariant::class.java).type) + } + } + + private object PoolTypeToken : TypeToken>>() + + // This structure sucks, but at least it allows unique parameters per + // each object (lmao, whos gonna write world json by hand anyway???? + // considering this is world generation data.) + data class Object(val pool: WeightedList>) : Item() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.OBJECT + + override fun toJson(): JsonElement { + return Starbound.gson.toJsonTree(pool, PoolTypeToken.type) + } + } + + // -------- DISTRIBUTION + abstract class DistributionData { + abstract val type: BiomePlacementDistributionType + + abstract fun itemStream(): Stream + } + + @JsonFactory + data class RandomDistribution( + val blockSeed: Long, + val randomItems: ImmutableList, + ) : DistributionData() { + override val type: BiomePlacementDistributionType + get() = BiomePlacementDistributionType.RANDOM + + override fun itemStream(): Stream { + return randomItems.stream() + } + } + + @JsonFactory + data class PeriodicDistribution( + val modulus: Int, + val modulusOffset: Int, + val densityFunction: AbstractPerlinNoise, + val modulusDistortion: AbstractPerlinNoise, + val weightedItems: ImmutableList>, + ) : DistributionData() { + override val type: BiomePlacementDistributionType + get() = BiomePlacementDistributionType.PERIODIC + + override fun itemStream(): Stream { + return weightedItems.stream().map { it.first } + } + } +} + +data class Biome( + val hueShift: Double = 0.0, + val baseName: String, + val description: String, + val mainBlock: Registry.Entry? = null, + val subBlocks: ImmutableList> = ImmutableList.of(), + val ores: ImmutableList, Double>> = ImmutableList.of(), + val musicTrack: AmbientNoisesDefinition? = null, + val ambientNoises: AmbientNoisesDefinition? = null, + val surfacePlaceables: BiomePlaceables = BiomePlaceables(), + val undergroundPlaceables: BiomePlaceables = BiomePlaceables(), + val parallax: Parallax? = null, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/BiomeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/BiomeDefinition.kt new file mode 100644 index 00000000..f4c51679 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/BiomeDefinition.kt @@ -0,0 +1,470 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.collect.filterNotNull +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.collect.WeightedList +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.JsonConfigFunction +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier +import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.defs.world.AmbientNoisesDefinition +import ru.dbotthepony.kstarbound.defs.world.BushVariant +import ru.dbotthepony.kstarbound.defs.world.GrassVariant +import ru.dbotthepony.kstarbound.defs.world.Parallax +import ru.dbotthepony.kstarbound.defs.world.SkyColoring +import ru.dbotthepony.kstarbound.defs.world.TreeVariant +import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat +import ru.dbotthepony.kstarbound.json.builder.JsonSingleton +import ru.dbotthepony.kstarbound.json.pairListAdapter +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import ru.dbotthepony.kstarbound.util.random.nextRange +import ru.dbotthepony.kstarbound.util.random.random +import java.util.random.RandomGenerator +import java.util.stream.IntStream + +enum class BiomePlacementDistributionType( + val jsonName: String, + val def: TypeToken, + val data: TypeToken, +) : IStringSerializable { + RANDOM("random", TypeToken.get(BiomePlaceablesDefinition.RandomDistribution::class.java), TypeToken.get(BiomePlaceables.RandomDistribution::class.java)), + PERIODIC("periodic", TypeToken.get(BiomePlaceablesDefinition.PeriodicDistribution::class.java), TypeToken.get(BiomePlaceables.PeriodicDistribution::class.java)); + + override fun match(name: String): Boolean { + return name.lowercase() == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } + + companion object { + val DEFINITION_ADAPTER = DispatchingAdapter("type", { type }, { def }, entries) + val DATA_ADAPTER = DispatchingAdapter("type", { type }, { data }, entries) + } +} + +enum class BiomePlacementItemType( + val jsonName: String, + val def: TypeToken, + val data: TypeToken, +) : IStringSerializable { + MICRO_DUNGEON("microdungeon", TypeToken.get(BiomePlaceablesDefinition.MicroDungeon::class.java), TypeToken.get(BiomePlaceables.MicroDungeon::class.java)), + TREASURE_BOX_SET("treasurebox", TypeToken.get(BiomePlaceablesDefinition.TreasureBox::class.java), TypeToken.get(BiomePlaceables.TreasureBox::class.java)), + GRASS("grass", TypeToken.get(BiomePlaceablesDefinition.Grass::class.java), TypeToken.get(BiomePlaceables.Grass::class.java)), + BUSH("bush", TypeToken.get(BiomePlaceablesDefinition.Bush::class.java), TypeToken.get(BiomePlaceables.Bush::class.java)), + TREE("tree", TypeToken.get(BiomePlaceablesDefinition.Tree::class.java), TypeToken.get(BiomePlaceables.Tree::class.java)), + OBJECT("object", TypeToken.get(BiomePlaceablesDefinition.Object::class.java), TypeToken.get(BiomePlaceables.Object::class.java)), + ; + + override fun match(name: String): Boolean { + return name.lowercase() == jsonName + } + + override fun write(out: JsonWriter) { + out.value(jsonName) + } + + companion object { + val DEFINITION_ADAPTER = DispatchingAdapter("type", { type }, { def }, entries) + val DATA_ADAPTER = DispatchingAdapter("type", { type }, { data }, entries) + } +} + +@JsonFactory +data class BiomePlaceablesDefinition( + val grassMod: ImmutableList> = ImmutableList.of(), + val grassModDensity: Double = 0.0, + val ceilingGrassMod: ImmutableList> = ImmutableList.of(), + val ceilingGrassModDensity: Double = 0.0, + val items: ImmutableList = ImmutableList.of(), +) { + enum class Placement { + FLOOR, CEILING, BACKGROUND, OCEAN; + } + + // ----------- ITEMS + @JsonFactory + data class DistributionItem( + val priority: Double = 0.0, + val variants: Int = 1, + val mode: Placement = Placement.FLOOR, + val distribution: AssetReference, + @JsonFlat + val data: DistributionItemData, + ) { + init { + checkNotNull(distribution.value) { "Distribution data is missing" } + } + + fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionItem { + return BiomePlaceables.DistributionItem( + priority = priority, + variants = variants, + mode = mode, + data = distribution.value!!.create(this, biome), + ) + } + } + + abstract class DistributionItemData { + abstract val type: BiomePlacementItemType + abstract fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item + } + + @JsonFactory + data class MicroDungeon(val microdungeons: ImmutableSet = ImmutableSet.of()) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.MICRO_DUNGEON + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + return BiomePlaceables.MicroDungeon(microdungeons) + } + } + + @JsonFactory + data class TreasureBox(val treasureBoxSets: ImmutableSet = ImmutableSet.of()) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.MICRO_DUNGEON + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + return BiomePlaceables.TreasureBox(treasureBoxSets.random(biome.random)) + } + } + + @JsonFactory + data class Grass(val grasses: ImmutableSet> = ImmutableSet.of()) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.GRASS + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + val valid = grasses.stream().map { it.entry }.filterNotNull().toList() + + if (valid.isEmpty()) { + throw NoSuchElementException("None of grass variants are valid (candidates: $grasses)") + } + + return BiomePlaceables.Grass(GrassVariant.create(valid.random(biome.random), biome.hueShift)) + } + } + + @JsonFactory + data class Tree( + val treeStemList: ImmutableList> = ImmutableList.of(), + val treeFoliageList: ImmutableList> = ImmutableList.of(), + val treeStemHueShiftMax: Double = 0.0, + val treeFoliageHueShiftMax: Double = 0.0, + val variantsRange: Vector2i = Vector2i(1, 1), + val subVariantsRange: Vector2i = Vector2i(2, 2), + val sameStemHueShift: Boolean = true, + val sameFoliageHueShift: Boolean = false, + ) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.GRASS + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + val validTreeStems = treeStemList.stream().map { it.entry }.filterNotNull().toList() + val validFoliage = treeFoliageList.stream().filter { it.key.left().isBlank() || it.entry != null }.toList() + + if (validTreeStems.isEmpty()) { + throw NoSuchElementException("None of tree stems variants are valid (candidates: $treeStemList)") + } + + if (validFoliage.isEmpty()) { + throw NoSuchElementException("None of tree foliage variants are valid (candidates: $treeFoliageList)") + } + + val pairs = ArrayList, Registry.Entry?>>() + + for (stem in validTreeStems) { + for (foliage in validFoliage) { + if (foliage.entry == null || stem.value.shape == foliage.value!!.shape) { + pairs.add(stem to foliage?.entry) + } + } + } + + if (pairs.isEmpty()) { + throw NoSuchElementException("Out of all possible combinations of tree stems and foliage, none match by shape (stems candidates: $validTreeStems; foliage candidates: $validFoliage)") + } + + val treeVariants = biome.random.nextRange(variantsRange) + + if (treeVariants <= 0) { + return BiomePlaceables.Tree(ImmutableList.of()) + } + + val trees = ArrayList() + + for (i in 0 until treeVariants) { + val subTreeVariants = biome.random.nextRange(subVariantsRange) + if (subTreeVariants <= 0) continue + val (stem, foliage) = pairs.random(biome.random) + + val treeStemHueShift = treeStemHueShiftMax * biome.random.nextDouble(-1.0, 1.0) + val treeFoliageHueShift = treeFoliageHueShiftMax * biome.random.nextDouble(-1.0, 1.0) + + if (foliage == null) { + for (i2 in 0 until subTreeVariants) { + trees.add(TreeVariant.create(stem, if (sameStemHueShift) treeStemHueShift else treeStemHueShiftMax * biome.random.nextDouble(-1.0, 1.0))) + } + } else { + for (i2 in 0 until subTreeVariants) { + trees.add(TreeVariant.create( + stem, if (sameStemHueShift) treeStemHueShift else treeStemHueShiftMax * biome.random.nextDouble(-1.0, 1.0), + foliage, if (sameFoliageHueShift) treeFoliageHueShift else treeFoliageHueShiftMax * biome.random.nextDouble(-1.0, 1.0) + )) + } + } + } + + return BiomePlaceables.Tree(ImmutableList.copyOf(trees)) + } + } + + @JsonFactory + data class BushData( + val name: Registry.Ref, + val baseHueShiftMax: Double = 0.0, + val modHueShiftMax: Double = 0.0, + ) + + @JsonFactory + data class Bush(val bushes: ImmutableSet = ImmutableSet.of()) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.BUSH + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + val valid = bushes.stream().filter { it.name.entry != null }.toList() + + if (valid.isEmpty()) { + throw NoSuchElementException("None of bush variants are valid (candidates: $bushes)") + } + + val (name, baseHueShiftMax, modHueShiftMax) = valid.random(biome.random) + val data = name.entry!!.value + val mod = if (data.mods.isNotEmpty()) data.mods.random(biome.random) else "" + + return BiomePlaceables.Bush(BushVariant.create(name.entry!!, biome.random.nextDouble(-1.0, 1.0) * baseHueShiftMax, mod, biome.random.nextDouble(-1.0 * 1.0) * modHueShiftMax)) + } + } + + @JsonFactory + data class ObjectPool(val pool: ImmutableList> = ImmutableList.of(), val parameters: JsonElement = JsonObject()) + + @JsonFactory + data class Object(val objectSets: ImmutableList) : DistributionItemData() { + override val type: BiomePlacementItemType + get() = BiomePlacementItemType.OBJECT + + override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { + if (objectSets.isEmpty()) { + return BiomePlaceables.Object(WeightedList()) + } + + val rand = objectSets.random(biome.random) + return BiomePlaceables.Object(WeightedList(rand.pool.stream().map { it.first to (it.second to rand.parameters) }.collect(ImmutableList.toImmutableList()))) + } + } + + // --------------- DISTRIBUTION + abstract class DistributionData { + abstract val type: BiomePlacementDistributionType + abstract fun create(self: DistributionItem, biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionData + } + + @JsonSingleton + object RandomDistribution : DistributionData() { + override val type: BiomePlacementDistributionType + get() = BiomePlacementDistributionType.RANDOM + + override fun create( + self: DistributionItem, + biome: BiomeDefinition.CreationParams + ): BiomePlaceables.DistributionData { + return BiomePlaceables.RandomDistribution( + blockSeed = biome.random.nextLong(), + randomItems = IntStream.range(0, self.variants) + .mapToObj { self.data.create(biome) } + .collect(ImmutableList.toImmutableList()) + ) + } + } + + @JsonFactory + data class PeriodicDistribution( + val modulus: Int = 1, + val octaves: Int = 1, + val alpha: Double = 2.0, + val beta: Double = 2.0, + val modulusVariance: Double = 0.0, + val densityPeriod: Double = 10.0, + val densityOffset: Double = 2.0, + val typePeriod: Double = 10.0, + val noiseType: PerlinNoiseParameters.Type = PerlinNoiseParameters.Type.PERLIN, + val noiseScale: Int = PerlinNoiseParameters.DEFAULT_SCALE, + ) : DistributionData() { + override val type: BiomePlacementDistributionType + get() = BiomePlacementDistributionType.PERIODIC + + override fun create( + self: DistributionItem, + biome: BiomeDefinition.CreationParams + ): BiomePlaceables.DistributionData { + val modulusOffset = if (modulus == 0) 0 else biome.random.nextInt(-modulus, modulus) + + return BiomePlaceables.PeriodicDistribution( + modulus = modulus, + modulusOffset = modulusOffset, + + densityFunction = AbstractPerlinNoise.of( + PerlinNoiseParameters( + type = noiseType, + scale = noiseScale, + octaves = octaves, + alpha = alpha, + beta = beta, + + frequency = 1.0 / densityPeriod, + amplitude = 1.0, + bias = densityOffset, + seed = biome.random.nextLong() + ) + ), + + modulusDistortion = AbstractPerlinNoise.of( + PerlinNoiseParameters( + type = noiseType, + scale = noiseScale, + octaves = octaves, + alpha = alpha, + beta = beta, + + frequency = 1.0 / modulus, + amplitude = modulusVariance, + bias = modulusVariance * 2.0, + seed = biome.random.nextLong() + ) + ), + + weightedItems = IntStream.range(0, self.variants) + .mapToObj { self.data.create(biome) } + .map { it to AbstractPerlinNoise.of( + PerlinNoiseParameters( + type = noiseType, + scale = noiseScale, + octaves = octaves, + alpha = alpha, + beta = beta, + + frequency = 1.0 / typePeriod, + amplitude = 1.0, + bias = 0.0, + seed = biome.random.nextLong(), + ) + ) } + .collect(ImmutableList.toImmutableList()) + ) + } + } + + fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables { + return BiomePlaceables( + grassMod = grassMod.random(biome.random) { null }?.entry, + ceilingGrassMod = ceilingGrassMod.random(biome.random) { null }?.entry, + grassModDensity = grassModDensity, + ceilingGrassModDensity = ceilingGrassModDensity, + items = items.stream() + .map { it.create(biome) } + .collect(ImmutableList.toImmutableList()) + ) + } +} + +@JsonFactory +data class BiomeDefinition( + val airless: Boolean = false, + val name: String, + val statusEffects: ImmutableSet = ImmutableSet.of(), + val weather: ImmutableList>>>> = ImmutableList.of(), // binned reference to other assets + val hueShiftOptions: ImmutableList = ImmutableList.of(), + val skyOptions: ImmutableList = ImmutableList.of(), + val description: String = "...", + val mainBlock: Registry.Ref? = null, + val subBlocks: ImmutableList>? = null, + val ores: Registry.Ref? = null, + val musicTrack: AmbientNoisesDefinition? = null, + val ambientNoises: AmbientNoisesDefinition? = null, + val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), + val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(), + val parallax: AssetReference? = null, +) { + data class CreationParams( + val hueShift: Double, + val random: RandomGenerator + ) + + fun skyColoring(random: RandomGenerator): SkyColoring { + if (skyOptions.isEmpty()) + return SkyColoring.BLACK + + return skyOptions.random(random) + } + + fun hueShift(random: RandomGenerator): Double { + if (hueShiftOptions.isEmpty()) + return 0.0 + + return hueShiftOptions.random(random) + } + + fun create(random: RandomGenerator, verticalMidPoint: Int, threatLevel: Double): Biome { + val hueShift = hueShift(random) + val data = CreationParams(hueShift = hueShift, random = random) + val surfacePlaceables = surfacePlaceables.create(data) + val undergroundPlaceables = undergroundPlaceables.create(data) + + return Biome( + baseName = name, + description = description, + hueShift = hueShift, + + mainBlock = mainBlock?.entry, + subBlocks = subBlocks?.stream()?.map { it.entry }?.filterNotNull()?.collect(ImmutableList.toImmutableList()) ?: ImmutableList.of(), + + musicTrack = musicTrack, + ambientNoises = ambientNoises, + surfacePlaceables = surfacePlaceables, + undergroundPlaceables = undergroundPlaceables, + + parallax = parallax?.value?.create(random, verticalMidPoint.toDouble(), hueShift, surfacePlaceables.firstTreeVariant()), + + ores = (ores?.value?.evaluate(threatLevel, oresAdapter)?.map { + it.stream() + .filter { it.second > 0.0 } + .map { Registries.tileModifiers[it.first] to it.second } + .filter { it.first != null } + .collect(ImmutableList.toImmutableList()) + }?.orElse(ImmutableList.of()) ?: ImmutableList.of()) as ImmutableList, Double>> + ) + } + + companion object { + private val oresAdapter by lazy { + Starbound.gson.pairListAdapter() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/ConstantTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/ConstantTerrainSelector.kt new file mode 100644 index 00000000..6c5439eb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/ConstantTerrainSelector.kt @@ -0,0 +1,15 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +class ConstantTerrainSelector(name: String, data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(name, data, parameters) { + @JsonFactory + data class Data(val value: Double) + + override val type: TerrainSelectorType + get() = TerrainSelectorType.CONSTANT + + override fun get(x: Int, y: Int): Double { + return config.value + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/DisplacementTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/DisplacementTerrainSelector.kt new file mode 100644 index 00000000..608b6ff0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/DisplacementTerrainSelector.kt @@ -0,0 +1,116 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise +import kotlin.math.roundToInt + +class DisplacementTerrainSelector(name: String, data: Data, parameters: TerrainSelectorParameters) : AbstractTerrainSelector(name, data, parameters) { + @JsonFactory + data class Data( + val xType: PerlinNoiseParameters.Type, + val xScale: Int = 512, + val xOctaves: Int, + val xFreq: Double, + val xAmp: Double, + val xBias: Double = 0.0, + val xAlpha: Double = 2.0, + val xBeta: Double = 2.0, + + val yType: PerlinNoiseParameters.Type, + val yScale: Int = 512, + val yOctaves: Int, + val yFreq: Double, + val yAmp: Double, + val yBias: Double = 0.0, + val yAlpha: Double = 2.0, + val yBeta: Double = 2.0, + + val xXInfluence: Double = 1.0, + val xYInfluence: Double = 1.0, + val yXInfluence: Double = 1.0, + val yYInfluence: Double = 1.0, + + val yClamp: Vector2d? = null, + val yClampSmoothing: Double = 0.0, + + val xClamp: Vector2d? = null, + val xClampSmoothing: Double = 0.0, + + val source: JsonObject, + ) + + val xFn: AbstractPerlinNoise + val yFn: AbstractPerlinNoise + val source: AbstractTerrainSelector<*> + + init { + // This allows to have multiple nested displacement selectors with different seeds + // original engine isn't capable of this because nested selectors will have the same seed + val parameters = parameters.withRandom() + val random = parameters.random() + + xFn = AbstractPerlinNoise.of(PerlinNoiseParameters( + type = data.xType, + scale = data.xScale, + octaves = data.xOctaves, + frequency = data.xFreq, + amplitude = data.xAmp, + bias = data.xBias, + alpha = data.xAlpha, + beta = data.xBeta, + )) + + yFn = AbstractPerlinNoise.of(PerlinNoiseParameters( + type = data.yType, + scale = data.yScale, + octaves = data.yOctaves, + frequency = data.yFreq, + amplitude = data.yAmp, + bias = data.yBias, + alpha = data.yAlpha, + beta = data.yBeta, + )) + + xFn.init(random.nextLong()) + yFn.init(random.nextLong()) + source = TerrainSelectorType.create(data.source, parameters) + } + + override fun get(x: Int, y: Int): Double { + return source[clampX(xFn[x * config.xXInfluence, y * config.xYInfluence]).roundToInt(), clampY(yFn[x * config.yXInfluence, y * config.yYInfluence]).roundToInt()] + } + + private fun clampX(v: Double): Double { + if (config.xClamp == null) + return v + + if (config.xClampSmoothing == 0.0) + return v.coerceIn(config.xClamp.x, config.xClamp.y) + + return 0.2 * ((v - config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) + + (v - 0.5 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) + + (v).coerceIn(config.xClamp.x, config.xClamp.y) + + (v + 0.5 * config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y) + + (v + config.xClampSmoothing).coerceIn(config.xClamp.x, config.xClamp.y)) + } + + private fun clampY(v: Double): Double { + if (config.yClamp == null) + return v + + if (config.xClampSmoothing == 0.0) + return v.coerceIn(config.yClamp.x, config.yClamp.y) + + return 0.2 * ((v - config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) + + (v - 0.5 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) + + (v).coerceIn(config.yClamp.x, config.yClamp.y) + + (v + 0.5 * config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y) + + (v + config.yClampSmoothing).coerceIn(config.yClamp.x, config.yClamp.y)) + } + + override val type: TerrainSelectorType + get() = TerrainSelectorType.DISPLACEMENT +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorFactory.kt new file mode 100644 index 00000000..54395857 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorFactory.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +class TerrainSelectorFactory>(val name: String, private val data: D, private val factory: (String, D, TerrainSelectorParameters) -> T) { + fun create(parameters: TerrainSelectorParameters): T { + return factory(name, data, parameters) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorParameters.kt new file mode 100644 index 00000000..f807c761 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorParameters.kt @@ -0,0 +1,31 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.util.random.RandomGenerator + +@JsonFactory +data class TerrainSelectorParameters( + val worldWidth: Int, + val baseHeight: Double, + val seed: Long = 0L, + val commonality: Double = 0.0 +) { + fun withSeed(seed: Long) = copy(seed = seed) + fun withCommonality(commonality: Double) = copy(commonality = commonality) + private var random: RandomGenerator? = null + + fun random(): RandomGenerator { + return random ?: ru.dbotthepony.kstarbound.util.random.random(seed) + } + + fun withRandom(): TerrainSelectorParameters { + if (random == null) + return withRandom(ru.dbotthepony.kstarbound.util.random.random(seed)) + + return this + } + + fun withRandom(randomGenerator: RandomGenerator): TerrainSelectorParameters { + return copy().also { it.random = randomGenerator } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt new file mode 100644 index 00000000..f052027a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/terrain/TerrainSelectorType.kt @@ -0,0 +1,75 @@ +package ru.dbotthepony.kstarbound.defs.world.terrain + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +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.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Starbound + +fun createNamedTerrainSelector(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { + return Registries.terrainSelectors.getOrThrow(name).value.create(parameters) +} + +enum class TerrainSelectorType(val jsonName: String) { + CONSTANT("constant"), + DISPLACEMENT("displacement"), + ; + + companion object : TypeAdapter>(), TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (AbstractTerrainSelector::class.java.isAssignableFrom(type.rawType)) { + return this as TypeAdapter + } + + return null + } + + override fun write(out: JsonWriter, value: AbstractTerrainSelector<*>?) { + if (value == null) + out.nullValue() + else + out.value(value.toJson()) + } + + private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } + + override fun read(`in`: JsonReader): AbstractTerrainSelector<*>? { + if (`in`.consumeNull()) + return null + + return create(objects.read(`in`)) + } + + fun createFactory(json: JsonObject): TerrainSelectorFactory<*, *> { + val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' element of terrain json") + val type = json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json") + + when (type) { + CONSTANT.jsonName -> { + return TerrainSelectorFactory(name, Starbound.gson.fromJson(json, ConstantTerrainSelector.Data::class.java), ::ConstantTerrainSelector) + } + + DISPLACEMENT.jsonName -> { + return TerrainSelectorFactory(name, Starbound.gson.fromJson(json, DisplacementTerrainSelector.Data::class.java), ::DisplacementTerrainSelector) + } + + else -> throw IllegalArgumentException("Unknown terrain selector type $type") + } + } + + fun create(json: JsonObject): AbstractTerrainSelector<*> { + return createFactory(json).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java)) + } + + fun create(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> { + return createFactory(json).create(parameters) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index 3cc37131..7ac855af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -4,11 +4,14 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.io.readDouble +import ru.dbotthepony.kommons.io.readFloat import ru.dbotthepony.kommons.io.readLong import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.writeDouble +import ru.dbotthepony.kommons.io.writeFloat import ru.dbotthepony.kommons.io.writeLong import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.world.ChunkPos @@ -40,3 +43,14 @@ fun InputStream.readHeader(header: String) { fun InputStream.readChunkPos(): ChunkPos { return ChunkPos(readSignedVarInt(), readSignedVarInt()) } + +fun OutputStream.writeColor(color: RGBAColor) { + writeFloat(color.red) + writeFloat(color.green) + writeFloat(color.blue) + writeFloat(color.alpha) +} + +fun InputStream.readColor(): RGBAColor { + return RGBAColor(readFloat(), readFloat(), readFloat(), readFloat()) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt index 3781ff26..8afec70f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/InternedJsonElementAdapter.kt @@ -12,6 +12,7 @@ import com.google.gson.internal.bind.TypeAdapters import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull class InternedJsonElementAdapter(val stringInterner: Interner) : TypeAdapter() { override fun write(out: JsonWriter, value: JsonElement?) { @@ -36,7 +37,7 @@ class InternedJsonElementAdapter(val stringInterner: Interner) : TypeAda } override fun read(`in`: JsonReader): JsonObject? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null val output = JsonObject() @@ -53,7 +54,7 @@ class InternedJsonElementAdapter(val stringInterner: Interner) : TypeAda } override fun read(`in`: JsonReader): JsonArray? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null val output = JsonArray() @@ -78,7 +79,7 @@ class InternedStringAdapter(val stringInterner: Interner) : TypeAdapter< } override fun read(`in`: JsonReader): String? { - if (`in`.peek() == JsonToken.NULL) + if (`in`.consumeNull()) return null if (`in`.peek() == JsonToken.BOOLEAN) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt index 952cb26e..875fb4f3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/JsonUtils.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.json 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 @@ -21,6 +22,10 @@ inline fun , reified E> Gson.collectionAdapter(): Type return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter } +inline fun Gson.mapAdapter(): TypeAdapter> { + return getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, K::class.java, V::class.java)) as TypeAdapter> +} + inline fun Gson.listAdapter(): TypeAdapter> { return collectionAdapter() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/KotlinAdapters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/KotlinAdapters.kt index 5210fcdf..f26dc72a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/KotlinAdapters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/KotlinAdapters.kt @@ -4,6 +4,7 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull object LongRangeAdapter : TypeAdapter() { override fun write(out: JsonWriter, value: LongRange?) { @@ -18,7 +19,7 @@ object LongRangeAdapter : TypeAdapter() { } override fun read(`in`: JsonReader): LongRange? { - if (`in`.peek() == JsonToken.NULL) { + if (`in`.consumeNull()) { return null } else { `in`.beginArray() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt index 2f758af9..bbbb590e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt @@ -65,7 +65,6 @@ annotation class JsonBuilder @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) annotation class JsonFactory( - val storesJson: Boolean = false, val asList: Boolean = false, val logMisses: Boolean = false, ) @@ -89,6 +88,10 @@ annotation class JsonFactory( @Retention(AnnotationRetention.RUNTIME) annotation class JsonImplementation(val implementingClass: KClass<*>) +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class JsonSingleton + object JsonImplementationTypeFactory : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { val delegate = type.rawType.getAnnotation(JsonImplementation::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt new file mode 100644 index 00000000..520a9a61 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/DispatchingAdapter.kt @@ -0,0 +1,84 @@ +package ru.dbotthepony.kstarbound.json.builder + +import com.google.gson.Gson +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +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.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.value + +inline fun DispatchingAdapter( + key: String, + noinline value2type: C.() -> E, + noinline type2value: E.() -> TypeToken, + elements: Collection, +): DispatchingAdapter { + return DispatchingAdapter(key, value2type, type2value, elements, E::class.java, C::class.java) +} + +class DispatchingAdapter( + val key: String, + val value2type: ELEMENT.() -> TYPE, + val type2value: TYPE.() -> TypeToken, + val types: Collection, + val typeClass: Class, + val baseValueClass: Class +) : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType == baseValueClass) { + return Impl(gson) as TypeAdapter + } + + return null + } + + 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) + out.nullValue() + else { + val type = value2type(value) + val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type") + + out.beginObject() + out.name(key) + typeAdapter.write(out, type) + + when (val result = adapter.toJsonTree(value)) { + JsonNull.INSTANCE -> {} // do nothing, probably singleton value + + // TODO: this is a considerable bottleneck, need proxied json writer which will hook right before top-most endObject() + is JsonObject -> { + for ((k, v) in result.entrySet()) { + out.name(k) + out.value(v) + } + } + + else -> throw JsonSyntaxException("Expected JSON Object or JSON NULL, got ${result::class.java}") + } + + out.endObject() + } + } + + override fun read(`in`: JsonReader): ELEMENT? { + if (`in`.consumeNull()) + return null + + val read = obj.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/EnumAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt index 4909eaf4..4ed7da62 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt @@ -14,8 +14,9 @@ import com.google.gson.stream.JsonWriter import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.util.sbIntern -import java.util.Arrays +import java.util.* import java.util.stream.Stream import kotlin.reflect.KClass import kotlin.reflect.full.isSuperclassOf @@ -25,27 +26,22 @@ interface IStringSerializable { fun write(out: JsonWriter) } -@Suppress("FunctionName") -inline fun >EnumAdapter(values: Stream = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter { +inline fun > EnumAdapter(values: Stream = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter { return EnumAdapter(T::class, values, default) } -@Suppress("FunctionName") -inline fun >EnumAdapter(values: Iterator, default: T? = null): EnumAdapter { +inline fun > EnumAdapter(values: Iterator, default: T? = null): EnumAdapter { return EnumAdapter(T::class, Streams.stream(values), default) } -@Suppress("FunctionName") -inline fun >EnumAdapter(values: Array, default: T? = null): EnumAdapter { +inline fun > EnumAdapter(values: Array, default: T? = null): EnumAdapter { return EnumAdapter(T::class, Arrays.stream(values), default) } -@Suppress("FunctionName") -inline fun >EnumAdapter(values: Collection, default: T? = null): EnumAdapter { +inline fun > EnumAdapter(values: Collection, default: T? = null): EnumAdapter { return EnumAdapter(T::class, values.stream(), default) } -@Suppress("name_shadowing") class EnumAdapter>(private val enum: KClass, values: Stream = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter() { constructor(clazz: Class, values: Stream = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default) @@ -60,7 +56,7 @@ class EnumAdapter>(private val enum: KClass, values: Stream = private val values = values.collect(ImmutableList.toImmutableList()) private val mapping: ImmutableMap private val areCustom = IStringSerializable::class.java.isAssignableFrom(enum.java) - private val misses = ObjectOpenHashSet() + private val misses = Collections.synchronizedSet(ObjectOpenHashSet()) init { val builder = Object2ObjectArrayMap() @@ -88,6 +84,8 @@ class EnumAdapter>(private val enum: KClass, values: Stream = override fun write(out: JsonWriter, value: T?) { if (value == null) { out.nullValue() + } else if (value is IStringSerializable) { + value.write(out) } else { out.value(value.name) } @@ -95,7 +93,7 @@ class EnumAdapter>(private val enum: KClass, values: Stream = @Suppress("unchecked_cast") override fun read(`in`: JsonReader): T? { - if (`in`.peek() == JsonToken.NULL) { + if (`in`.consumeNull()) { return null } 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 558e671f..c9a64dfd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/FactoryAdapter.kt @@ -17,6 +17,7 @@ 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 it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap @@ -48,7 +49,6 @@ class FactoryAdapter private constructor( val types: ImmutableList>, aliases: Map>, val asJsonArray: Boolean, - val storesJson: Boolean, val stringInterner: Interner, val logMisses: Boolean, private val elements: TypeAdapter @@ -78,10 +78,7 @@ class FactoryAdapter private constructor( * Обычный конструктор класса (без флагов "значения по умолчанию") */ private val regularFactory: KFunction = clazz.constructors.firstOrNull first@{ - var requiredSize = types.size - - if (storesJson) - requiredSize++ + val requiredSize = types.size if (it.parameters.size == requiredSize) { val iterator = types.iterator() @@ -99,20 +96,6 @@ class FactoryAdapter private constructor( } } - if (storesJson) { - val nextParam = factoryIterator.next() - - if (asJsonArray) { - if (!(nextParam.type.classifier as KClass<*>).isSubclassOf(List::class)) { - return@first false - } - } else { - if (!(nextParam.type.classifier as KClass<*>).isSubclassOf(Map::class)) { - return@first false - } - } - } - return@first true } @@ -125,12 +108,6 @@ class FactoryAdapter private constructor( private val syntheticFactory: Constructor? = try { val typelist = types.map { (it.type.classifier as KClass<*>).java }.toMutableList() - if (storesJson) - if (asJsonArray) - typelist.add(List::class.java) - else - typelist.add(Map::class.java) - for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1)) typelist.add(Int::class.java) @@ -141,7 +118,7 @@ class FactoryAdapter private constructor( null } - private val syntheticPrimitives = Int2ObjectOpenHashMap() + private val syntheticPrimitives = Int2ObjectAVLTreeMap() init { if (syntheticFactory != null) { @@ -201,27 +178,14 @@ class FactoryAdapter private constructor( return null // таблица присутствия значений (если значение true то на i было значение внутри json) - val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0)) - var readValues = arrayOfNulls(types.size + (if (storesJson) 1 else 0)) - - if (storesJson) - presentValues[presentValues.size - 1] = true + val presentValues = BooleanArray(types.size) + var readValues = arrayOfNulls(types.size) @Suppress("name_shadowing") var reader = reader // Если нам необходимо читать объект как набор данных массива, то давай if (asJsonArray) { - if (storesJson) { - val readArray = elements.read(reader) - - if (readArray !is JsonArray) - throw JsonParseException("Expected JSON element to be an Array, ${readArray::class.qualifiedName} given") - - reader = JsonTreeReader(readArray) - readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray, stringInterner) as List, stringInterner) - } - reader.beginArray() val iterator = types.iterator() var fieldId = 0 @@ -230,7 +194,7 @@ class FactoryAdapter private constructor( if (!iterator.hasNext()) { val name = fieldId.toString() - if (!storesJson && logMisses && loggedMisses.add(name)) { + if (logMisses && loggedMisses.add(name)) { LOGGER.warn("${clazz.qualifiedName} has no property for storing $name") } @@ -255,7 +219,7 @@ class FactoryAdapter private constructor( } else { var json: JsonObject by Delegates.notNull() - if (storesJson || types.any { it.isFlat }) { + if (types.any { it.isFlat }) { val readMap = elements.read(reader) if (readMap !is JsonObject) @@ -263,9 +227,6 @@ class FactoryAdapter private constructor( json = readMap reader = JsonTreeReader(readMap) - - if (storesJson) - readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap, stringInterner) as Map, stringInterner) } reader.beginObject() @@ -275,7 +236,7 @@ class FactoryAdapter private constructor( val fields = name2index[name] if (fields == null || fields.size == 0) { - if (!storesJson && logMisses && loggedMisses.add(name)) { + if (logMisses && loggedMisses.add(name)) { LOGGER.warn("${clazz.qualifiedName} has no property for storing $name") } @@ -393,7 +354,7 @@ class FactoryAdapter private constructor( if (readValues[i] != null) continue val param = regularFactory.parameters[i] - if (param.isOptional && !presentValues[i]) { + if (param.isOptional && (!presentValues[i] || readValues[i] == null && i in syntheticPrimitives)) { readValues[i] = syntheticPrimitives[i] } else if (!param.isOptional) { if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing") @@ -401,7 +362,11 @@ class FactoryAdapter private constructor( } } - return syntheticFactory.newInstance(*readValues) + try { + return syntheticFactory.newInstance(*readValues) + } catch (err: Throwable) { + throw JsonSyntaxException("Failed to instance $clazz", err) + } } } @@ -440,7 +405,6 @@ class FactoryAdapter private constructor( clazz = clazz, types = ImmutableList.copyOf(types.also { it.forEach{ it.resolve(gson) } }), asJsonArray = asList, - storesJson = storesJson, stringInterner = stringInterner, aliases = aliases, logMisses = logMisses, @@ -448,19 +412,6 @@ class FactoryAdapter private constructor( ) } - /** - * Принимает ли класс *последним* аргументом JSON структуру - * - * На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList] - * - * Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] последним аргументом, - * иначе поиск конструктора завершится неудачей - */ - fun storesJson(flag: Boolean = true): Builder { - storesJson = flag - return this - } - fun add(field: KProperty1, isFlat: Boolean = false, isMarkedNullable: Boolean? = null): Builder { types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable)) return this @@ -517,7 +468,6 @@ class FactoryAdapter private constructor( builder.inputAsList() } - builder.storesJson(config.storesJson) builder.stringInterner = stringInterner builder.logMisses = config.logMisses @@ -528,7 +478,7 @@ class FactoryAdapter private constructor( val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}") val params = foundConstructor.parameters - val lastIndex = if (config.storesJson) params.size - 1 else params.size + val lastIndex = params.size for (i in 0 until lastIndex) { val argument = params[i] diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/SingletonTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/SingletonTypeAdapter.kt new file mode 100644 index 00000000..6fa0e7de --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/SingletonTypeAdapter.kt @@ -0,0 +1,15 @@ +package ru.dbotthepony.kstarbound.json.builder + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter + +class SingletonTypeAdapter(val instance: V) : TypeAdapter() { + override fun write(out: JsonWriter, value: V) { + out.nullValue() + } + + override fun read(`in`: JsonReader): V { + return instance + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListAdapterFactory.kt deleted file mode 100644 index c9448268..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListAdapterFactory.kt +++ /dev/null @@ -1,17 +0,0 @@ -package ru.dbotthepony.kstarbound.json.factory - -import com.google.gson.Gson -import com.google.gson.TypeAdapter -import com.google.gson.TypeAdapterFactory -import com.google.gson.reflect.TypeToken -import java.lang.reflect.ParameterizedType - -object ArrayListAdapterFactory : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (ArrayList::class.java.isAssignableFrom(type.rawType) && type.type is ParameterizedType) { - return ArrayListTypeAdapter(gson.getAdapter(TypeToken.get((type.type as ParameterizedType).actualTypeArguments[0]))) as TypeAdapter - } - - return null - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListTypeAdapter.kt deleted file mode 100644 index e2c898c0..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ArrayListTypeAdapter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package ru.dbotthepony.kstarbound.json.factory - -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter -import java.util.ArrayList - -class ArrayListTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdapter>() { - override fun write(out: JsonWriter, value: ArrayList?) { - if (value == null) { - out.nullValue() - return - } - - if (value.size == 1) { - elementAdapter.write(out, value[0]) - return - } - - out.beginArray() - - for (v in value) { - elementAdapter.write(out, v) - } - - out.endArray() - } - - override fun read(reader: JsonReader): ArrayList? { - if (reader.peek() == JsonToken.NULL) - return null - - if (reader.peek() != JsonToken.BEGIN_ARRAY) { - // не массив, возможно упрощение структуры "a": [value] -> "a": value - val list = ArrayList(1) - list.add(elementAdapter.read(reader)) - return list - } - - reader.beginArray() - - val list = ArrayList() - - while (reader.peek() != JsonToken.END_ARRAY) { - list.add(elementAdapter.read(reader)) - } - - reader.endArray() - - return list - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt new file mode 100644 index 00000000..ba730e77 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/CollectionAdapterFactory.kt @@ -0,0 +1,71 @@ +package ru.dbotthepony.kstarbound.json.factory + +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 it.unimi.dsi.fastutil.doubles.DoubleArrayList +import it.unimi.dsi.fastutil.ints.IntArrayList +import it.unimi.dsi.fastutil.longs.LongArrayList +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.gson.consumeNull +import java.lang.reflect.ParameterizedType + +object CollectionAdapterFactory : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.type is ParameterizedType) { + val parentType = TypeToken.get((type.type as ParameterizedType).actualTypeArguments[0]) as TypeToken + + return when (type.rawType) { + ArrayList::class.java -> Adapter, Any>(::ArrayList, gson.getAdapter(parentType)) + List::class.java -> Adapter, Any>(::ArrayList, gson.getAdapter(parentType)) + Set::class.java -> Adapter, Any>(::ObjectOpenHashSet, gson.getAdapter(parentType)) + ObjectOpenHashSet::class.java -> Adapter, Any>(::ObjectOpenHashSet, gson.getAdapter(parentType)) + else -> null + } as TypeAdapter? + } else { + return when (type.rawType) { + IntArrayList::class.java -> Adapter(::IntArrayList, gson.getAdapter(Int::class.java)) + LongArrayList::class.java -> Adapter(::LongArrayList, gson.getAdapter(Long::class.java)) + DoubleArrayList::class.java -> Adapter(::DoubleArrayList, gson.getAdapter(Double::class.java)) + else -> null + } as TypeAdapter? + } + } + + private class Adapter, E>(val factory: () -> C, val parent: TypeAdapter) : TypeAdapter() { + override fun write(out: JsonWriter, value: C?) { + if (value == null) + out.nullValue() + else { + out.beginArray() + for (v in value) parent.write(out, v) + out.endArray() + } + } + + override fun read(`in`: JsonReader): C? { + if (`in`.consumeNull()) + return null + + val output = factory() + + if (`in`.peek() == JsonToken.BEGIN_ARRAY) { + `in`.beginArray() + + while (`in`.hasNext()) { + output.add(parent.read(`in`)) + } + + `in`.endArray() + } else { + output.add(parent.read(`in`)) + } + + return output + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt index 87d13a98..dd6a2114 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableArrayMapTypeAdapter.kt @@ -7,6 +7,7 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull class ImmutableArrayMapTypeAdapter(val keyAdapter: TypeAdapter, val elementAdapter: TypeAdapter) : TypeAdapter>() { override fun write(out: JsonWriter, value: ImmutableMap?) { @@ -26,7 +27,7 @@ class ImmutableArrayMapTypeAdapter(val keyAdapter: TypeAdapter, val ele } override fun read(reader: JsonReader): ImmutableMap? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null reader.beginArray() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableListTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableListTypeAdapter.kt index 3c3fb0bc..09f5c10d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableListTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableListTypeAdapter.kt @@ -6,6 +6,7 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull class ImmutableListTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdapter>() { override fun write(out: JsonWriter, value: ImmutableList?) { @@ -14,11 +15,6 @@ class ImmutableListTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdap return } - if (value.size == 1) { - elementAdapter.write(out, value[0]) - return - } - out.beginArray() for (v in value) { @@ -29,14 +25,9 @@ class ImmutableListTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdap } override fun read(reader: JsonReader): ImmutableList? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null - if (reader.peek() != JsonToken.BEGIN_ARRAY) { - // не массив, возможно упрощение структуры "a": [value] -> "a": value - return ImmutableList.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls")) - } - reader.beginArray() val builder = ImmutableList.Builder() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableMapTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableMapTypeAdapter.kt index e66582bc..a3412563 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableMapTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableMapTypeAdapter.kt @@ -8,6 +8,7 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull class ImmutableMapTypeAdapter(val stringInterner: Interner, val elementAdapter: TypeAdapter) : TypeAdapter>() { override fun write(out: JsonWriter, value: ImmutableMap?) { @@ -27,7 +28,7 @@ class ImmutableMapTypeAdapter(val stringInterner: Interner, val eleme } override fun read(reader: JsonReader): ImmutableMap? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null if (reader.peek() == JsonToken.BEGIN_ARRAY) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableSetTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableSetTypeAdapter.kt index 18de1d0f..12f7a598 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableSetTypeAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/ImmutableSetTypeAdapter.kt @@ -7,6 +7,7 @@ import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull class ImmutableSetTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdapter>() { override fun write(out: JsonWriter, value: ImmutableSet?) { @@ -15,11 +16,6 @@ class ImmutableSetTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdapt return } - if (value.size == 1) { - elementAdapter.write(out, value.first()) - return - } - out.beginArray() for (v in value) { @@ -30,14 +26,9 @@ class ImmutableSetTypeAdapter(val elementAdapter: TypeAdapter) : TypeAdapt } override fun read(reader: JsonReader): ImmutableSet? { - if (reader.peek() == JsonToken.NULL) + if (reader.consumeNull()) return null - if (reader.peek() != JsonToken.BEGIN_ARRAY) { - // не массив, возможно упрощение структуры "a": [value] -> "a": value - return ImmutableSet.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls")) - } - reader.beginArray() val builder = ImmutableSet.Builder() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/MapsTypeAdapterFactory.kt similarity index 96% rename from src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/MapsTypeAdapterFactory.kt index 4e762b2c..e5b1cdd0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/FastutilTypeAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/MapsTypeAdapterFactory.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.json +package ru.dbotthepony.kstarbound.json.factory import com.github.benmanes.caffeine.cache.Interner import com.google.gson.Gson @@ -11,7 +11,7 @@ import it.unimi.dsi.fastutil.objects.* import ru.dbotthepony.kommons.gson.consumeNull import java.lang.reflect.ParameterizedType -class FastutilTypeAdapterFactory(private val interner: Interner) : TypeAdapterFactory { +class MapsTypeAdapterFactory(private val interner: Interner) : TypeAdapterFactory { private fun map1(gson: Gson, type: TypeToken<*>, typeValue: TypeToken<*>, factoryHash: () -> Map, factoryTree: () -> Map): TypeAdapter>? { val p = type.type as? ParameterizedType ?: return null val typeKey = TypeToken.get(p.actualTypeArguments[0]) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/RGBAColorTypeAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/RGBAColorTypeAdapter.kt new file mode 100644 index 00000000..5c9592a6 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/RGBAColorTypeAdapter.kt @@ -0,0 +1,44 @@ +package ru.dbotthepony.kstarbound.json.factory + +import com.google.gson.TypeAdapter +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.kommons.math.RGBAColor + +// because despite description of "jsonToColor" in StarJsonExtra.hpp, they DO NOT support +// floats, color is always defined with 0-255 int components +object RGBAColorTypeAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: RGBAColor?) { + if (value == null) + out.nullValue() + else { + out.beginArray() + + out.value(value.redInt) + out.value(value.greenInt) + out.value(value.blueInt) + if (value.alphaInt != 255) out.value(value.alphaInt) + + out.endArray() + } + } + + override fun read(`in`: JsonReader): RGBAColor? { + if (`in`.consumeNull()) + return null + + `in`.beginArray() + + val red = `in`.nextInt() + val green = `in`.nextInt() + val blue = `in`.nextInt() + val alpha = `in`.peek().let { if (it == JsonToken.END_ARRAY) 255 else `in`.nextInt() } + + val result = RGBAColor(red, green, blue, alpha) + + `in`.endArray() + return result + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/SingletonTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/SingletonTypeAdapterFactory.kt new file mode 100644 index 00000000..6dd2cf51 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/factory/SingletonTypeAdapterFactory.kt @@ -0,0 +1,27 @@ +package ru.dbotthepony.kstarbound.json.factory + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import ru.dbotthepony.kstarbound.json.builder.JsonSingleton +import ru.dbotthepony.kstarbound.json.builder.SingletonTypeAdapter + +object SingletonTypeAdapterFactory : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + val raw = type.rawType + + if (raw.isAnnotationPresent(JsonSingleton::class.java)) { + val findInstance = raw.getDeclaredField("INSTANCE") + val f = findInstance.get(null) ?: throw NullPointerException("INSTANCE field is null") + + if (!raw.isAssignableFrom(f::class.java)) { + throw ClassCastException("${f::class.java} can not be assigned to $raw") + } + + return SingletonTypeAdapter(f) as TypeAdapter + } + + return null + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt index c1ade3e3..00ea102a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/RootBindings.kt @@ -247,7 +247,7 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument } private fun materialHealth(context: ExecutionContext, arguments: ArgumentIterator) { - context.returnBuffer.setTo(lookupStrict(Registries.tiles, arguments.nextAny()).value.damageConfig.health) + context.returnBuffer.setTo(lookupStrict(Registries.tiles, arguments.nextAny()).value.damageConfig.totalHealth) } private fun liquidName(context: ExecutionContext, arguments: ArgumentIterator) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 08252527..a320f7b7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -7,19 +7,25 @@ import io.netty.channel.ChannelInboundHandlerAdapter import io.netty.channel.ChannelOption import io.netty.channel.nio.NioEventLoopGroup import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import java.io.Closeable import java.util.* import kotlin.properties.Delegates -abstract class Connection(val side: ConnectionSide, val type: ConnectionType, val localUUID: UUID) : ChannelInboundHandlerAdapter(), Closeable { +abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable { abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any) var avatar: Avatar? = null var character: PlayerEntity? = null + val rpc = JsonRPC() - var channel: Channel by Delegates.notNull() + var connectionID: Int = -1 + + val hasChannel get() = ::channel.isInitialized + lateinit var channel: Channel protected set var isLegacy: Boolean = true @@ -37,7 +43,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va private val legacyValidator = PacketRegistry.LEGACY.Validator(side) private val legacySerializer = PacketRegistry.LEGACY.Serializer(side) - fun setupLegacy() { + open fun setupLegacy() { + if (isConnected) throw IllegalStateException("Already connected") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol") if (type == ConnectionType.MEMORY) { @@ -52,7 +59,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va isConnected = true } - fun setupNative() { + open fun setupNative() { + if (isConnected) throw IllegalStateException("Already connected") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol") if (type == ConnectionType.MEMORY) { @@ -65,8 +73,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va isLegacy = false isConnected = true + } - inGame() + protected open fun onChannelClosed() { + isConnected = false + LOGGER.info("Connection to ${channel.remoteAddress()} is closed") } fun bind(channel: Channel) { @@ -81,12 +92,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va channel.pipeline().addLast(this) channel.closeFuture().addListener { - isConnected = false - LOGGER.info("Connection to ${channel.remoteAddress()} is closed") + onChannelClosed() } } - protected abstract fun inGame() + abstract fun inGame() fun send(packet: IPacket) { channel.write(packet) @@ -97,7 +107,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va channel.flush() } - fun flush() { + open fun flush() { channel.flush() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt new file mode 100644 index 00000000..c717ed11 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt @@ -0,0 +1,137 @@ +package ru.dbotthepony.kstarbound.network + +import com.google.common.collect.ImmutableList +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.concurrent.CompletableFuture +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class JsonRPC { + enum class Command { + REQUEST, RESPONSE, FAIL; + } + + data class Entry(val command: Command, val id: Int, val handler: KOptional, val arguments: KOptional) { + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + stream.writeJsonElement(JsonObject().also { + it["command"] = command.name.lowercase() + it["id"] = id + handler.ifPresent { v -> it["handler"] = v } + arguments.ifPresent { v -> if (command == Command.RESPONSE) it["result"] = v else it["arguments"] = v } + }) + } else { + stream.write(command.ordinal) + stream.writeInt(id) + stream.writeKOptional(handler) { writeBinaryString(it) } + stream.writeKOptional(arguments) { writeJsonElement(it) } + } + } + + companion object { + fun native(stream: DataInputStream): Entry { + return Entry(Command.entries[stream.read()], stream.readInt(), stream.readKOptional { readBinaryString() }, stream.readKOptional { readJsonElement() }) + } + + fun legacy(stream: DataInputStream): Entry { + val data = stream.readJsonElement() + check(data is JsonObject) { "Expected JsonObject, got ${data::class}" } + val command = data["command"]?.asString?.uppercase() ?: throw JsonSyntaxException("Missing 'command' in RPC data") + val id = data["id"]?.asInt ?: throw JsonSyntaxException("Missing 'id' in RPC data") + val handler = KOptional.ofNullable(data["handler"]?.asString) + val arguments = KOptional.ofNullable(data["arguments"]) + return Entry(Command.entries.firstOrNull { it.name == command } ?: throw JsonSyntaxException("Invalid 'command': $command"), id, handler, arguments) + } + + fun read(stream: DataInputStream, isLegacy: Boolean): Entry { + if (isLegacy) + return legacy(stream) + else + return native(stream) + } + } + } + + fun interface Callback { + operator fun invoke(arguments: JsonElement): JsonElement + } + + private var commandCounter = 0 + private val pendingWrite = ArrayList() + private val responses = Int2ObjectOpenHashMap>() + private val lock = ReentrantLock() + private val handlers = Object2ObjectOpenHashMap() + + fun add(name: String, callback: Callback): (JsonElement) -> CompletableFuture { + val old = handlers.put(name, callback) + check(old == null) { "Duplicate RPC handler: $name" } + return { invoke(name, it) } + } + + operator fun invoke(handler: String, arguments: JsonElement): CompletableFuture { + lock.withLock { + val id = commandCounter++ + val response = CompletableFuture() + responses[id] = response + pendingWrite.add(Entry(Command.REQUEST, id, KOptional(handler), KOptional(arguments.deepCopy()))) + return response + } + } + + fun write(): List? { + lock.withLock { + if (pendingWrite.isEmpty()) + return null + + val result = ImmutableList.copyOf(pendingWrite) + pendingWrite.clear() + return result + } + } + + fun read(data: List) { + lock.withLock { + for (entry in data) { + try { + when (entry.command) { + Command.REQUEST -> { + val handler = handlers[entry.handler.value] ?: throw IllegalArgumentException("No such handler ${entry.handler.value}") + pendingWrite.add(Entry(Command.RESPONSE, entry.id, KOptional(), KOptional(handler(entry.arguments.value)))) + } + + Command.RESPONSE -> { + responses.remove(entry.id)?.complete(entry.arguments.orElse { JsonNull.INSTANCE }) + } + + Command.FAIL -> { + responses.remove(entry.id)?.completeExceptionally(RuntimeException("Remote RPC call failed")) + } + } + } catch (err: Throwable) { + LOGGER.error("Error while handling RPC call", err) + pendingWrite.add(Entry(Command.FAIL, entry.id, KOptional(), KOptional())) + } + } + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt new file mode 100644 index 00000000..15d5f12e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/LegacyNetworkCellState.kt @@ -0,0 +1,129 @@ +package ru.dbotthepony.kstarbound.network + +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials +import java.io.DataInputStream +import java.io.DataOutputStream + +data class LegacyNetworkTileState( + val tile: Int, // ushort + val tileHueShift: Int, // ubyte + val tileColor: Int, // ubyte + val tileModifier: Int, // ushort + val tileModifierHueShift: Int, // ubyte +) { + fun write(stream: DataOutputStream) { + if (tile !in 1 .. 65535) { // empty or can't be represented by legacy protocol + // very interesting, very yes + // what about literal 0 material? + stream.writeShort(0) + } else { + stream.writeShort(tile) + stream.write(tileHueShift) + stream.write(tileColor) + + if (tileModifier !in 1 .. 65535) { // empty or can't be represented by legacy protocol + stream.writeShort(0) + } else { + stream.writeShort(tileModifier) + stream.write(tileModifierHueShift) + } + } + } + + companion object { + val EMPTY = LegacyNetworkTileState(0, 0, 0, 0, 0) + val NULL = LegacyNetworkTileState(BuiltinMetaMaterials.NULL.id!!, 0, 0, 0, 0) + + fun read(stream: DataInputStream): LegacyNetworkTileState { + val tile = stream.readUnsignedShort() + val tileHueShift: Int + val tileColor: Int + val tileModifier: Int + val tileModifierHueShift: Int + + if (tile == 0) { + return EMPTY + } else { + tileHueShift = stream.readUnsignedByte() + tileColor = stream.readUnsignedByte() + tileModifier = stream.readUnsignedShort() + + if (tileModifier == 0) { + tileModifierHueShift = 0 + } else { + tileModifierHueShift = stream.readUnsignedByte() + } + } + + return LegacyNetworkTileState(tile, tileHueShift, tileColor, tileModifier, tileModifierHueShift) + } + } +} + +data class LegacyNetworkCellState( + val background: LegacyNetworkTileState, + val foreground: LegacyNetworkTileState, + + val collisionType: Int, // ubyte + val blockBiomeIndex: Int, // ubyte + val environmentBiomeIndex: Int, // ubyte + val liquid: LegacyNetworkLiquidState, + val dungeonId: Int, // ushort +) { + fun write(stream: DataOutputStream) { + background.write(stream) + foreground.write(stream) + + stream.write(collisionType) + stream.write(blockBiomeIndex) + stream.write(environmentBiomeIndex) + liquid.write(stream) + stream.writeVarInt(dungeonId) + } + + companion object { + val EMPTY = LegacyNetworkCellState(LegacyNetworkTileState.EMPTY, LegacyNetworkTileState.EMPTY, 0, 0, 0, LegacyNetworkLiquidState.EMPTY, 0) + val NULL = LegacyNetworkCellState(LegacyNetworkTileState.NULL, LegacyNetworkTileState.NULL, 0, 0, 0, LegacyNetworkLiquidState.EMPTY, 0) + + fun read(stream: DataInputStream): LegacyNetworkCellState { + return LegacyNetworkCellState( + LegacyNetworkTileState.read(stream), + LegacyNetworkTileState.read(stream), + stream.readUnsignedByte(), + stream.readUnsignedByte(), + stream.readUnsignedByte(), + LegacyNetworkLiquidState.read(stream), + stream.readVarInt() + ) + } + } +} + +data class LegacyNetworkLiquidState( + val liquid: Int, // ubyte + val level: Int, // ubyte +) { + fun write(stream: DataOutputStream) { + stream.write(liquid) + + if (liquid in 1 .. 255) { // empty or can't be represented by legacy protocol + stream.write(level) + } + } + + companion object { + val EMPTY = LegacyNetworkLiquidState(0, 0) + + fun read(stream: DataInputStream): LegacyNetworkLiquidState { + val liquid = stream.readUnsignedByte() + + if (liquid in 1 .. 255) { + return LegacyNetworkLiquidState(liquid, stream.readUnsignedByte()) + } + + return EMPTY + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 371102ee..c3fc5036 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -13,28 +13,34 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.readSignedVarInt -import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.writeSignedVarInt -import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket +import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.PingPacket +import ru.dbotthepony.kstarbound.network.packets.PongPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket import java.io.BufferedInputStream import java.io.DataInputStream import java.io.DataOutputStream +import java.io.FilterInputStream import java.io.InputStream +import java.util.zip.Deflater import java.util.zip.InflaterInputStream import kotlin.math.absoluteValue import kotlin.reflect.KClass @@ -66,6 +72,10 @@ class PacketRegistry(val isLegacy: Boolean) { return add(T::class, reader, direction) } + private inline fun add(value: T, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry { + return add(T::class, { _, _ -> value }, direction) + } + private fun skip(amount: Int = 1) { for (i in 0 until amount) { packets.add(null) @@ -77,6 +87,38 @@ class PacketRegistry(val isLegacy: Boolean) { packets.add(null) } + // avoid zip bomb + private class LimitingInputStream(inputStream: InputStream) : FilterInputStream(inputStream) { + private var read = 0L + + override fun read(): Int { + if (read >= MAX_PACKET_SIZE) + return -1 + + read++ + return super.read() + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + if (read >= MAX_PACKET_SIZE) + return -1 + + val actual = super.read(b, off, len.coerceAtMost((MAX_PACKET_SIZE - read).coerceAtMost(Int.MAX_VALUE.toLong()).toInt())) + + if (actual > 0) + read += actual + + return actual + } + + override fun available(): Int { + if (read >= MAX_PACKET_SIZE) + return 0 + else + return super.available() + } + } + inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() { private val backlog = ByteArrayList() private var discardBytes = 0 @@ -105,17 +147,23 @@ class PacketRegistry(val isLegacy: Boolean) { val stream: InputStream if (isCompressed) { - stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size))) + stream = BufferedInputStream(LimitingInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size)))) } else { stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size) } - try { - ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy)) - } catch (err: Throwable) { - LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) + // legacy protocol allows to stitch multiple packets of same type together without + // separate headers for each + while (stream.available() > 0) { + try { + ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy)) + } catch (err: Throwable) { + LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) + } } + stream.close() + backlog.clear() readingType = null isCompressed = false @@ -142,6 +190,9 @@ class PacketRegistry(val isLegacy: Boolean) { } else if (!type.direction.acceptedOn(side)) { LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes") discardBytes = dataLength.absoluteValue + } else if (dataLength.absoluteValue >= MAX_PACKET_SIZE) { + LOGGER.error("Packet ($packetType/${type.type}) of ${dataLength.absoluteValue} bytes is bigger than maximum allowed $MAX_PACKET_SIZE bytes") + discardBytes = dataLength.absoluteValue } else { LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue) readingType = type @@ -171,13 +222,39 @@ class PacketRegistry(val isLegacy: Boolean) { if (isLegacy) check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" } - val buff = ctx.alloc().buffer(stream.length + 5) - val stream2 = ByteBufOutputStream(buff) - stream2.writeByte(type.id) - stream2.writeSignedVarInt(stream.length) - stream2.write(stream.array, 0, stream.length) - LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length) - ctx.write(buff, promise) + if (stream.length >= 512) { + // compress + val deflater = Deflater(3) + val buffers = ByteArrayList(1024) + val buffer = ByteArray(1024) + deflater.setInput(stream.array, 0, stream.length) + + while (!deflater.needsInput()) { + val deflated = deflater.deflate(buffer) + + if (deflated > 0) + buffers.addElements(buffers.size, buffer, 0, deflated) + else + break + } + + val buff = ctx.alloc().buffer(buffers.size + 5) + val stream2 = ByteBufOutputStream(buff) + stream2.writeByte(type.id) + stream2.writeSignedVarInt(-buffers.size) + stream2.write(buffers.elements(), 0, buffers.size) + LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes / COMPRESSED size {} bytes)", type.id, type.type, side, stream.length, buffers.size) + ctx.write(buff, promise) + } else { + // send as-is + val buff = ctx.alloc().buffer(stream.length + 5) + val stream2 = ByteBufOutputStream(buff) + stream2.writeByte(type.id) + stream2.writeSignedVarInt(stream.length) + stream2.write(stream.array, 0, stream.length) + LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length) + ctx.write(buff, promise) + } } } } @@ -213,6 +290,12 @@ class PacketRegistry(val isLegacy: Boolean) { } companion object { + const val MAX_PACKET_SIZE = 64L * 1024L * 1024L // 64 MiB + // this includes both compressed and uncompressed + // Original game allows 16 mebibyte packets + // but it doesn't account for compression bomb (packets are fully uncompressed + // right away without limiting decompressed output size) + private val LOGGER = LogManager.getLogger() val NATIVE = PacketRegistry(false) @@ -266,16 +349,16 @@ class PacketRegistry(val isLegacy: Boolean) { // Packets sent bidirectionally between the universe client and the universe // server - LEGACY.skip("ClientContextUpdate") + LEGACY.add(ClientContextUpdatePacket::read) // Packets sent world server -> world client LEGACY.add(::WorldStartPacket) // WorldStart - LEGACY.skip("WorldStop") + LEGACY.add(::WorldStopPacket) LEGACY.skip("WorldLayoutUpdate") LEGACY.skip("WorldParametersUpdate") LEGACY.skip("CentralStructureUpdate") - LEGACY.skip("TileArrayUpdate") - LEGACY.skip("TileUpdate") + LEGACY.add(LegacyTileArrayUpdatePacket::read) + LEGACY.add(LegacyTileUpdatePacket::read) LEGACY.skip("TileLiquidUpdate") LEGACY.skip("TileDamageUpdate") LEGACY.skip("TileModificationFailure") @@ -286,7 +369,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("SetDungeonBreathable") LEGACY.skip("SetPlayerStart") LEGACY.skip("FindUniqueEntityResponse") - LEGACY.skip("Pong") + LEGACY.add(PongPacket) // Packets sent world client -> world server LEGACY.skip("ModifyTileList") @@ -299,7 +382,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.skip("WorldClientStateUpdate") LEGACY.skip("FindUniqueEntity") LEGACY.skip("WorldStartAcknowledge") - LEGACY.skip("Ping") + LEGACY.add(PingPacket) // Packets sent bidirectionally between world client and world server LEGACY.skip("EntityCreate") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt new file mode 100644 index 00000000..81adf72d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ClientContextUpdatePacket.kt @@ -0,0 +1,96 @@ +package ru.dbotthepony.kstarbound.network.packets + +import it.unimi.dsi.fastutil.bytes.ByteArrayList +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import ru.dbotthepony.kommons.io.ByteKey +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readByteKey +import ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.readMap +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.io.writeMap +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.network.JsonRPC +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataInputStream +import java.io.DataOutputStream + +/** + * Embeds RPC (remote procedure call) data for setting various variables on other side + */ +class ClientContextUpdatePacket( + val rpcEntries: List, + val shipChunks: KOptional>>, + val networkedVars: KOptional +) : IClientPacket, IServerPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + // this is stupid + run { + val wrap = FastByteArrayOutputStream() + DataOutputStream(wrap).writeCollection(rpcEntries) { it.write(this, true) } + stream.writeVarInt(wrap.length) + stream.write(wrap.array, 0, wrap.length) + } + + shipChunks.ifPresent { + val wrap = FastByteArrayOutputStream() + DataOutputStream(wrap).writeMap(it, { it.write(this) }, { writeKOptional(it) { writeByteArray(it) } }) + stream.writeByteArray(wrap.array, 0, wrap.length) + } + + networkedVars.ifPresent { + stream.writeVarInt(it.size) + stream.write(it.elements(), 0, it.size) + } + } else { + stream.writeCollection(rpcEntries) { it.write(this, false) } + + stream.writeKOptional(shipChunks) { + writeMap(it, { it.write(this) }, { writeKOptional(it) { writeByteArray(it) } }) + } + + stream.writeKOptional(networkedVars) { + writeVarInt(it.size) + write(it.elements(), 0, it.size) + } + } + } + + override fun play(connection: ServerConnection) { + connection.rpc.read(rpcEntries) + } + + override fun play(connection: ClientConnection) { + connection.rpc.read(rpcEntries) + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): ClientContextUpdatePacket { + if (isLegacy) { + // beyond stupid + val rpc = stream.readByteArray() + + return ClientContextUpdatePacket( + DataInputStream(FastByteArrayInputStream(rpc)).readCollection { JsonRPC.Entry.legacy(this) }, + if (stream.available() > 0) KOptional(stream.readMap({ readByteKey() }, { readKOptional { readByteArray() } })) else KOptional(), + if (stream.available() > 0) KOptional(ByteArrayList.wrap(stream.readByteArray())) else KOptional(), + ) + } else { + return ClientContextUpdatePacket( + stream.readCollection { JsonRPC.Entry.native(this) }, + stream.readKOptional { readMap({ readByteKey() }, { readKOptional { readByteArray() } }) }, + stream.readKOptional { ByteArrayList.wrap(readByteArray()) }) + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt new file mode 100644 index 00000000..dc8a4e81 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/PingPong.kt @@ -0,0 +1,27 @@ +package ru.dbotthepony.kstarbound.network.packets + +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import java.io.DataOutputStream + +object PongPacket : IClientPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) stream.write(0) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} + +object PingPacket : IServerPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) stream.write(0) + } + + override fun play(connection: ServerConnection) { + connection.send(PongPacket) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt index b99320b0..5e967c78 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/ProtocolRequestPacket.kt @@ -1,12 +1,10 @@ package ru.dbotthepony.kstarbound.network.packets import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection import java.io.DataInputStream import java.io.DataOutputStream -import java.util.UUID data class ProtocolRequestPacket(val version: Int) : IServerPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt index 5f4f7761..0f2c3db5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/ConnectSuccessPacket.kt @@ -5,7 +5,7 @@ import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kstarbound.client.ClientConnection -import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation +import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation import ru.dbotthepony.kstarbound.network.IClientPacket import java.io.DataInputStream import java.io.DataOutputStream diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt new file mode 100644 index 00000000..10f5fe61 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/LegacyTileUpdatePacket.kt @@ -0,0 +1,76 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.arrays.Object2DArray +import ru.dbotthepony.kommons.io.readSignedVarInt +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.readVector2i +import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.io.writeStruct2i +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.vector.Vector2i +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState +import ru.dbotthepony.kstarbound.world.Chunk +import java.io.DataInputStream +import java.io.DataOutputStream + +class LegacyTileUpdatePacket(val position: Vector2i, val tile: LegacyNetworkCellState) : IClientPacket { + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeSignedVarInt(position.x) + stream.writeSignedVarInt(position.y) + tile.write(stream) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): LegacyTileUpdatePacket { + check(isLegacy) { "Using legacy packet in native protocol" } + return LegacyTileUpdatePacket(Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt()), LegacyNetworkCellState.read(stream)) + } + } +} + +class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray) : IClientPacket { + constructor(chunk: Chunk<*, *>) : this(chunk.pos.tile, chunk.legacyNetworkCells()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeSignedVarInt(origin.x) + stream.writeSignedVarInt(origin.y) + stream.writeVarInt(data.rows) + stream.writeVarInt(data.columns) + + for (y in data.columnIndices) { + for (x in data.rowIndices) { + data[y, x].write(stream) + } + } + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } + + companion object { + fun read(stream: DataInputStream, isLegacy: Boolean): LegacyTileArrayUpdatePacket { + check(isLegacy) { "Using legacy packet in native protocol" } + val origin = Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt()) + + val rows = stream.readVarInt() + val columns = stream.readVarInt() + + val data = Object2DArray.nulls(columns, rows) + + for (y in data.columnIndices) { + for (x in data.rowIndices) { + data[y, x] = LegacyNetworkCellState.read(stream) + } + } + + return LegacyTileArrayUpdatePacket(origin, data as Object2DArray) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt new file mode 100644 index 00000000..27ef0aef --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/clientbound/WorldStopPacket.kt @@ -0,0 +1,20 @@ +package ru.dbotthepony.kstarbound.network.packets.clientbound + +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kstarbound.client.ClientConnection +import ru.dbotthepony.kstarbound.network.IClientPacket +import java.io.DataInputStream +import java.io.DataOutputStream + +class WorldStopPacket(val reason: String = "") : IClientPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString()) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeBinaryString(reason) + } + + override fun play(connection: ClientConnection) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt index 6ec23bb0..c73ac18e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ClientConnectPacket.kt @@ -15,7 +15,7 @@ import ru.dbotthepony.kommons.io.writeKOptional import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.util.KOptional -import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation +import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation import ru.dbotthepony.kstarbound.defs.player.ShipUpgrades import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket @@ -58,7 +58,9 @@ data class ClientConnectPacket( override fun play(connection: ServerConnection) { LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')") - connection.sendAndFlush(ConnectSuccessPacket(4, UUID(4L, 4L), CelestialBaseInformation())) + connection.receiveShipChunks(shipChunks) + connection.sendAndFlush(ConnectSuccessPacket(connection.connectionID, UUID(4L, 4L), CelestialBaseInformation())) + connection.inGame() } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 91f7f608..85b4192b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -1,11 +1,15 @@ package ru.dbotthepony.kstarbound.server import io.netty.channel.ChannelHandlerContext +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.io.ByteKey +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket @@ -14,16 +18,34 @@ import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket +import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket +import ru.dbotthepony.kstarbound.server.world.IChunkSource +import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.IChunkListener +import ru.dbotthepony.kstarbound.world.WorldGeometry import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject -import java.util.* +import kotlin.properties.Delegates -class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) { +// serverside part of connection +class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) { var world: ServerWorld? = null + lateinit var shipWorld: ServerWorld + private set + + init { + connectionID = server.nextConnectionID.incrementAndGet() + } + + override fun toString(): String { + val channel = if (hasChannel) channel.remoteAddress().toString() else "" + val ship = if (::shipWorld.isInitialized) shipWorld.toString() else "" + return "ServerConnection[ID=$connectionID channel=$channel / $ship]" + } var trackedPosition: Vector2d = Vector2d.ZERO set(value) { @@ -52,6 +74,25 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } + private val shipChunks = Object2ObjectOpenHashMap>() + private val modifiedShipChunks = ObjectOpenHashSet() + var shipChunkSource by Delegates.notNull() + private set + + override fun setupLegacy() { + super.setupLegacy() + shipChunkSource = LegacyChunkSource.memory(shipChunks) + } + + override fun setupNative() { + super.setupNative() + } + + fun receiveShipChunks(chunks: Map>) { + check(shipChunks.isEmpty()) { "Already has ship chunks" } + shipChunks.putAll(chunks) + } + private val tickets = Object2ObjectOpenHashMap() private val pendingSend = ObjectLinkedOpenHashSet() @@ -72,12 +113,35 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } + override fun flush() { + val entries = rpc.write() + + if (entries != null || modifiedShipChunks.isNotEmpty()) { + channel.write(ClientContextUpdatePacket( + entries ?: listOf(), + KOptional(modifiedShipChunks.associateWith { shipChunks[it]!! }), + KOptional(ByteArrayList()))) + + modifiedShipChunks.clear() + } + + super.flush() + } + fun onLeaveWorld() { tickets.values.forEach { it.cancel() } tickets.clear() pendingSend.clear() } + override fun onChannelClosed() { + super.onChannelClosed() + + if (::shipWorld.isInitialized) { + shipWorld.close() + } + } + private fun recomputeTrackedChunks() { val world = world ?: return val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition) @@ -129,7 +193,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn for (pos in itr) { val chunk = world.chunkMap[pos] ?: continue - send(ChunkCellsPacket(chunk)) + + if (isLegacy) { + send(LegacyTileArrayUpdatePacket(chunk)) + } else { + send(ChunkCellsPacket(chunk)) + } + itr.remove() } } @@ -149,7 +219,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } override fun inGame() { - server.playerInGame(this) + // server.playerInGame(this) + + LOGGER.info("Initializing ship world for $this") + shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false)) + shipWorld.addChunkSource(shipChunkSource) + shipWorld.thread.start() + shipWorld.acceptPlayer(this) } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt index 54fd03e0..8fe14eaf 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/StarboundServer.kt @@ -31,6 +31,8 @@ sealed class StarboundServer(val root: File) : Closeable { val thread = Thread(spinner, "Starbound Server $serverID") val universe = ServerUniverse() + val nextConnectionID = AtomicInteger() + val settings = ServerSettings() val channels = ServerChannels(this) val lock = ReentrantLock() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt index 36b9a93b..22054a5c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyChunkSource.kt @@ -22,20 +22,21 @@ import java.io.ByteArrayInputStream import java.io.DataInputStream import java.util.concurrent.CompletableFuture import java.util.function.Supplier -import java.util.zip.Inflater import java.util.zip.InflaterInputStream -class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { - private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL) +class LegacyChunkSource(val loader: Loader) : IChunkSource { + fun interface Loader { + operator fun invoke(at: ByteKey): CompletableFuture> + } override fun getTiles(pos: ChunkPos): CompletableFuture>> { val chunkX = pos.x val chunkY = pos.y val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) - return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carrier).thenApplyAsync { + return loader(key).thenApplyAsync { it.map { - val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater()))) + val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) reader.skipBytes(3) val result = Object2DArray.nulls(CHUNK_SIZE, CHUNK_SIZE) @@ -46,6 +47,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { } } + reader.close() result as Object2DArray } } @@ -56,9 +58,9 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { val chunkY = pos.y val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) - return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carrier).thenApplyAsync { + return loader(key).thenApplyAsync { it.map { - val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater()))) + val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it)))) val i = reader.readVarInt() val objects = ArrayList() @@ -76,6 +78,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { } } + reader.close() objects } } @@ -84,5 +87,15 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { companion object { private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val LOGGER = LogManager.getLogger() + + fun file(file: BTreeDB5): LegacyChunkSource { + val carrier = CarriedExecutor(Starbound.IO_EXECUTOR) + val loader = Loader { key -> CompletableFuture.supplyAsync(Supplier { file.read(key) }, carrier) } + return LegacyChunkSource(loader) + } + + fun memory(backing: Map>): LegacyChunkSource { + return LegacyChunkSource { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) } + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt index f783621a..b4fd2c63 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerUniverse.kt @@ -7,29 +7,27 @@ import com.google.gson.stream.JsonReader import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import kotlinx.coroutines.future.await import ru.dbotthepony.kommons.collect.chainOptionalFutures import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.io.BTreeDB6 import ru.dbotthepony.kommons.util.AABBi -import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation -import ru.dbotthepony.kstarbound.defs.CelestialGenerationInformation -import ru.dbotthepony.kstarbound.defs.CelestialNames +import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation +import ru.dbotthepony.kstarbound.defs.world.CelestialGenerationInformation +import ru.dbotthepony.kstarbound.defs.world.CelestialNames +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.util.random.staticRandom64 -import ru.dbotthepony.kstarbound.world.CoordinateMapper import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.UniversePos -import ru.dbotthepony.kstarbound.world.positiveModulo import java.io.Closeable import java.io.File import java.io.InputStreamReader import java.time.Duration -import java.util.* import java.util.concurrent.CompletableFuture import kotlin.collections.ArrayList @@ -69,18 +67,39 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab private val sources = ArrayList() private val closeables = ArrayList() - override fun name(pos: UniversePos): CompletableFuture> { - return getChunk(pos).thenApply { - it.flatMap { it.parameters(pos) }.map { it.name } - } + override suspend fun parameters(pos: UniversePos): CelestialParameters? { + return getChunk(pos)?.parameters(pos) } - override fun scanSystems(region: AABBi, includedTypes: Set?): CompletableFuture> { + override suspend fun hasChildren(pos: UniversePos): Boolean { + val system = getChunk(pos)?.systems?.get(pos.location) ?: return false + + if (pos.isSystem) + return system.planets.isNotEmpty() + else if (pos.isPlanet) + return system.planets[pos.orbitNumber]?.satellites?.isNotEmpty() ?: false + + return false + } + + override suspend fun children(pos: UniversePos): List { + val chunk = getChunk(pos) ?: return emptyList() + val system = chunk.systems[pos.location] ?: return listOf() + + if (pos.isSystem) + return system.planets.keys.intStream().mapToObj { UniversePos(pos.location, it) }.toList() + else if (pos.isPlanet) + return system.planets[pos.planetOrbit]?.satellites?.keys?.intStream()?.mapToObj { UniversePos(pos.location, pos.planetOrbit, it) }?.toList() ?: listOf() + + return listOf() + } + + override suspend fun scanSystems(region: AABBi, includedTypes: Set?): List { val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null val futures = ArrayList>>() for (pos in chunkPositions(region)) { - val f = getChunk(pos).thenApply { + val f = getChunkFuture(pos).thenApply { it.map> { val result = ArrayList() @@ -103,23 +122,23 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab futures.add(f) } - return CompletableFuture.allOf(*futures.toTypedArray()) - .thenApply { futures.stream().flatMap { it.get().stream() }.toList() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + return futures.stream().flatMap { it.get().stream() }.toList() } - override fun scanConstellationLines(region: AABBi): CompletableFuture>> { + override suspend fun scanConstellationLines(region: AABBi): List> { val futures = ArrayList>>>() for (pos in chunkPositions(region)) { - val f = getChunk(pos).thenApply { + val f = getChunkFuture(pos).thenApply { it.map>> { ObjectArrayList(it.constellations) }.orElse(listOf()) } futures.add(f) } - return CompletableFuture.allOf(*futures.toTypedArray()) - .thenApply { futures.stream().flatMap { it.get().stream() }.toList() } + CompletableFuture.allOf(*futures.toTypedArray()).await() + return futures.stream().flatMap { it.get().stream() }.toList() } override fun scanRegionFullyLoaded(region: AABBi): Boolean { @@ -140,12 +159,22 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab .scheduler(Scheduler.systemScheduler()) .build() - fun getChunk(pos: UniversePos): CompletableFuture> { + fun getChunkFuture(pos: Vector2i): CompletableFuture> { + return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } } + } + + suspend fun getChunk(pos: UniversePos): UniverseChunk? { return getChunk(world2chunk(Vector2i(pos.location))) } - fun getChunk(pos: Vector2i): CompletableFuture> { - return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } } + suspend fun getChunk(pos: Vector2i): UniverseChunk? { + val get = getChunkFuture(pos).await() + + if (get.isPresent) { + return get.value + } else { + return null + } } init { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index b8ef6cd8..f1184cc7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.server.world +import com.google.gson.JsonObject import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet @@ -7,6 +8,8 @@ import ru.dbotthepony.kommons.collect.chainOptionalFutures import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket +import ru.dbotthepony.kstarbound.defs.world.WorldTemplate +import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.server.StarboundServer import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.util.ExecutionSpinner @@ -19,6 +22,7 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import java.util.Collections import java.util.concurrent.CompletableFuture import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.ReentrantLock @@ -28,9 +32,8 @@ import kotlin.concurrent.withLock class ServerWorld( val server: StarboundServer, - seed: Long, geometry: WorldGeometry, -) : World(seed, geometry) { +) : World(geometry) { init { server.worlds.add(this) } @@ -41,9 +44,29 @@ class ServerWorld( private fun doAcceptPlayer(player: ServerConnection): Boolean { if (player !in internalPlayers) { internalPlayers.add(player) + player.onLeaveWorld() player.world?.removePlayer(player) player.world = this - player.sendAndFlush(JoinWorldPacket(this)) + + if (player.isLegacy) { + player.sendAndFlush(WorldStartPacket( + templateData = WorldTemplate(geometry).toJson(true), + skyData = ByteArray(0), + weatherData = ByteArray(0), + playerStart = playerSpawnPosition, + playerRespawn = playerSpawnPosition, + respawnInWorld = respawnInWorld, + dungeonGravity = mapOf(), + dungeonBreathable = mapOf(), + protectedDungeonIDs = setOf(), + worldProperties = JsonObject(), + connectionID = player.connectionID, + localInterpolationMode = false, + )) + } else { + player.sendAndFlush(JoinWorldPacket(this)) + } + return true } @@ -51,6 +74,9 @@ class ServerWorld( } fun acceptPlayer(player: ServerConnection): CompletableFuture { + check(!isClosed.get()) { "$this is invalid" } + unpause() + try { return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox) } catch (err: RejectedExecutionException) { @@ -71,12 +97,10 @@ class ServerWorld( } val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) - val thread = Thread(spinner, "Starbound Server World $seed") + val thread = Thread(spinner, "Starbound Server World Thread") val ticketListLock = ReentrantLock() - @Volatile - var isClosed: Boolean = false - private set + private val isClosed = AtomicBoolean() init { thread.isDaemon = true @@ -89,10 +113,18 @@ class ServerWorld( chunkProviders.add(source) } + fun pause() { + if (!isClosed.get()) spinner.pause() + } + + fun unpause() { + if (!isClosed.get()) spinner.unpause() + } + override fun close() { - if (!isClosed) { + if (isClosed.compareAndSet(false, true)) { super.close() - isClosed = true + spinner.unpause() lock.withLock { internalPlayers.forEach { @@ -105,7 +137,7 @@ class ServerWorld( } private fun spin(): Boolean { - if (isClosed) return false + if (isClosed.get()) return false try { think() @@ -124,7 +156,7 @@ class ServerWorld( } override fun thinkInner() { - internalPlayers.forEach { if (!isClosed) it.tick() } + internalPlayers.forEach { if (!isClosed.get()) it.tick() } ticketListLock.withLock { ticketLists.removeIf { 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 0aea0f18..47c3ceb4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt @@ -18,11 +18,10 @@ import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector3i -import ru.dbotthepony.kstarbound.defs.CelestialParameters -import ru.dbotthepony.kstarbound.defs.CelestialPlanet +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters +import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairListAdapter -import ru.dbotthepony.kstarbound.json.pairSetAdapter import ru.dbotthepony.kstarbound.world.UniversePos class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { @@ -31,15 +30,15 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { val systems = Object2ObjectOpenHashMap() val constellations = ObjectOpenHashSet>() - fun parameters(coordinate: UniversePos): KOptional { - val system = systems[coordinate.location] ?: return KOptional() + fun parameters(coordinate: UniversePos): CelestialParameters? { + val system = systems[coordinate.location] ?: return null if (coordinate.isSystem) - return KOptional(system.parameters) + return system.parameters else if (coordinate.isPlanet) - return KOptional.ofNullable(system.planets[coordinate.planetOrbit]?.parameters) + return system.planets[coordinate.planetOrbit]?.parameters else if (coordinate.isSatellite) - return KOptional.ofNullable(system.planets[coordinate.planetOrbit]?.satellites?.get(coordinate.satelliteOrbit)) + return system.planets[coordinate.planetOrbit]?.satellites?.get(coordinate.satelliteOrbit) else throw RuntimeException("unreachable code") } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt index 085da532..96e13e37 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseSource.kt @@ -17,13 +17,14 @@ import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.defs.CelestialParameters -import ru.dbotthepony.kstarbound.defs.CelestialPlanet +import ru.dbotthepony.kstarbound.defs.world.CelestialParameters +import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.math.Line2d +import ru.dbotthepony.kstarbound.util.binnedChoice import ru.dbotthepony.kstarbound.util.paddedNumber import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.staticRandom64 @@ -69,7 +70,7 @@ private fun key(chunkPos: IStruct2i): ByteKey { } class LegacyUniverseSource(private val db: BTreeDB5) : UniverseSource(), Closeable { - private val carried = CarriedExecutor(Starbound.STORAGE_IO_POOL) + private val carried = CarriedExecutor(Starbound.IO_EXECUTOR) override fun close() { carried.shutdown() @@ -168,9 +169,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv val type = universe.generationInformation.systemTypeBins .stream() - .sorted { o1, o2 -> o2.first.compareTo(o1.first) } - .filter { it.first <= typeSelector } - .findFirst().map { it.second }.orElse("") + .binnedChoice(typeSelector).orElse("") if (type.isBlank()) return null @@ -365,7 +364,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv } } - private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL) + private val carrier = CarriedExecutor(Starbound.IO_EXECUTOR) val reader: UniverseSource = Reader() val generator: UniverseSource = Generator() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt index 656fbae1..45d1b47b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/AssetPathStack.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.util import ru.dbotthepony.kstarbound.Starbound +import java.io.File object AssetPathStack { private val _stack = object : ThreadLocal>() { @@ -49,4 +50,11 @@ object AssetPathStack { fun remapSafe(path: String): String { return remap(last() ?: return path, path) } + + fun relativeTo(base: String, path: String): String { + if (path.isNotEmpty() && path[0] == '/') + return path + + return if (base.endsWith('/')) "$base$path" else "${base.substringBeforeLast('/')}/$path" + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt index 3bdb5a1b..40c3968e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionSpinner.kt @@ -50,7 +50,28 @@ class ExecutionSpinner(private val waiter: Runnable, private val spinner: Boolea return Starbound.TICK_TIME_ADVANCE_NANOS - (System.nanoTime() - lastRender) - frameRenderTime } + private var carrier: Thread? = null + @Volatile + private var isPaused = false + + fun pause() { + isPaused = true + } + + fun unpause() { + if (isPaused) { + isPaused = false + carrier?.let { LockSupport.unpark(it) } + } + } + fun spin(): Boolean { + carrier = Thread.currentThread() + + while (isPaused) { + LockSupport.park() + } + var diff = timeUntilNextFrame() while (diff > 0L) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt index 60a45470..a6514d61 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/HashTableInterner.kt @@ -139,7 +139,7 @@ class HashTableInterner(private val segmentBits: Int = log(Runtime.getR // chained linking hash table backed by plain array // while this increase memory usage (linked list), this greatly - // simplify logic, and make scanning a bit faster because we don't jump to neighbour nodes + // simplify logic, and make scanning a bit faster given if there are narrow collisions avoidances // (assuming past our neighbour there is no such key) private inner class Segment(val size: Int, private val lock: ReentrantLock) { private val queue = ReferenceQueue() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ListInterner.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ListInterner.kt new file mode 100644 index 00000000..4a6e4b09 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ListInterner.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.kstarbound.util + +import com.github.benmanes.caffeine.cache.Interner + +class ListInterner : Interner, MutableIterable { + val list = ArrayList() + + override fun intern(sample: E): E { + val indexOf = list.indexOf(sample) + + if (indexOf == -1) { + list.add(sample) + return sample + } + + return list[indexOf] + } + + override fun iterator(): MutableIterator { + return list.iterator() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt new file mode 100644 index 00000000..5da35feb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt @@ -0,0 +1,93 @@ +package ru.dbotthepony.kstarbound.util + +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.objects.Object2ObjectMap +import it.unimi.dsi.fastutil.objects.Object2ObjectMaps + +class RenderDirectives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap) { + constructor() : this(Object2ObjectAVLTreeMap()) + constructor(directives: String) : this() { + if (directives.isNotBlank()) { + if ('?' !in directives) { + if ('=' !in directives) { + throw IllegalArgumentException("Missing render directive delimiter in '$directives'") + } + + // assume it is just "name=value" + val key = directives.substringBefore('=') + val value = directives.substringAfter('=') + directivesInternal[key] = value + } else { + // gets interesting + for (pair in directives.split('?').filter { it.isNotBlank() }) { + if ('=' !in pair) { + throw IllegalArgumentException("Missing render directive delimiter in '$pair' (full string: '$directives')") + } + + val key = pair.substringBefore('=') + val value = pair.substringAfter('=') + directivesInternal[key] = value + } + } + } + } + + val directives: Object2ObjectMap = Object2ObjectMaps.unmodifiable(directivesInternal) + + override fun toString(): String { + if (directivesInternal.isEmpty()) + return "RenderDirectives[empty]" + else + return "RenderDirectives[?${directivesInternal.entries.joinToString("?") { "${it.key}=${it.value}" }}]" + } + + override fun equals(other: Any?): Boolean { + return this === other || other is RenderDirectives && directivesInternal == other.directivesInternal + } + + override fun hashCode(): Int { + return directivesInternal.hashCode() + } + + fun add(directive: String, value: String): RenderDirectives { + if (directivesInternal[directive] == value) + return this + + val copy = directivesInternal.clone() + copy[directive] = value + return RenderDirectives(copy) + } + + fun add(directives: String): RenderDirectives { + if ('?' !in directives) { + if ('=' !in directives) { + throw IllegalArgumentException("Missing render directive delimiter in $directives") + } + + // assume it is just "name=value" + val key = directives.substringBefore('=') + val value = directives.substringAfter('=') + return add(key, value) + } else { + // gets interesting + val split = directives.split('?').filter { it.isNotBlank() } + + if (split.isEmpty()) + return this + + val copy = directivesInternal.clone() + + for (pair in split) { + if ('=' !in pair) { + throw IllegalArgumentException("Missing render directive delimiter in '$pair' (full string: $directives)") + } + + val key = pair.substringBefore('=') + val value = pair.substringAfter('=') + copy[key] = value + } + + return RenderDirectives(copy) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt index 8bc87f06..63252cb0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Utils.kt @@ -5,6 +5,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonObject import ru.dbotthepony.kstarbound.Starbound import java.util.* +import java.util.stream.Stream fun String.sbIntern(): String { return Starbound.STRINGS.intern(this) @@ -76,3 +77,9 @@ fun paddedNumber(number: Int, digits: Int): String { return "0".repeat(digits - str.length) + str } } + +fun , T : Any> Stream>.binnedChoice(value: C): Optional { + return this.sorted { o1, o2 -> o2.first.compareTo(o1.first) } + .filter { it.first <= value } + .findFirst().map { it.second } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt index 25b4d5c3..654a1552 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.util.random import com.google.gson.Gson +import com.google.gson.JsonObject import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken @@ -10,6 +11,8 @@ import ru.dbotthepony.kommons.arrays.Double2DArray import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kommons.gson.value import kotlin.math.floor import kotlin.math.sqrt @@ -27,6 +30,9 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) { var isInitialized = false private set + var seed: Long = 0L + private set + init { if (parameters.seed != null) { init(parameters.seed) @@ -37,13 +43,14 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) { protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double) - protected val p = IntArray(parameters.scale * 2 + 2) - protected val g1 = DoubleArray(parameters.scale * 2 + 2) - protected val g2 = Double2DArray.allocate(parameters.scale * 2 + 2, 2) - protected val g3 = Double2DArray.allocate(parameters.scale * 2 + 2, 3) + protected val p by lazy { IntArray(parameters.scale * 2 + 2) } + protected val g1 by lazy { DoubleArray(parameters.scale * 2 + 2) } + protected val g2 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 2) } + protected val g3 by lazy { Double2DArray.allocate(parameters.scale * 2 + 2, 3) } fun init(seed: Long) { isInitialized = true + this.seed = seed p.fill(0) g1.fill(0.0) @@ -215,12 +222,22 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) { } override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (type.rawType == AbstractPerlinNoise::class.java) { + if (AbstractPerlinNoise::class.java.isAssignableFrom(type.rawType)) { return object : TypeAdapter() { private val parent = gson.getAdapter(PerlinNoiseParameters::class.java) override fun write(out: JsonWriter, value: AbstractPerlinNoise?) { - return parent.write(out, value?.parameters) + if (value == null) + out.nullValue() + else { + val json = parent.toJsonTree(value.parameters) as JsonObject + + if (value.seed != 0L) { + json["seed"] = value.seed + } + + out.value(json) + } } override fun read(`in`: JsonReader): AbstractPerlinNoise? { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt index 10489585..7ee7015a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/MWCRandom.kt @@ -37,7 +37,7 @@ class MWCRandom(seed: Long = System.nanoTime(), cycle: Int = 256, windupIteratio // initial windup for (i in 0 until windupIterations) { - nextInt() + nextLong() } } 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 5aa572e6..e98e047e 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,8 @@ package ru.dbotthepony.kstarbound.util.random import com.google.gson.JsonArray import com.google.gson.JsonElement import it.unimi.dsi.fastutil.bytes.ByteConsumer +import ru.dbotthepony.kommons.util.IStruct2d +import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.XXHash32 import ru.dbotthepony.kommons.util.XXHash64 import java.util.* @@ -10,6 +12,7 @@ import java.util.random.RandomGenerator import java.util.stream.IntStream import kotlin.NoSuchElementException import kotlin.collections.List +import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.floor import kotlin.math.ln @@ -75,6 +78,14 @@ fun staticRandom32(vararg values: Any): Int { return digest.digestAsInt() } +fun staticRandomFloat(vararg values: Any): Float { + return staticRandom32(*values).toFloat().absoluteValue / Int.MAX_VALUE.toFloat() +} + +fun staticRandomDouble(vararg values: Any): Double { + return staticRandom64(*values).toDouble().absoluteValue / Long.MAX_VALUE.toDouble() +} + fun staticRandom64(vararg values: Any): Long { val digest = XXHash64(1997293021376312589L) @@ -207,3 +218,11 @@ fun JsonArray.random(random: RandomGenerator, default: () -> JsonElement): JsonE return elementAt(random.nextInt(size())) } + +fun RandomGenerator.nextRange(range: IStruct2i): Int { + return if (range.component1() == range.component2()) return range.component1() else nextInt(range.component1(), range.component2()) +} + +fun RandomGenerator.nextRange(range: IStruct2d): Double { + return if (range.component1() == range.component2()) return range.component1() else nextDouble(range.component1(), range.component2()) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt index 16934559..3fb8201a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Chunk.kt @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.ICellAccess import ru.dbotthepony.kstarbound.world.api.ImmutableCell @@ -66,6 +67,15 @@ abstract class Chunk, This : Chunk { + if (cells.isInitialized()) { + val cells = cells.value + return Object2DArray(CHUNK_SIZE, CHUNK_SIZE) { a, b -> cells[a, b].toLegacyNet() } + } else { + return Object2DArray(CHUNK_SIZE, CHUNK_SIZE, LegacyNetworkCellState.NULL) + } + } + fun loadCells(source: Object2DArray) { val ours = cells.value source.checkSizeEquals(ours) @@ -153,10 +163,6 @@ abstract class Chunk, This : Chunk> + abstract suspend fun parameters(pos: UniversePos): CelestialParameters? + + suspend fun name(pos: UniversePos): String? { + return parameters(pos)?.name + } /** * Return all valid system coordinates in the given x/y range. All systems @@ -21,8 +24,11 @@ abstract class Universe { * from the top in 2d. The z-coordinate is there simpy as a validation * parameter. */ - abstract fun scanSystems(region: AABBi, includedTypes: Set? = null): CompletableFuture> - abstract fun scanConstellationLines(region: AABBi): CompletableFuture>> + abstract suspend fun scanSystems(region: AABBi, includedTypes: Set? = null): List + abstract suspend fun scanConstellationLines(region: AABBi): List> + + abstract suspend fun hasChildren(pos: UniversePos): Boolean + abstract suspend fun children(pos: UniversePos): List /** * Returns false if part or all of the specified region is not loaded. This diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index a939b71c..26c06370 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -30,16 +30,11 @@ import java.util.function.Predicate import java.util.random.RandomGenerator import java.util.stream.Stream -abstract class World, ChunkType : Chunk>( - val seed: Long, - val geometry: WorldGeometry, -) : ICellAccess, Closeable { +abstract class World, ChunkType : Chunk>(val geometry: WorldGeometry) : ICellAccess, Closeable { val background = TileView.Background(this) val foreground = TileView.Foreground(this) val mailbox = MailboxExecutorService() - final override fun randomLongFor(x: Int, y: Int) = super.randomLongFor(x, y) xor seed - override fun getCellDirect(x: Int, y: Int): AbstractCell { if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL return getCell(x, y) @@ -225,6 +220,16 @@ abstract class World, ChunkType : Chunk() val tileEntities = ReferenceOpenHashSet() + var playerSpawnPosition = Vector2d.ZERO + protected set + var respawnInWorld = false + protected set + + open fun setPlayerSpawn(position: Vector2d, respawnInWorld: Boolean) { + playerSpawnPosition = position + this.respawnInWorld = respawnInWorld + } + abstract fun isSameThread(): Boolean fun ensureSameThread() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt index 8d09e95e..a65f8e95 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/WorldGeometry.kt @@ -13,6 +13,11 @@ import java.io.DataOutputStream data class WorldGeometry(val size: Vector2i, val loopX: Boolean, val loopY: Boolean) { constructor(buff: DataInputStream) : this(buff.readVector2i(), buff.readBoolean(), buff.readBoolean()) + init { + require(size.x > 0) { "Invalid world width: ${size.x}" } + require(size.y > 0) { "Invalid world height: ${size.y}" } + } + val x: CoordinateMapper = if (loopX) CoordinateMapper.Wrapper(size.x) else CoordinateMapper.Clamper(size.x) val y: CoordinateMapper = if (loopY) CoordinateMapper.Wrapper(size.y) else CoordinateMapper.Clamper(size.y) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index ef66715d..6107b493 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api import com.github.benmanes.caffeine.cache.Interner import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.util.HashTableInterner import java.io.DataInputStream import java.io.DataOutputStream @@ -19,6 +20,10 @@ sealed class AbstractCell { abstract fun immutable(): ImmutableCell abstract fun mutable(): MutableCell + fun toLegacyNet(): LegacyNetworkCellState { + return LegacyNetworkCellState(background.toLegacyNet(), foreground.toLegacyNet(), 0, biome, envBiome, liquid.toLegacyNet(), dungeonId) + } + fun write(stream: DataOutputStream) { foreground.write(stream) background.write(stream) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt index b9826e09..b8b858d2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractLiquidState.kt @@ -4,7 +4,7 @@ import com.github.benmanes.caffeine.cache.Interner import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition -import ru.dbotthepony.kstarbound.util.HashTableInterner +import ru.dbotthepony.kstarbound.network.LegacyNetworkLiquidState import java.io.DataInputStream import java.io.DataOutputStream @@ -17,6 +17,14 @@ sealed class AbstractLiquidState { abstract fun mutable(): MutableLiquidState abstract fun immutable(): ImmutableLiquidState + fun toLegacyNet(): LegacyNetworkLiquidState { + if (def?.id != null && def!!.id!! in 1 .. 255) { + return LegacyNetworkLiquidState(def!!.id!!, (level * 255f).toInt().coerceIn(0, 255)) + } else { + return LegacyNetworkLiquidState.EMPTY + } + } + fun write(stream: DataOutputStream) { stream.writeByte(def?.id ?: 0) stream.writeFloat(level) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt index ba37df8a..ffc3bffb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractTileState.kt @@ -6,6 +6,7 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.TileDefinition +import ru.dbotthepony.kstarbound.network.LegacyNetworkTileState import ru.dbotthepony.kstarbound.util.HashTableInterner import java.io.DataInputStream import java.io.DataOutputStream @@ -20,13 +21,30 @@ sealed class AbstractTileState { abstract fun immutable(): ImmutableTileState abstract fun mutable(): MutableTileState + fun byteHueShift(): Int { + return (hueShift / 360f * 255).toInt() + } + + fun byteModifierHueShift(): Int { + return (modifierHueShift / 360f * 255).toInt() + } + + fun toLegacyNet(): LegacyNetworkTileState { + if (material.id != null && material.id in 0 .. 65535) { + val validMod = modifier?.id != null && modifier!!.id!! in 0 .. 65535 + return LegacyNetworkTileState(material.id!!, byteHueShift(), color.ordinal, if (validMod) modifier!!.id!! else 0, if (validMod) byteModifierHueShift() else 0) + } else { + return LegacyNetworkTileState.EMPTY + } + } + fun write(stream: DataOutputStream) { stream.writeShort(material.id ?: 0) stream.writeBoolean(modifier != null) stream.writeShort(modifier?.id ?: 0) stream.writeByte(color.ordinal) - stream.write((hueShift / 360f * 255).toInt()) - stream.write((modifierHueShift / 360f * 255).toInt()) + stream.write(byteHueShift()) + stream.write(byteModifierHueShift()) } companion object { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt index 24bd6cab..27183822 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ICellAccess.kt @@ -20,31 +20,5 @@ interface ICellAccess { * whenever cell was set */ fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean - - /** - * Возвращает псевдослучайное Long для заданной позиции - * - * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции - */ - fun randomLongFor(x: Int, y: Int): Long { - var long = x * 738548L + y * 2191293543L - long = long xor 8339437585692L - long = (long ushr 4) or (long shl 52) - long *= 7848344324L - long = (long ushr 12) or (long shl 44) - return long - } - - /** - * Возвращает псевдослучайное нормализированное Double для заданной позиции - * - * Для использования в рендерах и прочих вещах, которым нужно стабильное число на основе своей позиции - */ - fun randomDoubleFor(x: Int, y: Int): Double { - return (randomLongFor(x, y) / 9.223372036854776E18) / 2.0 + 0.5 - } - - fun randomLongFor(pos: Vector2i) = randomLongFor(pos.x, pos.y) - fun randomDoubleFor(pos: Vector2i) = randomDoubleFor(pos.x, pos.y) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/OffsetCellAccess.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/OffsetCellAccess.kt index 45a906d0..80b2584b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/OffsetCellAccess.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/OffsetCellAccess.kt @@ -17,9 +17,4 @@ class OffsetCellAccess(private val parent: ICellAccess, var x: Int, var y: Int) override fun setCell(x: Int, y: Int, cell: AbstractCell): Boolean { return parent.setCell(x, y, cell) } - - override fun randomLongFor(x: Int, y: Int) = parent.randomLongFor(x + this.x, y + this.y) - override fun randomDoubleFor(x: Int, y: Int) = parent.randomDoubleFor(x + this.x, y + this.y) - override fun randomLongFor(pos: Vector2i) = parent.randomLongFor(pos.x + this.x, pos.y + this.y) - override fun randomDoubleFor(pos: Vector2i) = parent.randomDoubleFor(pos.x + this.x, pos.y + this.y) } \ No newline at end of file