WorldTemplate, WorldLayout, VisitableWorldParameters, Biomes, BiomeDefinitions, Trees, Grass, Bushes, Terrain Selectors

RenderDirectives, JsonRPC, Json adapters fixes, DispatchingTypeAdapter
This commit is contained in:
DBotThePony 2024-03-17 11:10:18 +07:00
parent 73cf5f596c
commit d37bad79c6
Signed by: DBot
GPG Key ID: DCC23B5715498507
117 changed files with 5304 additions and 707 deletions

20
ADDITIONS.md Normal file
View File

@ -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.

View File

@ -13,6 +13,8 @@ version = "0.1-SNAPSHOT"
val lwjglVersion: String by project val lwjglVersion: String by project
val lwjglNatives: String by project val lwjglNatives: String by project
val kotlinVersion: String by project
val kotlinCoroutinesVersion: String by project
repositories { repositories {
mavenCentral() mavenCentral()
@ -39,8 +41,9 @@ tasks.compileKotlin {
dependencies { dependencies {
val kommonsVersion: String by project val kommonsVersion: String by project
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.10") implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.10") 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-api:2.17.1")
implementation("org.apache.logging.log4j:log4j-core:2.17.1") implementation("org.apache.logging.log4j:log4j-core:2.17.1")

View File

@ -1,8 +1,9 @@
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.0 kotlinVersion=1.9.10
kommonsVersion=2.7.16 kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.9.20
ffiVersion=2.2.13 ffiVersion=2.2.13
lwjglVersion=3.3.0 lwjglVersion=3.3.0

View File

@ -1,13 +1,23 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableMap
import com.google.gson.TypeAdapter
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.ClientConfigParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters
import ru.dbotthepony.kstarbound.defs.MovementParameters 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 ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinTask import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.properties.Delegates
import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KMutableProperty0
object GlobalDefaults { object GlobalDefaults {
@ -22,6 +32,27 @@ object GlobalDefaults {
var clientParameters = ClientConfigParameters() var clientParameters = ClientConfigParameters()
private set private set
var worldTemplate by Delegates.notNull<WorldTemplateConfig>()
private set
var terrestrialWorlds by Delegates.notNull<TerrestrialWorldsConfig>()
private set
var asteroidWorlds by Delegates.notNull<AsteroidWorldsConfig>()
private set
var dungeonWorlds by Delegates.notNull<ImmutableMap<String, DungeonWorldsConfig>>()
private set
var grassDamage by Delegates.notNull<TileDamageConfig>()
private set
var treeDamage by Delegates.notNull<TileDamageConfig>()
private set
var bushDamage by Delegates.notNull<TileDamageConfig>()
private set
private object EmptyTask : ForkJoinTask<Unit>() { private object EmptyTask : ForkJoinTask<Unit>() {
private fun readResolve(): Any = EmptyTask private fun readResolve(): Any = EmptyTask
override fun getRawResult() { override fun getRawResult() {
@ -35,30 +66,44 @@ object GlobalDefaults {
} }
} }
private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>, executor: ExecutorService): Future<*> { private fun <T> load(path: String, accept: KMutableProperty0<T>, adapter: TypeAdapter<T>): Future<*> {
val file = Starbound.locate(path) val file = Starbound.loadJsonAsset(path)
if (!file.exists) { if (file == null) {
LOGGER.fatal("$path does not exist, expect bad things to happen!") LOGGER.fatal("$path does not exist or is not a file, expect bad things to happen!")
return EmptyTask
} else if (!file.isFile) {
LOGGER.fatal("$path is not a file, expect bad things to happen!")
return EmptyTask return EmptyTask
} else { } else {
return executor.submit { return Starbound.EXECUTOR.submit {
AssetPathStack("/") { try {
accept.set(Starbound.gson.fromJson(file.jsonReader(), T::class.java)) 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<Future<*>> { private inline fun <reified T> load(path: String, accept: KMutableProperty0<T>): Future<*> {
return load(path, accept, Starbound.gson.getAdapter(T::class.java))
}
fun load(): List<Future<*>> {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.add(load("/default_actor_movement.config", ::actorMovementParameters, executor)) tasks.add(load("/default_actor_movement.config", ::actorMovementParameters))
tasks.add(load("/default_movement.config", ::movementParameters, executor)) tasks.add(load("/default_movement.config", ::movementParameters))
tasks.add(load("/client.config", ::clientParameters, executor)) 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 return tasks
} }

View File

@ -1,14 +1,17 @@
package ru.dbotthepony.kstarbound 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.apache.logging.log4j.LogManager
import org.lwjgl.Version import org.lwjgl.Version
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.io.ByteKey import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource
@ -23,7 +26,6 @@ import java.io.DataInputStream
import java.io.File import java.io.File
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.util.* import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.Inflater import java.util.zip.Inflater
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
@ -38,10 +40,28 @@ fun main() {
val data = ServerUniverse() val data = ServerUniverse()
val t = System.nanoTime() 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(System.nanoTime() - t)
println(result)
data.close() data.close()
return return
@ -74,8 +94,8 @@ fun main() {
val server = IntegratedStarboundServer(File("./")) val server = IntegratedStarboundServer(File("./"))
val client = StarboundClient.create().get() val client = StarboundClient.create().get()
//val client2 = StarboundClient.create().get() //val client2 = StarboundClient.create().get()
val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false)) val world = ServerWorld(server, WorldGeometry(Vector2i(3000, 2000), true, false))
world.addChunkSource(LegacyChunkSource(db)) world.addChunkSource(LegacyChunkSource.file(db))
world.thread.start() world.thread.start()
//Starbound.addFilePath(File("./unpacked_assets/")) //Starbound.addFilePath(File("./unpacked_assets/"))

View File

@ -67,14 +67,14 @@ object RecipeRegistry {
} }
} }
fun load(fileTree: Map<String, List<IStarboundFile>>, executor: ExecutorService): List<Future<*>> { fun load(fileTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val files = fileTree["recipe"] ?: return emptyList() val files = fileTree["recipe"] ?: return emptyList()
val elements = Starbound.gson.getAdapter(JsonElement::class.java) val elements = Starbound.gson.getAdapter(JsonElement::class.java)
val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java) val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java)
return files.map { listedFile -> return files.map { listedFile ->
executor.submit { Starbound.EXECUTOR.submit {
try { try {
val json = elements.read(listedFile.jsonReader()) val json = elements.read(listedFile.jsonReader())
val value = recipes.fromJsonTree(json) val value = recipes.fromJsonTree(json)

View File

@ -1,11 +1,14 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject 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 com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction import ru.dbotthepony.kstarbound.defs.JsonFunction
import ru.dbotthepony.kstarbound.defs.Species import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition 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.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier import ru.dbotthepony.kstarbound.defs.tile.MaterialModifier
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition 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 ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.function.Supplier
import kotlin.collections.ArrayList
object Registries { object Registries {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val registries = ArrayList<Registry<*>>() private val registriesInternal = ArrayList<Registry<*>>()
val registries: List<Registry<*>> = Collections.unmodifiableList(registriesInternal)
private val adapters = ArrayList<TypeAdapterFactory>()
val tiles = Registry<TileDefinition>("tiles").also(registries::add) fun registerAdapters(gsonBuilder: GsonBuilder) {
val tileModifiers = Registry<MaterialModifier>("tile modifiers").also(registries::add) adapters.forEach { gsonBuilder.registerTypeAdapterFactory(it) }
val liquid = Registry<LiquidDefinition>("liquid").also(registries::add) }
val species = Registry<Species>("species").also(registries::add)
val statusEffects = Registry<StatusEffectDefinition>("status effects").also(registries::add) val tiles = Registry<TileDefinition>("tiles").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val particles = Registry<ParticleDefinition>("particles").also(registries::add) val tileModifiers = Registry<MaterialModifier>("tile modifiers").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val items = Registry<IItemDefinition>("items").also(registries::add) val liquid = Registry<LiquidDefinition>("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val questTemplates = Registry<QuestTemplate>("quest templates").also(registries::add) val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val techs = Registry<TechDefinition>("techs").also(registries::add) val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val jsonFunctions = Registry<JsonFunction>("json functions").also(registries::add) val particles = Registry<ParticleDefinition>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val json2Functions = Registry<Json2Function>("json 2functions").also(registries::add) val items = Registry<IItemDefinition>("item").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val npcTypes = Registry<NpcTypeDefinition>("npc types").also(registries::add) val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val projectiles = Registry<ProjectileDefinition>("projectiles").also(registries::add) val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val tenants = Registry<TenantDefinition>("tenants").also(registries::add) val jsonFunctions = Registry<JsonFunction>("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treasurePools = Registry<TreasurePoolDefinition>("treasure pools").also(registries::add) val json2Functions = Registry<Json2Function>("json 2function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skills").also(registries::add) val jsonConfigFunctions = Registry<JsonConfigFunction>("json config function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterTypes = Registry<MonsterTypeDefinition>("monster types").also(registries::add) val npcTypes = Registry<NpcTypeDefinition>("npc type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val worldObjects = Registry<ObjectDefinition>("objects").also(registries::add) val projectiles = Registry<ProjectileDefinition>("projectile").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val tenants = Registry<TenantDefinition>("tenant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treasurePools = Registry<TreasurePoolDefinition>("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val biomes = Registry<BiomeDefinition>("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val terrainSelectors = Registry<TerrainSelectorFactory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val grassVariants = Registry<GrassVariant.Data>("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treeFoliageVariants = Registry<TreeVariant.FoliageData>("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> { private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, Int?> {
return { mapper.invoke(it) to null } return { mapper.invoke(it) to null }
@ -69,33 +95,16 @@ object Registries {
return { mapper.invoke(it) to mapperInt.invoke(it) } return { mapper.invoke(it) to mapperInt.invoke(it) }
} }
fun validate(): Boolean { fun validate(): CompletableFuture<Boolean> {
var any = false val futures = ArrayList<CompletableFuture<Boolean>>()
any = !tiles.validate() || any for (registry in registriesInternal)
any = !tileModifiers.validate() || any futures.add(CompletableFuture.supplyAsync { registry.validate() })
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
return !any return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } }
} }
private inline fun <reified T : Any> loadRegistry( private inline fun <reified T : Any> loadRegistry(
executor: ExecutorService,
registry: Registry<T>, registry: Registry<T>,
files: List<IStarboundFile>, files: List<IStarboundFile>,
noinline keyProvider: (T) -> Pair<String, Int?> noinline keyProvider: (T) -> Pair<String, Int?>
@ -104,9 +113,10 @@ object Registries {
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) } val elementAdapter by lazy { Starbound.gson.getAdapter(JsonElement::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
executor.submit { Starbound.EXECUTOR.submit {
try { try {
AssetPathStack(listedFile.computeDirectory()) { AssetPathStack(listedFile.computeDirectory()) {
// TODO: json patch support
val elem = elementAdapter.read(listedFile.jsonReader()) val elem = elementAdapter.read(listedFile.jsonReader())
val read = adapter.fromJsonTree(elem) val read = adapter.fromJsonTree(elem)
val keys = keyProvider(read) val keys = keyProvider(read)
@ -126,35 +136,43 @@ object Registries {
} }
fun finishLoad() { fun finishLoad() {
registries.forEach { it.finishLoad() } registriesInternal.forEach { it.finishLoad() }
} }
fun load(fileTree: Map<String, List<IStarboundFile>>, executor: ExecutorService): List<Future<*>> { fun load(fileTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
tasks.addAll(loadItemDefinitions(fileTree, executor)) tasks.addAll(loadItemDefinitions(fileTree))
tasks.addAll(loadJsonFunctions(fileTree["functions"] ?: listOf(), executor)) tasks.addAll(loadTerrainSelectors(fileTree["terrain"] ?: listOf()))
tasks.addAll(loadJson2Functions(fileTree["2functions"] ?: listOf(), executor))
tasks.addAll(loadTreasurePools(fileTree["treasurepools"] ?: listOf(), executor))
tasks.addAll(loadRegistry(executor, tiles, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId))) tasks.addAll(loadRegistry(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(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(liquid, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)))
tasks.addAll(loadRegistry(executor, worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
tasks.addAll(loadRegistry(executor, statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
tasks.addAll(loadRegistry(executor, species, fileTree["species"] ?: listOf(), key(Species::kind))) tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind)))
tasks.addAll(loadRegistry(executor, particles, fileTree["particle"] ?: listOf(), key(ParticleDefinition::kind))) tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), key(ParticleDefinition::kind)))
tasks.addAll(loadRegistry(executor, questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
tasks.addAll(loadRegistry(executor, techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
tasks.addAll(loadRegistry(executor, npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
tasks.addAll(loadRegistry(executor, monsterSkills, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name))) 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 return tasks
} }
private fun loadItemDefinitions(files: Map<String, Collection<IStarboundFile>>, executor: ExecutorService): List<Future<*>> { private fun loadItemDefinitions(files: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
val fileMap = mapOf( val fileMap = mapOf(
"item" to ItemDefinition::class.java, "item" to ItemDefinition::class.java,
"currency" to CurrencyItemDefinition::class.java, "currency" to CurrencyItemDefinition::class.java,
@ -176,7 +194,7 @@ object Registries {
val adapter by lazy { Starbound.gson.getAdapter(clazz) } val adapter by lazy { Starbound.gson.getAdapter(clazz) }
for (listedFile in fileList) { for (listedFile in fileList) {
tasks.add(executor.submit { tasks.add(Starbound.EXECUTOR.submit {
try { try {
val json = objects.read(listedFile.jsonReader()) val json = objects.read(listedFile.jsonReader())
val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) } val def = AssetPathStack(listedFile.computeDirectory()) { adapter.fromJsonTree(json) }
@ -194,65 +212,46 @@ object Registries {
return tasks return tasks
} }
private fun loadJsonFunctions(files: Collection<IStarboundFile>, executor: ExecutorService): List<Future<*>> { private inline fun <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }
val elementAdapter by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
return files.map { listedFile -> return files.map { listedFile ->
executor.submit { Starbound.EXECUTOR.submit {
try { 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()) { for ((k, v) in json.entrySet()) {
try { try {
val fn = Starbound.gson.fromJson<JsonFunction>(JsonTreeReader(v), JsonFunction::class.java) val value = adapter.fromJsonTree(v)
jsonFunctions.add(k, fn, v, listedFile) transform(value, k)
registry.add {
registry.add(k, value, v, listedFile)
}
} catch (err: Exception) { } 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) { } catch (err: Exception) {
LOGGER.error("Loading json function definition $listedFile", err) LOGGER.error("Loading ${registry.name} definition $listedFile", err)
} }
} }
} }
} }
private fun loadJson2Functions(files: Collection<IStarboundFile>, executor: ExecutorService): List<Future<*>> { private fun loadTerrainSelectors(files: Collection<IStarboundFile>): List<Future<*>> {
return files.map { listedFile -> return files.map { listedFile ->
executor.submit { Starbound.EXECUTOR.submit {
try { 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()) { terrainSelectors.add {
try { terrainSelectors.add(factory.name, factory)
val fn = Starbound.gson.fromJson<Json2Function>(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)
}
} }
} catch (err: Exception) { } catch (err: Exception) {
LOGGER.error("Loading json 2function definition $listedFile", err) LOGGER.error("Loading terrain selector $listedFile", err)
}
}
}
}
private fun loadTreasurePools(files: Collection<IStarboundFile>, executor: ExecutorService): List<Future<*>> {
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<TreasurePoolDefinition>(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)
} }
} }
} }

View File

@ -1,16 +1,8 @@
package ru.dbotthepony.kstarbound package ru.dbotthepony.kstarbound
import com.google.gson.Gson
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject 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.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps 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 it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.Either 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 ru.dbotthepony.kstarbound.util.traverseJsonPath
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantLock
inline fun <reified S : Any> Registry<S>.adapter(): TypeAdapterFactory { import java.util.function.Supplier
return object : TypeAdapterFactory { import kotlin.collections.set
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { import kotlin.concurrent.withLock
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<Registry.Entry<S>>() {
override fun write(out: JsonWriter, value: Registry.Entry<S>?) {
if (value != null) {
out.value(value.key)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Entry<S>? {
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<Registry.Ref<S>>() {
override fun write(out: JsonWriter, value: Registry.Ref<S>?) {
if (value != null) {
value.key.map(out::value, out::value)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Ref<S>? {
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<T>?
}
}
}
class Registry<T : Any>(val name: String) { class Registry<T : Any>(val name: String) {
private val keysInternal = Object2ObjectOpenHashMap<String, Impl>() private val keysInternal = Object2ObjectOpenHashMap<String, Impl>()
@ -105,11 +33,13 @@ class Registry<T : Any>(val name: String) {
} }
fun finishLoad() { fun finishLoad() {
var next = backlog.poll() lock.withLock {
var next = backlog.poll()
while (next != null) { while (next != null) {
next.run() next.run()
next = backlog.poll() next = backlog.poll()
}
} }
} }
@ -193,7 +123,7 @@ class Registry<T : Any>(val name: String) {
} }
override fun toString(): 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<T> override val registry: Registry<T>
@ -203,6 +133,14 @@ class Registry<T : Any>(val name: String) {
operator fun get(index: String): Entry<T>? = lock.withLock { keysInternal[index] } operator fun get(index: String): Entry<T>? = lock.withLock { keysInternal[index] }
operator fun get(index: Int): Entry<T>? = lock.withLock { idsInternal[index] } operator fun get(index: Int): Entry<T>? = lock.withLock { idsInternal[index] }
fun getOrThrow(index: String): Entry<T> {
return get(index) ?: throw NoSuchElementException("No such $name: $index")
}
fun getOrThrow(index: Int): Entry<T> {
return get(index) ?: throw NoSuchElementException("No such $name: $index")
}
fun ref(index: String): Ref<T> = lock.withLock { fun ref(index: String): Ref<T> = lock.withLock {
keyRefs.computeIfAbsent(index, Object2ObjectFunction { keyRefs.computeIfAbsent(index, Object2ObjectFunction {
val ref = RefImpl(Either.left(it as String)) val ref = RefImpl(Either.left(it as String))
@ -227,29 +165,29 @@ class Registry<T : Any>(val name: String) {
operator fun contains(index: Int) = lock.withLock { index in idsInternal } operator fun contains(index: Int) = lock.withLock { index in idsInternal }
fun validate(): Boolean { fun validate(): Boolean {
var any = true var valid = true
keyRefs.values.forEach { keyRefs.values.forEach {
if (!it.isPresent) { if (!it.isPresent) {
LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") 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 { idRefs.values.forEach {
if (!it.isPresent) { if (!it.isPresent) {
LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") 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<T> { fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
lock.withLock { lock.withLock {
if (key in keysInternal) { if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
@ -275,11 +213,11 @@ class Registry<T : Any>(val name: String) {
fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry<T> { fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
lock.withLock { lock.withLock {
if (key in keysInternal) { if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
if (id in idsInternal) { if (id in idsInternal) {
LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
} }
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
@ -307,7 +245,7 @@ class Registry<T : Any>(val name: String) {
fun add(key: String, value: T, isBuiltin: Boolean = false): Entry<T> { fun add(key: String, value: T, isBuiltin: Boolean = false): Entry<T> {
lock.withLock { lock.withLock {
if (key in keysInternal) { if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })
@ -334,11 +272,11 @@ class Registry<T : Any>(val name: String) {
fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry<T> { fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry<T> {
lock.withLock { lock.withLock {
if (key in keysInternal) { if (key in keysInternal) {
LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name at '$key' (new def originate from <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
} }
if (id in idsInternal) { if (id in idsInternal) {
LOGGER.warn("Overwriting Registry entry with ID '$id' (new def originate from <code>; old def originate from ${idsInternal[id]?.file ?: "<code>"})") LOGGER.warn("Overwriting $name with ID '$id' (new def originate from <code>; old def originate from ${idsInternal[id]?.file ?: "<code>"})")
} }
val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) })

View File

@ -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 <reified S : Any> Registry<S>.adapter(): TypeAdapterFactory {
return RegistryTypeAdapterFactory(this, S::class)
}
class RegistryTypeAdapterFactory<S : Any>(private val registry: Registry<S>, private val clazz: KClass<S>) : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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<T>
} else if (type.rawType == Registry.Ref::class.java) {
return RefImpl(gson) as TypeAdapter<T>
}
return null
}
private inner class EntryImpl(gson: Gson) : TypeAdapter<Registry.Entry<S>>() {
override fun write(out: JsonWriter, value: Registry.Entry<S>?) {
if (value != null) {
out.value(value.key)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Entry<S>? {
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<Registry.Ref<S>>() {
override fun write(out: JsonWriter, value: Registry.Ref<S>?) {
if (value != null) {
value.key.map(out::value, out::value)
} else {
out.nullValue()
}
}
override fun read(`in`: JsonReader): Registry.Ref<S>? {
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()}")
}
}
}
}

View File

@ -4,10 +4,11 @@ import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.* import com.google.gson.*
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.AABBTypeAdapter import ru.dbotthepony.kommons.gson.AABBTypeAdapter
import ru.dbotthepony.kommons.gson.AABBiTypeAdapter import ru.dbotthepony.kommons.gson.AABBiTypeAdapter
import ru.dbotthepony.kommons.gson.ColorTypeAdapter
import ru.dbotthepony.kommons.gson.EitherTypeAdapter import ru.dbotthepony.kommons.gson.EitherTypeAdapter
import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter import ru.dbotthepony.kommons.gson.KOptionalTypeAdapter
import ru.dbotthepony.kommons.gson.NothingAdapter 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`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation
import ru.dbotthepony.kstarbound.defs.player.BlueprintLearnList 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.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.InternedJsonElementAdapter
import ru.dbotthepony.kstarbound.json.InternedStringAdapter import ru.dbotthepony.kstarbound.json.InternedStringAdapter
import ru.dbotthepony.kstarbound.json.LongRangeAdapter 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.BuilderAdapter
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonImplementationTypeFactory 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.ImmutableCollectionAdapterFactory
import ru.dbotthepony.kstarbound.json.factory.PairAdapterFactory 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.math.*
import ru.dbotthepony.kstarbound.server.world.UniverseChunk import ru.dbotthepony.kstarbound.server.world.UniverseChunk
import ru.dbotthepony.kstarbound.util.ItemStack import ru.dbotthepony.kstarbound.util.ItemStack
@ -52,10 +61,10 @@ import java.io.*
import java.lang.ref.Cleaner import java.lang.ref.Cleaner
import java.text.DateFormat import java.text.DateFormat
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors import java.util.concurrent.ExecutorService
import java.util.concurrent.ForkJoinPool import java.util.concurrent.ForkJoinPool
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.SynchronousQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -100,13 +109,22 @@ object Starbound : ISBFileLocator {
private val ioPoolCounter = AtomicInteger() 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()}") val thread = Thread(it, "Starbound Storage IO ${ioPoolCounter.getAndIncrement()}")
thread.isDaemon = true thread.isDaemon = true
thread.priority = Thread.MIN_PRIORITY thread.priority = Thread.MIN_PRIORITY
return@ThreadFactory thread 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 CLEANER: Cleaner = Cleaner.create {
val t = Thread(it, "Starbound Global Cleaner") val t = Thread(it, "Starbound Global Cleaner")
t.isDaemon = true t.isDaemon = true
@ -118,6 +136,7 @@ object Starbound : ISBFileLocator {
// Hrm. // Hrm.
// val strings: Interner<String> = Interner.newWeakInterner() // val strings: Interner<String> = Interner.newWeakInterner()
// val strings: Interner<String> = Interner { it } // val strings: Interner<String> = Interner { it }
@JvmField
val STRINGS: Interner<String> = interner(5) val STRINGS: Interner<String> = interner(5)
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
@ -141,14 +160,14 @@ object Starbound : ISBFileLocator {
// Обработчик @JsonImplementation // Обработчик @JsonImplementation
registerTypeAdapterFactory(JsonImplementationTypeFactory) registerTypeAdapterFactory(JsonImplementationTypeFactory)
// списки, наборы, т.п.
registerTypeAdapterFactory(CollectionAdapterFactory)
// ImmutableList, ImmutableSet, ImmutableMap // ImmutableList, ImmutableSet, ImmutableMap
registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS)) registerTypeAdapterFactory(ImmutableCollectionAdapterFactory(STRINGS))
// fastutil collections // fastutil collections
registerTypeAdapterFactory(FastutilTypeAdapterFactory(STRINGS)) registerTypeAdapterFactory(MapsTypeAdapterFactory(STRINGS))
// ArrayList
registerTypeAdapterFactory(ArrayListAdapterFactory)
// все enum'ы без особых настроек // все enum'ы без особых настроек
registerTypeAdapterFactory(EnumAdapter.Companion) registerTypeAdapterFactory(EnumAdapter.Companion)
@ -164,6 +183,8 @@ object Starbound : ISBFileLocator {
// KOptional<> // KOptional<>
registerTypeAdapterFactory(KOptionalTypeAdapter) registerTypeAdapterFactory(KOptionalTypeAdapter)
registerTypeAdapterFactory(SingletonTypeAdapterFactory)
// Pair<> // Pair<>
registerTypeAdapterFactory(PairAdapterFactory) registerTypeAdapterFactory(PairAdapterFactory)
registerTypeAdapterFactory(SBPattern.Companion) registerTypeAdapterFactory(SBPattern.Companion)
@ -173,7 +194,7 @@ object Starbound : ISBFileLocator {
registerTypeAdapter(ColorReplacements.Companion) registerTypeAdapter(ColorReplacements.Companion)
registerTypeAdapterFactory(BlueprintLearnList.Companion) registerTypeAdapterFactory(BlueprintLearnList.Companion)
registerTypeAdapter(ColorTypeAdapter.nullSafe()) registerTypeAdapter(RGBAColorTypeAdapter)
registerTypeAdapter(Drawable::Adapter) registerTypeAdapter(Drawable::Adapter)
registerTypeAdapter(ObjectOrientation::Adapter) registerTypeAdapter(ObjectOrientation::Adapter)
@ -197,10 +218,12 @@ object Starbound : ISBFileLocator {
registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER) registerTypeAdapter(JsonFunction.CONSTRAINT_ADAPTER)
registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER) registerTypeAdapter(JsonFunction.INTERPOLATION_ADAPTER)
registerTypeAdapter(JsonFunction.Companion) registerTypeAdapter(JsonFunction.Companion)
registerTypeAdapter(JsonConfigFunction::Adapter)
registerTypeAdapterFactory(Json2Function.Companion) registerTypeAdapterFactory(Json2Function.Companion)
// Общее // Общее
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS)) registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
registerTypeAdapterFactory(TerrainSelectorType.Companion)
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL)) registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL))
@ -223,24 +246,19 @@ object Starbound : ISBFileLocator {
registerTypeAdapterFactory(Poly.Companion) registerTypeAdapterFactory(Poly.Companion)
registerTypeAdapterFactory(Registries.tiles.adapter()) registerTypeAdapter(CelestialParameters::Adapter)
registerTypeAdapterFactory(Registries.tileModifiers.adapter())
registerTypeAdapterFactory(Registries.liquid.adapter()) registerTypeAdapterFactory(BiomePlacementDistributionType.DATA_ADAPTER)
registerTypeAdapterFactory(Registries.items.adapter()) registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(Registries.species.adapter()) registerTypeAdapterFactory(BiomePlacementItemType.DATA_ADAPTER)
registerTypeAdapterFactory(Registries.statusEffects.adapter()) registerTypeAdapterFactory(BiomePlacementItemType.DEFINITION_ADAPTER)
registerTypeAdapterFactory(Registries.particles.adapter()) registerTypeAdapterFactory(BiomePlaceables.Item.Companion)
registerTypeAdapterFactory(Registries.questTemplates.adapter())
registerTypeAdapterFactory(Registries.techs.adapter()) // register companion first, so it has lesser priority than dispatching adapter
registerTypeAdapterFactory(Registries.jsonFunctions.adapter()) registerTypeAdapterFactory(VisitableWorldParametersType.Companion)
registerTypeAdapterFactory(Registries.json2Functions.adapter()) registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER)
registerTypeAdapterFactory(Registries.npcTypes.adapter())
registerTypeAdapterFactory(Registries.projectiles.adapter()) Registries.registerAdapters(this)
registerTypeAdapterFactory(Registries.tenants.adapter())
registerTypeAdapterFactory(Registries.treasurePools.adapter())
registerTypeAdapterFactory(Registries.monsterSkills.adapter())
registerTypeAdapterFactory(Registries.monsterTypes.adapter())
registerTypeAdapterFactory(Registries.worldObjects.adapter())
registerTypeAdapter(LongRangeAdapter) registerTypeAdapter(LongRangeAdapter)
@ -418,7 +436,7 @@ object Starbound : ISBFileLocator {
checkMailbox() checkMailbox()
} }
private fun doInitialize(parallel: Boolean) { private fun doInitialize() {
if (!initializing && !initialized) { if (!initializing && !initialized) {
initializing = true initializing = true
} else { } else {
@ -464,11 +482,10 @@ object Starbound : ISBFileLocator {
checkMailbox() checkMailbox()
val tasks = ArrayList<Future<*>>() val tasks = ArrayList<Future<*>>()
val pool = if (parallel) ForkJoinPool.commonPool() else Executors.newFixedThreadPool(1)
tasks.addAll(Registries.load(ext2files, pool)) tasks.addAll(Registries.load(ext2files))
tasks.addAll(RecipeRegistry.load(ext2files, pool)) tasks.addAll(RecipeRegistry.load(ext2files))
tasks.addAll(GlobalDefaults.load(pool)) tasks.addAll(GlobalDefaults.load())
val total = tasks.size.toDouble() val total = tasks.size.toDouble()
@ -479,9 +496,6 @@ object Starbound : ISBFileLocator {
LockSupport.parkNanos(5_000_000L) LockSupport.parkNanos(5_000_000L)
} }
if (!parallel)
pool.shutdown()
Registries.finishLoad() Registries.finishLoad()
RecipeRegistry.finishLoad() RecipeRegistry.finishLoad()
@ -491,12 +505,12 @@ object Starbound : ISBFileLocator {
initialized = true initialized = true
} }
fun initializeGame(parallel: Boolean = true) { fun initializeGame(): Future<*> {
mailbox.submit { doInitialize(parallel) } return mailbox.submit { doInitialize() }
} }
fun bootstrapGame() { fun bootstrapGame(): Future<*> {
mailbox.submit { doBootstrap() } return mailbox.submit { doBootstrap() }
} }
private fun checkMailbox() { private fun checkMailbox() {

View File

@ -44,6 +44,7 @@ fun interface ISBFileLocator {
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist
*/ */
@Deprecated("This does not reflect json patches")
fun jsonReader(path: String) = locate(path).jsonReader() fun jsonReader(path: String) = locate(path).jsonReader()
} }
@ -157,6 +158,7 @@ interface IStarboundFile : ISBFileLocator {
* @throws IllegalStateException if file is a directory * @throws IllegalStateException if file is a directory
* @throws FileNotFoundException if file does not exist * @throws FileNotFoundException if file does not exist
*/ */
@Deprecated("This does not reflect json patches")
fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true } fun jsonReader(): JsonReader = JsonReader(reader()).also { it.isLenient = true }
/** /**

View File

@ -8,28 +8,33 @@ import io.netty.channel.local.LocalAddress
import io.netty.channel.local.LocalChannel import io.netty.channel.local.LocalChannel
import io.netty.channel.socket.nio.NioSocketChannel import io.netty.channel.socket.nio.NioSocketChannel
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.network.Connection import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IClientPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import java.net.SocketAddress import java.net.SocketAddress
import java.util.* import java.util.*
// client -> server // clientside part of connection
class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid: UUID) : Connection(ConnectionSide.CLIENT, type, uuid) { class ClientConnection(val client: StarboundClient, type: ConnectionType) : Connection(ConnectionSide.CLIENT, type) {
private fun sendHello() { private fun sendHello() {
isLegacy = false isLegacy = false
//sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION)) //sendAndFlush(ProtocolRequestPacket(Starbound.LEGACY_PROTOCOL_VERSION))
sendAndFlush(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION)) sendAndFlush(ProtocolRequestPacket(Starbound.NATIVE_PROTOCOL_VERSION))
} }
var connectionID: Int = -1
override fun inGame() { override fun inGame() {
} }
override fun toString(): String {
val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>"
return "ClientConnection[ID=$connectionID channel=$channel]"
}
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg is IClientPacket) { if (msg is IClientPacket) {
try { 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 { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID): ClientConnection { fun connectToLocalServer(client: StarboundClient, address: LocalAddress, uuid: UUID): ClientConnection {
LOGGER.info("Trying to connect to local server at $address with Client UUID $uuid") 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() Bootstrap()
.group(NIO_POOL) .group(NIO_POOL)
@ -69,7 +84,7 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType, uuid:
fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID): ClientConnection { fun connectToRemoteServer(client: StarboundClient, address: SocketAddress, uuid: UUID): ClientConnection {
LOGGER.info("Trying to connect to remote server at $address with Client UUID $uuid") 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() Bootstrap()
.group(NIO_POOL) .group(NIO_POOL)

View File

@ -11,19 +11,18 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.* import java.util.*
data class JoinWorldPacket(val uuid: UUID, val seed: Long, val geometry: WorldGeometry) : IClientPacket { data class JoinWorldPacket(val uuid: UUID, val geometry: WorldGeometry) : IClientPacket {
constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID(), buff.readLong(), WorldGeometry(buff)) constructor(buff: DataInputStream, isLegacy: Boolean) : this(buff.readUUID(), WorldGeometry(buff))
constructor(world: World<*, *>) : this(UUID(0L, 0L), world.seed, world.geometry) constructor(world: World<*, *>) : this(UUID(0L, 0L), world.geometry)
override fun write(stream: DataOutputStream, isLegacy: Boolean) { override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeUUID(uuid) stream.writeUUID(uuid)
stream.writeLong(seed)
geometry.write(stream) geometry.write(stream)
} }
override fun play(connection: ClientConnection) { override fun play(connection: ClientConnection) {
connection.client.mailbox.execute { connection.client.mailbox.execute {
connection.client.world = ClientWorld(connection.client, seed, geometry) connection.client.world = ClientWorld(connection.client, geometry)
} }
} }
} }

View File

@ -14,11 +14,15 @@ import ru.dbotthepony.kstarbound.client.gl.*
import ru.dbotthepony.kstarbound.client.gl.shader.UberShader import ru.dbotthepony.kstarbound.client.gl.shader.UberShader
import ru.dbotthepony.kstarbound.client.gl.vertex.* import ru.dbotthepony.kstarbound.client.gl.vertex.*
import ru.dbotthepony.kstarbound.defs.tile.* 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.ITileAccess
import ru.dbotthepony.kstarbound.world.api.AbstractTileState import ru.dbotthepony.kstarbound.world.api.AbstractTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import java.time.Duration import java.time.Duration
import java.util.concurrent.Callable import java.util.concurrent.Callable
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
/** /**
* Хранит в себе программы для отрисовки определённых [TileDefinition] * Хранит в себе программы для отрисовки определённых [TileDefinition]
@ -150,7 +154,7 @@ class TileRenderer(val renderers: TileRenderers, val def: IRenderableTile) {
var maxs = piece.texturePosition + piece.textureSize var maxs = piece.texturePosition + piece.textureSize
if (def.renderParameters.variants != 0 && piece.variantStride != null && piece.image == null) { 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 mins += piece.variantStride * variant
maxs += piece.variantStride * variant maxs += piece.variantStride * variant
} }

View File

@ -36,9 +36,8 @@ import kotlin.concurrent.withLock
class ClientWorld( class ClientWorld(
val client: StarboundClient, val client: StarboundClient,
seed: Long,
geometry: WorldGeometry, geometry: WorldGeometry,
) : World<ClientWorld, ClientChunk>(seed, geometry) { ) : World<ClientWorld, ClientChunk>(geometry) {
private fun determineChunkSize(cells: Int): Int { private fun determineChunkSize(cells: Int): Int {
for (i in 64 downTo 1) { for (i in 64 downTo 1) {
if (cells % i == 0) { if (cells % i == 0) {

View File

@ -7,6 +7,7 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
@ -46,6 +47,39 @@ class WeightedList<E>(val parent: ImmutableList<Pair<Double, E>>) {
return sample(random.nextDouble(sum)) return sample(random.nextDouble(sum))
} }
fun sample(amount: Int, random: RandomGenerator): List<E> {
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<E>()
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 { companion object : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == WeightedList::class.java) { if (type.rawType == WeightedList::class.java) {

View File

@ -9,8 +9,10 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
@ -27,23 +29,23 @@ data class AssetReference<V>(val path: String?, val fullPath: String?, val value
if (type.rawType == AssetReference::class.java) { if (type.rawType == AssetReference::class.java) {
val param = type.type as? ParameterizedType ?: return null val param = type.type as? ParameterizedType ?: return null
return object : TypeAdapter<AssetReference<Any>>() { return object : TypeAdapter<AssetReference<T>>() {
private val cache = ConcurrentHashMap<String, Pair<Any, JsonElement>>() private val cache = Collections.synchronizedMap(Object2ObjectOpenHashMap<String, Pair<T, JsonElement>>())
private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<Any> private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
private val strings = gson.getAdapter(String::class.java) private val strings = gson.getAdapter(String::class.java)
private val jsons = gson.getAdapter(JsonElement::class.java) private val jsons = gson.getAdapter(JsonElement::class.java)
private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>()) private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
override fun write(out: JsonWriter, value: AssetReference<Any>?) { override fun write(out: JsonWriter, value: AssetReference<T>?) {
if (value == null) if (value == null)
out.nullValue() out.nullValue()
else else
out.value(value.fullPath) out.value(value.fullPath)
} }
override fun read(`in`: JsonReader): AssetReference<Any>? { override fun read(`in`: JsonReader): AssetReference<T>? {
if (`in`.peek() == JsonToken.NULL) { if (`in`.consumeNull()) {
return null return null
} else if (`in`.peek() == JsonToken.STRING) { } else if (`in`.peek() == JsonToken.STRING) {
val path = strings.read(`in`)!! val path = strings.read(`in`)!!
@ -56,21 +58,16 @@ data class AssetReference<V>(val path: String?, val fullPath: String?, val value
if (fullPath in missing) if (fullPath in missing)
return null return null
val file = Starbound.locate(fullPath) val json = Starbound.loadJsonAsset(fullPath)
if (!file.exists) { if (json == null) {
logger.error("File does not exist: ${file.computeFullPath()}") logger.error("JSON asset does not exist: $fullPath")
missing.add(fullPath) missing.add(fullPath)
return AssetReference(path, fullPath, null, null) 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('/')) { val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
adapter.read(JsonTreeReader(json)) adapter.fromJsonTree(json)
} }
if (value == null) { if (value == null) {

View File

@ -6,6 +6,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
import ru.dbotthepony.kommons.gson.consumeNull
class ColorReplacements private constructor(private val mapping: Int2IntOpenHashMap) { class ColorReplacements private constructor(private val mapping: Int2IntOpenHashMap) {
constructor(mapping: Map<Int, Int>) : this(Int2IntOpenHashMap(mapping)) constructor(mapping: Map<Int, Int>) : this(Int2IntOpenHashMap(mapping))
@ -33,7 +34,7 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
} }
override fun read(`in`: JsonReader): ColorReplacements? { override fun read(`in`: JsonReader): ColorReplacements? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
else if (`in`.peek() == JsonToken.STRING) { else if (`in`.peek() == JsonToken.STRING) {
if (`in`.nextString() != "") if (`in`.nextString() != "")

View File

@ -9,6 +9,7 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
@JsonImplementation(ThingDescription::class) @JsonImplementation(ThingDescription::class)
@ -83,6 +84,13 @@ data class ThingDescription(
val EMPTY = 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<String> = Interner { it }) : TypeAdapterFactory { class Factory(val interner: Interner<String> = Interner { it }) : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == ThingDescription::class.java) { if (type.rawType == ThingDescription::class.java) {
@ -114,13 +122,13 @@ data class ThingDescription(
} }
override fun read(`in`: JsonReader): ThingDescription? { override fun read(`in`: JsonReader): ThingDescription? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
`in`.beginObject() `in`.beginObject()
var shortdescription = "..." var shortdescription: String? = null
var description = "..." var description: String? = null
val racial = ImmutableMap.Builder<String, String>() val racial = ImmutableMap.Builder<String, String>()
val racialShort = ImmutableMap.Builder<String, String>() val racialShort = ImmutableMap.Builder<String, String>()
@ -146,9 +154,18 @@ data class ThingDescription(
`in`.endObject() `in`.endObject()
if (shortdescription == null && description == null) {
shortdescription = "..."
description = "..."
} else if (shortdescription == null) {
shortdescription = description
} else if (description == null) {
description = shortdescription
}
return ThingDescription( return ThingDescription(
shortdescription = shortdescription, shortdescription = shortdescription!!,
description = description, description = description!!,
racialDescription = racial.build(), racialDescription = racial.build(),
racialShortDescription = racialShort.build() racialShortDescription = racialShort.build()
) )

View File

@ -36,8 +36,8 @@ data class ItemReference(
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == ItemReference::class.java) { if (type.rawType == ItemReference::class.java) {
return object : TypeAdapter<ItemReference>() { return object : TypeAdapter<ItemReference>() {
private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = false), gson, stringInterner) private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = false), gson, stringInterner)
private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(storesJson = false, asList = true), 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<Registry.Ref<IItemDefinition>> private val references = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, IItemDefinition::class.java)) as TypeAdapter<Registry.Ref<IItemDefinition>>
override fun write(out: JsonWriter, value: ItemReference?) { override fun write(out: JsonWriter, value: ItemReference?) {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.defs package ru.dbotthepony.kstarbound.defs
import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
@ -219,10 +220,10 @@ abstract class JsonDriven(val path: String) {
for ((k, v) in b.entrySet()) { for ((k, v) in b.entrySet()) {
val existing = a[k] val existing = a[k]
if (existing == null) { if (existing is JsonObject && v is JsonObject) {
a[k] = v.deepCopy()
} else if (existing is JsonObject && v is JsonObject) {
a[k] = mergeNoCopy(existing, v) a[k] = mergeNoCopy(existing, v)
} else if (existing !is JsonObject) {
a[k] = v.deepCopy()
} }
} }

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory 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.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter import ru.dbotthepony.kommons.gson.Vector2dTypeAdapter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.math.linearInterpolation import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.builder.EnumAdapter import ru.dbotthepony.kstarbound.json.builder.EnumAdapter
enum class JsonFunctionInterpolation { enum class JsonFunctionInterpolation {
@ -110,7 +114,7 @@ class JsonFunction(
} }
override fun read(reader: JsonReader): JsonFunction? { override fun read(reader: JsonReader): JsonFunction? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null return null
reader.beginArray() reader.beginArray()
@ -232,7 +236,7 @@ class Json2Function(
} }
override fun read(reader: JsonReader): Json2Function? { override fun read(reader: JsonReader): Json2Function? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null return null
reader.beginArray() reader.beginArray()
@ -323,3 +327,44 @@ class Json2Function(
} }
} }
} }
class JsonConfigFunction(val data: ImmutableList<Pair<Double, JsonElement>>) {
fun evaluate(point: Double): JsonElement? {
return data.lastOrNull { it.first <= point }?.second
}
fun <T> evaluate(point: Double, adapter: TypeAdapter<T>): KOptional<T> {
val eval = data.lastOrNull { it.first <= point }?.second ?: return KOptional()
return KOptional(adapter.fromJsonTree(eval))
}
class Adapter(gson: Gson) : TypeAdapter<JsonConfigFunction>() {
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<ImmutableList<Pair<Double, JsonElement>>>
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()))
}
}
}

View File

@ -17,6 +17,8 @@ import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
@Deprecated("Don't use directly, use one of subtypes instead")
@Suppress("DEPRECATION")
sealed class JsonReference<E : JsonElement?>(val path: String?, val fullPath: String?) { sealed class JsonReference<E : JsonElement?>(val path: String?, val fullPath: String?) {
abstract val value: E abstract val value: E

View File

@ -1,12 +1,14 @@
package ru.dbotthepony.kstarbound.defs 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 import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
data class PerlinNoiseParameters( data class PerlinNoiseParameters(
val type: Type = Type.PERLIN, val type: Type = Type.PERLIN,
val seed: Long? = null, val seed: Long? = null,
val scale: Int = 512, val scale: Int = DEFAULT_SCALE,
val octaves: Int = 1, val octaves: Int = 1,
val gain: Double = 2.0, val gain: Double = 2.0,
val offset: Double = 1.0, val offset: Double = 1.0,
@ -20,9 +22,21 @@ data class PerlinNoiseParameters(
require(scale >= 16) { "Too little perlin noise scale" } require(scale >= 16) { "Too little perlin noise scale" }
} }
enum class Type { enum class Type(val jsonName: String) : IStringSerializable {
PERLIN, PERLIN("perlin"),
BILLOW, BILLOW("billow"),
RIDGED_MULTI; 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
} }
} }

View File

@ -302,7 +302,7 @@ class Image private constructor(
private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) } private val objects by lazy { Starbound.gson.getAdapter(JsonObject::class.java) }
private val vectors by lazy { Starbound.gson.getAdapter(Vector4i::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 vectors2 by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val cache = ConcurrentHashMap<String, Optional<List<DataSprite>>>() private val configCache = ConcurrentHashMap<String, Optional<List<DataSprite>>>()
private val imageCache = ConcurrentHashMap<String, Optional<Image>>() private val imageCache = ConcurrentHashMap<String, Optional<Image>>()
private val logger = LogManager.getLogger() private val logger = LogManager.getLogger()
@ -505,7 +505,7 @@ class Image private constructor(
val name = path.substringBefore(':').substringAfterLast('/').substringBefore('.') val name = path.substringBefore(':').substringAfterLast('/').substringBefore('.')
while (true) { 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) if (find.isPresent)
return find.get() return find.get()

View File

@ -6,6 +6,7 @@ import com.google.gson.internal.bind.JsonTreeReader
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
@ -27,7 +28,7 @@ data class InventoryIcon(
} }
override fun read(`in`: JsonReader): InventoryIcon? { override fun read(`in`: JsonReader): InventoryIcon? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
if (`in`.peek() == JsonToken.STRING) { if (`in`.peek() == JsonToken.STRING) {

View File

@ -7,6 +7,7 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.image.SpriteReference
import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@ -53,7 +54,7 @@ interface IArmorItemDefinition : ILeveledItemDefinition, IScriptableItemDefiniti
} }
override fun read(`in`: JsonReader): Frames? { override fun read(`in`: JsonReader): Frames? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
else if (`in`.peek() == JsonToken.STRING) else if (`in`.peek() == JsonToken.STRING)
return Frames(frames.read(`in`), null, null) return Frames(frames.read(`in`), null, null)

View File

@ -10,6 +10,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory 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? { override fun read(`in`: JsonReader): BlueprintLearnList? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
val tiers = Int2ObjectArrayMap<ImmutableList<Entry>>() val tiers = Int2ObjectArrayMap<ImmutableList<Entry>>()

View File

@ -23,8 +23,8 @@ object BuiltinMetaMaterials {
damageFactors = Object2DoubleMaps.emptyMap(), damageFactors = Object2DoubleMaps.emptyMap(),
damageRecovery = 1.0, damageRecovery = 1.0,
maximumEffectTime = 0.0, maximumEffectTime = 0.0,
health = null, totalHealth = Double.MAX_VALUE,
harvestLevel = null, harvestLevel = Int.MAX_VALUE,
) )
)) ))

View File

@ -5,10 +5,10 @@ import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory @JsonFactory
class TileDamageConfig( data class TileDamageConfig(
val damageFactors: Object2DoubleMap<String> = Object2DoubleMaps.emptyMap(), val damageFactors: Object2DoubleMap<String> = Object2DoubleMaps.emptyMap(),
val damageRecovery: Double = 1.0, val damageRecovery: Double = 1.0,
val maximumEffectTime: Double = 0.0, val maximumEffectTime: Double = 1.5,
val health: Double? = null, val totalHealth: Double = 1.0,
val harvestLevel: Int? = null val harvestLevel: Int = 1,
) )

View File

@ -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<AssetPath>)
}

View File

@ -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<String> = ImmutableSet.of(),
val overrideTech: ImmutableSet<String>? = null,
val globalDirectives: ImmutableSet<String>? = 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<Terrain>,
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}" }
}
}

View File

@ -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
}
}
}

View File

@ -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<Shape>,
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<String>)
@JsonFactory
data class DataShape(val base: String, val mods: ImmutableMap<String, ImmutableList<String>> = ImmutableMap.of())
@JsonFactory
data class Data(
val name: String,
@JsonFlat
val descriptions: ThingDescription,
val shapes: ImmutableList<DataShape>,
val mods: ImmutableSet<String> = ImmutableSet.of(),
val ceiling: Boolean = false,
val ephemeral: Boolean = true,
val damageTable: AssetReference<TileDamageConfig>? = 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<Data>, 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())
)
}
}
}

View File

@ -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)
}
}

View File

@ -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.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonObject 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.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise 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<String> = WeightedList(),
val systemPrefixNames: WeightedList<String> = WeightedList(),
val systemSuffixNames: WeightedList<String> = WeightedList(),
val planetarySuffixes: ImmutableList<String> = ImmutableList.of(),
val satelliteSuffixes: ImmutableList<String> = 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<String> = WeightedList(),
val satelliteTypes: WeightedList<String> = WeightedList(),
)
@JsonFactory
data class CelestialPlanet(val parameters: CelestialParameters, val satellites: Int2ObjectMap<CelestialParameters>)
@JsonFactory @JsonFactory
data class CelestialGenerationInformation( data class CelestialGenerationInformation(
@ -127,7 +74,4 @@ data class CelestialGenerationInformation(
it.value.typeName = it.key it.value.typeName = it.key
} }
} }
} }
@JsonFactory
data class CelestialParameters(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject)

View File

@ -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<String> = WeightedList(),
val systemPrefixNames: WeightedList<String> = WeightedList(),
val systemSuffixNames: WeightedList<String> = WeightedList(),
val planetarySuffixes: ImmutableList<String> = ImmutableList.of(),
val satelliteSuffixes: ImmutableList<String> = ImmutableList.of(),
)

View File

@ -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<String> = WeightedList(),
val satelliteTypes: WeightedList<String> = WeightedList(),
)

View File

@ -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<CelestialParameters>() {
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)
}
}
}

View File

@ -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<CelestialParameters>)

View File

@ -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<Double, Vector2d>,
val airless: Boolean = false,
val environmentStatusEffects: ImmutableSet<String> = ImmutableSet.of(),
val overrideTech: ImmutableSet<String>? = null,
val globalDirectives: ImmutableSet<String>? = null,
val beamUpRule: BeamUpRule = BeamUpRule.SURFACE, // TODO: ??? why surface? in floating dungeon.
val disableDeathDrops: Boolean = false,
val worldEdgeForceRegions: WorldEdgeForceRegion = WorldEdgeForceRegion.TOP,
val weatherPool: WeightedList<String>? = 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}" }
}
}

View File

@ -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
}
}
}

View File

@ -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<String> = 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<String>,
val ceiling: Boolean = false,
val ephemeral: Boolean = true,
val description: String = name,
val damageTable: AssetReference<TileDamageConfig>? = 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<Data>, 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)
)
}
}
}

View File

@ -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<Layer> = ImmutableList.of(),
) {
@JsonFactory
data class Data(
val verticalOrigin: Double = 0.0,
val layers: ImmutableList<DataLayer>,
) {
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<Double, Vector2d>,
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<String>()
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<String>,
val alpha: Double,
val parallaxValue: Vector2d,
val repeat: Pair<Boolean, Boolean>,
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")
}
}
}
}

View File

@ -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, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
val dayColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
val eveningColors: Pair<RGBAColor, RGBAColor> = RGBAColor.BLACK to RGBAColor.BLACK,
val nightColors: Pair<RGBAColor, RGBAColor> = 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<Pair<String, String>>)
class SkyParameters() {
var skyType = SkyType.BARREN
var seed = 0L
var dayLength: Double? = null
var horizonClouds = false
var skyColoring: Either<SkyColoring, RGBAColor> = Either.right(RGBAColor.BLACK)
var spaceLevel: Double? = null
var surfaceLevel: Double? = null
var nearbyPlanet: Pair<List<Pair<String, Double>>, 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
}
}
}

View File

@ -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<String>? = null,
val globalDirectives: ImmutableSet<String>? = 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<Int, String>?,
val caveLiquidSeedDensity: Double,
val oceanLiquid: Either<Int, String>?,
val oceanLiquidLevel: Int,
val encloseLiquids: Boolean,
val fillMicrodungeons: Boolean,
)
@JsonFactory
data class Layer(
val layerMinHeight: Int,
val layerBaseHeight: Int,
val dungeons: ImmutableSet<String>,
val dungeonXVariance: Int,
val primaryRegion: Region,
val primarySubRegion: Region,
val secondaryRegions: ImmutableList<Region>,
val secondarySubRegions: ImmutableList<Region>,
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<Int, String>?
// 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<Int, String>?,
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<Layer>,
val coreLayer: Layer,
)
var primaryBiome: String by Delegates.notNull()
private set
var primarySurfaceLiquid: Either<Int, String>? = 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<Layer> 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<Double, JsonArray>() }
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<WeightedList<String>> }
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<Region, Region> {
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<Pair<Region, Region>>()
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<Layer>()
var ulayer = readLayer("underground${undergroundLayers.size + 1}")
while (ulayer != null) {
undergroundLayers.add(ulayer)
ulayer = readLayer("underground${undergroundLayers.size + 1}")
}
parameters.undergroundLayers = undergroundLayers
return parameters
}
}
}

View File

@ -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<String, JsonObject>,
val planetDefaults: JsonObject,
val planetSizes: ImmutableMap<String, JsonObject>,
val planetTypes: ImmutableMap<String, JsonObject>,
)

View File

@ -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<TileDamageConfig>? = 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<StemData>, 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<StemData>, stemHueShift: Double, fdata: Registry.Entry<FoliageData>, 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,
)
}
}
}

View File

@ -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,
)

View File

@ -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<out VisitableWorldParameters>) : 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 <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (VisitableWorldParameters::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<VisitableWorldParameters>() {
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<T>
}
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<String> by Delegates.notNull()
protected set
var overrideTech: Set<String>? = null
protected set
var globalDirectives: Set<String>? = 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<String>? = 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<Double, Vector2d>,
val airless: Boolean,
val environmentStatusEffects: Set<String>,
val overrideTech: Set<String>?,
val globalDirectives: Set<String>?,
val beamUpRule: BeamUpRule,
val disableDeathDrops: Boolean,
val terraformed: Boolean,
val worldEdgeForceRegions: WorldEdgeForceRegion,
val weatherPool: WeightedList<String>?,
)
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
}
}

View File

@ -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<AbstractTerrainSelector<*>>()
val biomes = ListInterner<Biome>()
val playerStartSearchRegions = ArrayList<AABBi>()
val layers = ArrayList<Layer>()
private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>()
@JsonFactory
data class SerializedLayer(
val yStart: Int,
val boundaries: IntArrayList,
val cells: ImmutableList<JsonObject>,
)
inner class Layer(val yStart: Int) : Comparable<Layer> {
val boundaries = IntArrayList()
val cells = ArrayList<Region>()
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<Int, String>? = null,
val caveLiquidSeedDensity: Double = 0.0,
val oceanLiquid: Either<Int, String>? = 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<AbstractTerrainSelector<*>>,
val foregroundOreSelector: List<AbstractTerrainSelector<*>>,
val backgroundOreSelector: List<AbstractTerrainSelector<*>>,
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<AABBi>,
val biomes: List<Biome>,
val terrainSelectors: List<AbstractTerrainSelector<*>>,
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<AbstractTerrainSelector<*>>()
val foregroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
val backgroundOreSelector = ArrayList<AbstractTerrainSelector<*>>()
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<RegionParameters>, secondarySubRegions: List<RegionParameters>,
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<Biome>()
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)
}
}
}

View File

@ -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<WorldGeometry, Vector2i>,
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
}
}
}

View File

@ -0,0 +1,8 @@
package ru.dbotthepony.kstarbound.defs.world
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class WorldTemplateConfig(
val playerStartSearchYRange: Int,
)

View File

@ -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<D : Any>(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]"
}
}

View File

@ -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<MaterialModifier>? = null,
val ceilingGrassMod: Registry.Entry<MaterialModifier>? = null,
val grassModDensity: Double = 0.0,
val ceilingGrassModDensity: Double = 0.0,
val items: ImmutableList<DistributionItem> = 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 <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Item::class.java.isAssignableFrom(type.rawType)) {
return object : TypeAdapter<Item>() {
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<TreeVariant>()
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<T>
}
return null
}
}
}
data class MicroDungeon(val microdungeons: ImmutableSet<String> = 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<TreeVariant>) : 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<WeightedList<Pair<String, JsonElement>>>()
// 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<Pair<String, JsonElement>>) : 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<Item>
}
@JsonFactory
data class RandomDistribution(
val blockSeed: Long,
val randomItems: ImmutableList<Item>,
) : DistributionData() {
override val type: BiomePlacementDistributionType
get() = BiomePlacementDistributionType.RANDOM
override fun itemStream(): Stream<Item> {
return randomItems.stream()
}
}
@JsonFactory
data class PeriodicDistribution(
val modulus: Int,
val modulusOffset: Int,
val densityFunction: AbstractPerlinNoise,
val modulusDistortion: AbstractPerlinNoise,
val weightedItems: ImmutableList<Pair<Item, AbstractPerlinNoise>>,
) : DistributionData() {
override val type: BiomePlacementDistributionType
get() = BiomePlacementDistributionType.PERIODIC
override fun itemStream(): Stream<Item> {
return weightedItems.stream().map { it.first }
}
}
}
data class Biome(
val hueShift: Double = 0.0,
val baseName: String,
val description: String,
val mainBlock: Registry.Entry<TileDefinition>? = null,
val subBlocks: ImmutableList<Registry.Entry<TileDefinition>> = ImmutableList.of(),
val ores: ImmutableList<Pair<Registry.Entry<MaterialModifier>, Double>> = ImmutableList.of(),
val musicTrack: AmbientNoisesDefinition? = null,
val ambientNoises: AmbientNoisesDefinition? = null,
val surfacePlaceables: BiomePlaceables = BiomePlaceables(),
val undergroundPlaceables: BiomePlaceables = BiomePlaceables(),
val parallax: Parallax? = null,
)

View File

@ -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<out BiomePlaceablesDefinition.DistributionData>,
val data: TypeToken<out BiomePlaceables.DistributionData>,
) : 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<out BiomePlaceablesDefinition.DistributionItemData>,
val data: TypeToken<out BiomePlaceables.Item>,
) : 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<Registry.Ref<MaterialModifier>> = ImmutableList.of(),
val grassModDensity: Double = 0.0,
val ceilingGrassMod: ImmutableList<Registry.Ref<MaterialModifier>> = ImmutableList.of(),
val ceilingGrassModDensity: Double = 0.0,
val items: ImmutableList<DistributionItem> = 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<DistributionData>,
@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<String> = 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<String> = 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<Registry.Ref<GrassVariant.Data>> = 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<Registry.Ref<TreeVariant.StemData>> = ImmutableList.of(),
val treeFoliageList: ImmutableList<Registry.Ref<TreeVariant.FoliageData>> = 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<Pair<Registry.Entry<TreeVariant.StemData>, Registry.Entry<TreeVariant.FoliageData>?>>()
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<TreeVariant>()
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<BushVariant.Data>,
val baseHueShiftMax: Double = 0.0,
val modHueShiftMax: Double = 0.0,
)
@JsonFactory
data class Bush(val bushes: ImmutableSet<BushData> = 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<Pair<Double, String>> = ImmutableList.of(), val parameters: JsonElement = JsonObject())
@JsonFactory
data class Object(val objectSets: ImmutableList<ObjectPool>) : 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<String> = ImmutableSet.of(),
val weather: ImmutableList<Pair<Double, ImmutableList<AssetReference<WeightedList<String>>>>> = ImmutableList.of(), // binned reference to other assets
val hueShiftOptions: ImmutableList<Double> = ImmutableList.of(),
val skyOptions: ImmutableList<SkyColoring> = ImmutableList.of(),
val description: String = "...",
val mainBlock: Registry.Ref<TileDefinition>? = null,
val subBlocks: ImmutableList<Registry.Ref<TileDefinition>>? = null,
val ores: Registry.Ref<JsonConfigFunction>? = null,
val musicTrack: AmbientNoisesDefinition? = null,
val ambientNoises: AmbientNoisesDefinition? = null,
val surfacePlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val undergroundPlaceables: BiomePlaceablesDefinition = BiomePlaceablesDefinition(),
val parallax: AssetReference<Parallax.Data>? = 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<Pair<Registry.Entry<MaterialModifier>, Double>>
)
}
companion object {
private val oresAdapter by lazy {
Starbound.gson.pairListAdapter<String, Double>()
}
}
}

View File

@ -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<ConstantTerrainSelector.Data>(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
}
}

View File

@ -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<DisplacementTerrainSelector.Data>(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
}

View File

@ -0,0 +1,7 @@
package ru.dbotthepony.kstarbound.defs.world.terrain
class TerrainSelectorFactory<D : Any, out T : AbstractTerrainSelector<D>>(val name: String, private val data: D, private val factory: (String, D, TerrainSelectorParameters) -> T) {
fun create(parameters: TerrainSelectorParameters): T {
return factory(name, data, parameters)
}
}

View File

@ -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 }
}
}

View File

@ -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<AbstractTerrainSelector<*>>(), TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (AbstractTerrainSelector::class.java.isAssignableFrom(type.rawType)) {
return this as TypeAdapter<T>
}
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)
}
}
}

View File

@ -4,11 +4,14 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.util.IStruct2d import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.io.readDouble import ru.dbotthepony.kommons.io.readDouble
import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readLong import ru.dbotthepony.kommons.io.readLong
import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeDouble import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeLong import ru.dbotthepony.kommons.io.writeLong
import ru.dbotthepony.kommons.io.writeSignedVarInt import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
@ -40,3 +43,14 @@ fun InputStream.readHeader(header: String) {
fun InputStream.readChunkPos(): ChunkPos { fun InputStream.readChunkPos(): ChunkPos {
return ChunkPos(readSignedVarInt(), readSignedVarInt()) 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())
}

View File

@ -12,6 +12,7 @@ import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAdapter<JsonElement>() { class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAdapter<JsonElement>() {
override fun write(out: JsonWriter, value: JsonElement?) { override fun write(out: JsonWriter, value: JsonElement?) {
@ -36,7 +37,7 @@ class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAda
} }
override fun read(`in`: JsonReader): JsonObject? { override fun read(`in`: JsonReader): JsonObject? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
val output = JsonObject() val output = JsonObject()
@ -53,7 +54,7 @@ class InternedJsonElementAdapter(val stringInterner: Interner<String>) : TypeAda
} }
override fun read(`in`: JsonReader): JsonArray? { override fun read(`in`: JsonReader): JsonArray? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
val output = JsonArray() val output = JsonArray()
@ -78,7 +79,7 @@ class InternedStringAdapter(val stringInterner: Interner<String>) : TypeAdapter<
} }
override fun read(`in`: JsonReader): String? { override fun read(`in`: JsonReader): String? {
if (`in`.peek() == JsonToken.NULL) if (`in`.consumeNull())
return null return null
if (`in`.peek() == JsonToken.BOOLEAN) if (`in`.peek() == JsonToken.BOOLEAN)

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.json package ru.dbotthepony.kstarbound.json
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
@ -21,6 +22,10 @@ inline fun <reified C : Collection<E>, reified E> Gson.collectionAdapter(): Type
return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter<C> return getAdapter(TypeToken.getParameterized(C::class.java, E::class.java)) as TypeAdapter<C>
} }
inline fun <reified K, reified V> Gson.mapAdapter(): TypeAdapter<ImmutableMap<K, V>> {
return getAdapter(TypeToken.getParameterized(ImmutableMap::class.java, K::class.java, V::class.java)) as TypeAdapter<ImmutableMap<K, V>>
}
inline fun <reified E> Gson.listAdapter(): TypeAdapter<ImmutableList<E>> { inline fun <reified E> Gson.listAdapter(): TypeAdapter<ImmutableList<E>> {
return collectionAdapter() return collectionAdapter()
} }

View File

@ -4,6 +4,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
object LongRangeAdapter : TypeAdapter<LongRange>() { object LongRangeAdapter : TypeAdapter<LongRange>() {
override fun write(out: JsonWriter, value: LongRange?) { override fun write(out: JsonWriter, value: LongRange?) {
@ -18,7 +19,7 @@ object LongRangeAdapter : TypeAdapter<LongRange>() {
} }
override fun read(`in`: JsonReader): LongRange? { override fun read(`in`: JsonReader): LongRange? {
if (`in`.peek() == JsonToken.NULL) { if (`in`.consumeNull()) {
return null return null
} else { } else {
`in`.beginArray() `in`.beginArray()

View File

@ -65,7 +65,6 @@ annotation class JsonBuilder
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonFactory( annotation class JsonFactory(
val storesJson: Boolean = false,
val asList: Boolean = false, val asList: Boolean = false,
val logMisses: Boolean = false, val logMisses: Boolean = false,
) )
@ -89,6 +88,10 @@ annotation class JsonFactory(
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonImplementation(val implementingClass: KClass<*>) annotation class JsonImplementation(val implementingClass: KClass<*>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonSingleton
object JsonImplementationTypeFactory : TypeAdapterFactory { object JsonImplementationTypeFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val delegate = type.rawType.getAnnotation(JsonImplementation::class.java) val delegate = type.rawType.getAnnotation(JsonImplementation::class.java)

View File

@ -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 <reified E : Any, reified C : Any> DispatchingAdapter(
key: String,
noinline value2type: C.() -> E,
noinline type2value: E.() -> TypeToken<out C>,
elements: Collection<E>,
): DispatchingAdapter<E, C> {
return DispatchingAdapter(key, value2type, type2value, elements, E::class.java, C::class.java)
}
class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
val key: String,
val value2type: ELEMENT.() -> TYPE,
val type2value: TYPE.() -> TypeToken<out ELEMENT>,
val types: Collection<TYPE>,
val typeClass: Class<TYPE>,
val baseValueClass: Class<ELEMENT>
) : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == baseValueClass) {
return Impl(gson) as TypeAdapter<T>
}
return null
}
private inner class Impl(gson: Gson) : TypeAdapter<ELEMENT>() {
private val typeAdapter = gson.getAdapter(typeClass)
private val adapters = types.associateWith { gson.getAdapter(type2value.invoke(it)) } as Map<TYPE, TypeAdapter<ELEMENT>>
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)
}
}
}

View File

@ -14,8 +14,9 @@ import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.util.sbIntern import ru.dbotthepony.kstarbound.util.sbIntern
import java.util.Arrays import java.util.*
import java.util.stream.Stream import java.util.stream.Stream
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf import kotlin.reflect.full.isSuperclassOf
@ -25,27 +26,22 @@ interface IStringSerializable {
fun write(out: JsonWriter) fun write(out: JsonWriter)
} }
@Suppress("FunctionName") inline fun <reified T : Enum<T>> EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
inline fun <reified T : Enum<T>>EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, values, default) return EnumAdapter(T::class, values, default)
} }
@Suppress("FunctionName") inline fun <reified T : Enum<T>> EnumAdapter(values: Iterator<T>, default: T? = null): EnumAdapter<T> {
inline fun <reified T : Enum<T>>EnumAdapter(values: Iterator<T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, Streams.stream(values), default) return EnumAdapter(T::class, Streams.stream(values), default)
} }
@Suppress("FunctionName") inline fun <reified T : Enum<T>> EnumAdapter(values: Array<out T>, default: T? = null): EnumAdapter<T> {
inline fun <reified T : Enum<T>>EnumAdapter(values: Array<out T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, Arrays.stream(values), default) return EnumAdapter(T::class, Arrays.stream(values), default)
} }
@Suppress("FunctionName") inline fun <reified T : Enum<T>> EnumAdapter(values: Collection<T>, default: T? = null): EnumAdapter<T> {
inline fun <reified T : Enum<T>>EnumAdapter(values: Collection<T>, default: T? = null): EnumAdapter<T> {
return EnumAdapter(T::class, values.stream(), default) return EnumAdapter(T::class, values.stream(), default)
} }
@Suppress("name_shadowing")
class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter<T?>() { class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> = Arrays.stream(enum.java.enumConstants), val default: T? = null) : TypeAdapter<T?>() {
constructor(clazz: Class<T>, values: Stream<T> = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default) constructor(clazz: Class<T>, values: Stream<T> = Arrays.stream(clazz.enumConstants), default: T? = null) : this(clazz.kotlin, values, default)
@ -60,7 +56,7 @@ class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> =
private val values = values.collect(ImmutableList.toImmutableList()) private val values = values.collect(ImmutableList.toImmutableList())
private val mapping: ImmutableMap<String, T> private val mapping: ImmutableMap<String, T>
private val areCustom = IStringSerializable::class.java.isAssignableFrom(enum.java) private val areCustom = IStringSerializable::class.java.isAssignableFrom(enum.java)
private val misses = ObjectOpenHashSet<String>() private val misses = Collections.synchronizedSet(ObjectOpenHashSet<String>())
init { init {
val builder = Object2ObjectArrayMap<String, T>() val builder = Object2ObjectArrayMap<String, T>()
@ -88,6 +84,8 @@ class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> =
override fun write(out: JsonWriter, value: T?) { override fun write(out: JsonWriter, value: T?) {
if (value == null) { if (value == null) {
out.nullValue() out.nullValue()
} else if (value is IStringSerializable) {
value.write(out)
} else { } else {
out.value(value.name) out.value(value.name)
} }
@ -95,7 +93,7 @@ class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> =
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
override fun read(`in`: JsonReader): T? { override fun read(`in`: JsonReader): T? {
if (`in`.peek() == JsonToken.NULL) { if (`in`.consumeNull()) {
return null return null
} }

View File

@ -17,6 +17,7 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter 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.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
@ -48,7 +49,6 @@ class FactoryAdapter<T : Any> private constructor(
val types: ImmutableList<ReferencedProperty<T, *>>, val types: ImmutableList<ReferencedProperty<T, *>>,
aliases: Map<String, List<String>>, aliases: Map<String, List<String>>,
val asJsonArray: Boolean, val asJsonArray: Boolean,
val storesJson: Boolean,
val stringInterner: Interner<String>, val stringInterner: Interner<String>,
val logMisses: Boolean, val logMisses: Boolean,
private val elements: TypeAdapter<JsonElement> private val elements: TypeAdapter<JsonElement>
@ -78,10 +78,7 @@ class FactoryAdapter<T : Any> private constructor(
* Обычный конструктор класса (без флагов "значения по умолчанию") * Обычный конструктор класса (без флагов "значения по умолчанию")
*/ */
private val regularFactory: KFunction<T> = clazz.constructors.firstOrNull first@{ private val regularFactory: KFunction<T> = clazz.constructors.firstOrNull first@{
var requiredSize = types.size val requiredSize = types.size
if (storesJson)
requiredSize++
if (it.parameters.size == requiredSize) { if (it.parameters.size == requiredSize) {
val iterator = types.iterator() val iterator = types.iterator()
@ -99,20 +96,6 @@ class FactoryAdapter<T : Any> 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 return@first true
} }
@ -125,12 +108,6 @@ class FactoryAdapter<T : Any> private constructor(
private val syntheticFactory: Constructor<T>? = try { private val syntheticFactory: Constructor<T>? = try {
val typelist = types.map { (it.type.classifier as KClass<*>).java }.toMutableList() 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)) for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1))
typelist.add(Int::class.java) typelist.add(Int::class.java)
@ -141,7 +118,7 @@ class FactoryAdapter<T : Any> private constructor(
null null
} }
private val syntheticPrimitives = Int2ObjectOpenHashMap<Any>() private val syntheticPrimitives = Int2ObjectAVLTreeMap<Any>()
init { init {
if (syntheticFactory != null) { if (syntheticFactory != null) {
@ -201,27 +178,14 @@ class FactoryAdapter<T : Any> private constructor(
return null return null
// таблица присутствия значений (если значение true то на i было значение внутри json) // таблица присутствия значений (если значение true то на i было значение внутри json)
val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0)) val presentValues = BooleanArray(types.size)
var readValues = arrayOfNulls<Any>(types.size + (if (storesJson) 1 else 0)) var readValues = arrayOfNulls<Any>(types.size)
if (storesJson)
presentValues[presentValues.size - 1] = true
@Suppress("name_shadowing") @Suppress("name_shadowing")
var reader = reader var reader = reader
// Если нам необходимо читать объект как набор данных массива, то давай // Если нам необходимо читать объект как набор данных массива, то давай
if (asJsonArray) { 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<Any>, stringInterner)
}
reader.beginArray() reader.beginArray()
val iterator = types.iterator() val iterator = types.iterator()
var fieldId = 0 var fieldId = 0
@ -230,7 +194,7 @@ class FactoryAdapter<T : Any> private constructor(
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
val name = fieldId.toString() 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") LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
} }
@ -255,7 +219,7 @@ class FactoryAdapter<T : Any> private constructor(
} else { } else {
var json: JsonObject by Delegates.notNull() var json: JsonObject by Delegates.notNull()
if (storesJson || types.any { it.isFlat }) { if (types.any { it.isFlat }) {
val readMap = elements.read(reader) val readMap = elements.read(reader)
if (readMap !is JsonObject) if (readMap !is JsonObject)
@ -263,9 +227,6 @@ class FactoryAdapter<T : Any> private constructor(
json = readMap json = readMap
reader = JsonTreeReader(readMap) reader = JsonTreeReader(readMap)
if (storesJson)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap, stringInterner) as Map<String, Any>, stringInterner)
} }
reader.beginObject() reader.beginObject()
@ -275,7 +236,7 @@ class FactoryAdapter<T : Any> private constructor(
val fields = name2index[name] val fields = name2index[name]
if (fields == null || fields.size == 0) { 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") LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
} }
@ -393,7 +354,7 @@ class FactoryAdapter<T : Any> private constructor(
if (readValues[i] != null) continue if (readValues[i] != null) continue
val param = regularFactory.parameters[i] 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] readValues[i] = syntheticPrimitives[i]
} else if (!param.isOptional) { } else if (!param.isOptional) {
if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing") if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing")
@ -401,7 +362,11 @@ class FactoryAdapter<T : Any> 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<T : Any> private constructor(
clazz = clazz, clazz = clazz,
types = ImmutableList.copyOf(types.also { it.forEach{ it.resolve(gson) } }), types = ImmutableList.copyOf(types.also { it.forEach{ it.resolve(gson) } }),
asJsonArray = asList, asJsonArray = asList,
storesJson = storesJson,
stringInterner = stringInterner, stringInterner = stringInterner,
aliases = aliases, aliases = aliases,
logMisses = logMisses, logMisses = logMisses,
@ -448,19 +412,6 @@ class FactoryAdapter<T : Any> private constructor(
) )
} }
/**
* Принимает ли класс *последним* аргументом JSON структуру
*
* На самом деле, JSON "заворачивается" в [ImmutableMap], или [ImmutableList] если указано [asList]/[inputAsList]
*
* Поэтому, конструктор класса ОБЯЗАН принимать [Map]/[ImmutableMap] или [List]/[ImmutableList] последним аргументом,
* иначе поиск конструктора завершится неудачей
*/
fun storesJson(flag: Boolean = true): Builder<T> {
storesJson = flag
return this
}
fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, isMarkedNullable: Boolean? = null): Builder<T> { fun <V> add(field: KProperty1<T, V>, isFlat: Boolean = false, isMarkedNullable: Boolean? = null): Builder<T> {
types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable)) types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable))
return this return this
@ -517,7 +468,6 @@ class FactoryAdapter<T : Any> private constructor(
builder.inputAsList() builder.inputAsList()
} }
builder.storesJson(config.storesJson)
builder.stringInterner = stringInterner builder.stringInterner = stringInterner
builder.logMisses = config.logMisses builder.logMisses = config.logMisses
@ -528,7 +478,7 @@ class FactoryAdapter<T : Any> private constructor(
val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}") val foundConstructor = kclass.primaryConstructor ?: throw NoSuchElementException("Can't determine primary constructor for ${kclass.qualifiedName}")
val params = foundConstructor.parameters 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) { for (i in 0 until lastIndex) {
val argument = params[i] val argument = params[i]

View File

@ -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<V>(val instance: V) : TypeAdapter<V>() {
override fun write(out: JsonWriter, value: V) {
out.nullValue()
}
override fun read(`in`: JsonReader): V {
return instance
}
}

View File

@ -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 <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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<T>
}
return null
}
}

View File

@ -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<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ArrayList<E>>() {
override fun write(out: JsonWriter, value: ArrayList<E>?) {
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<E>? {
if (reader.peek() == JsonToken.NULL)
return null
if (reader.peek() != JsonToken.BEGIN_ARRAY) {
// не массив, возможно упрощение структуры "a": [value] -> "a": value
val list = ArrayList<E>(1)
list.add(elementAdapter.read(reader))
return list
}
reader.beginArray()
val list = ArrayList<E>()
while (reader.peek() != JsonToken.END_ARRAY) {
list.add(elementAdapter.read(reader))
}
reader.endArray()
return list
}
}

View File

@ -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 <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.type is ParameterizedType) {
val parentType = TypeToken.get((type.type as ParameterizedType).actualTypeArguments[0]) as TypeToken<Any>
return when (type.rawType) {
ArrayList::class.java -> Adapter<ArrayList<Any>, Any>(::ArrayList, gson.getAdapter(parentType))
List::class.java -> Adapter<ArrayList<Any>, Any>(::ArrayList, gson.getAdapter(parentType))
Set::class.java -> Adapter<ObjectOpenHashSet<Any>, Any>(::ObjectOpenHashSet, gson.getAdapter(parentType))
ObjectOpenHashSet::class.java -> Adapter<ObjectOpenHashSet<Any>, Any>(::ObjectOpenHashSet, gson.getAdapter(parentType))
else -> null
} as TypeAdapter<T>?
} 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<T>?
}
}
private class Adapter<C : MutableCollection<E>, E>(val factory: () -> C, val parent: TypeAdapter<E>) : TypeAdapter<C>() {
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
}
}
}

View File

@ -7,6 +7,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() { class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<K, V>>() {
override fun write(out: JsonWriter, value: ImmutableMap<K, V>?) { override fun write(out: JsonWriter, value: ImmutableMap<K, V>?) {
@ -26,7 +27,7 @@ class ImmutableArrayMapTypeAdapter<K, V>(val keyAdapter: TypeAdapter<K>, val ele
} }
override fun read(reader: JsonReader): ImmutableMap<K, V>? { override fun read(reader: JsonReader): ImmutableMap<K, V>? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null return null
reader.beginArray() reader.beginArray()

View File

@ -6,6 +6,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableList<E>>() { class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableList<E>>() {
override fun write(out: JsonWriter, value: ImmutableList<E>?) { override fun write(out: JsonWriter, value: ImmutableList<E>?) {
@ -14,11 +15,6 @@ class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdap
return return
} }
if (value.size == 1) {
elementAdapter.write(out, value[0])
return
}
out.beginArray() out.beginArray()
for (v in value) { for (v in value) {
@ -29,14 +25,9 @@ class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdap
} }
override fun read(reader: JsonReader): ImmutableList<E>? { override fun read(reader: JsonReader): ImmutableList<E>? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null 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() reader.beginArray()
val builder = ImmutableList.Builder<E>() val builder = ImmutableList.Builder<E>()

View File

@ -8,6 +8,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableMapTypeAdapter<V>(val stringInterner: Interner<String>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<String, V>>() { class ImmutableMapTypeAdapter<V>(val stringInterner: Interner<String>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<ImmutableMap<String, V>>() {
override fun write(out: JsonWriter, value: ImmutableMap<String, V>?) { override fun write(out: JsonWriter, value: ImmutableMap<String, V>?) {
@ -27,7 +28,7 @@ class ImmutableMapTypeAdapter<V>(val stringInterner: Interner<String>, val eleme
} }
override fun read(reader: JsonReader): ImmutableMap<String, V>? { override fun read(reader: JsonReader): ImmutableMap<String, V>? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null return null
if (reader.peek() == JsonToken.BEGIN_ARRAY) { if (reader.peek() == JsonToken.BEGIN_ARRAY) {

View File

@ -7,6 +7,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableSet<E>>() { class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableSet<E>>() {
override fun write(out: JsonWriter, value: ImmutableSet<E>?) { override fun write(out: JsonWriter, value: ImmutableSet<E>?) {
@ -15,11 +16,6 @@ class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapt
return return
} }
if (value.size == 1) {
elementAdapter.write(out, value.first())
return
}
out.beginArray() out.beginArray()
for (v in value) { for (v in value) {
@ -30,14 +26,9 @@ class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapt
} }
override fun read(reader: JsonReader): ImmutableSet<E>? { override fun read(reader: JsonReader): ImmutableSet<E>? {
if (reader.peek() == JsonToken.NULL) if (reader.consumeNull())
return null 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() reader.beginArray()
val builder = ImmutableSet.Builder<E>() val builder = ImmutableSet.Builder<E>()

View File

@ -1,4 +1,4 @@
package ru.dbotthepony.kstarbound.json package ru.dbotthepony.kstarbound.json.factory
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson import com.google.gson.Gson
@ -11,7 +11,7 @@ import it.unimi.dsi.fastutil.objects.*
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
class FastutilTypeAdapterFactory(private val interner: Interner<String>) : TypeAdapterFactory { class MapsTypeAdapterFactory(private val interner: Interner<String>) : TypeAdapterFactory {
private fun map1(gson: Gson, type: TypeToken<*>, typeValue: TypeToken<*>, factoryHash: () -> Map<Any?, Any?>, factoryTree: () -> Map<Any?, Any?>): TypeAdapter<MutableMap<Any?, Any?>>? { private fun map1(gson: Gson, type: TypeToken<*>, typeValue: TypeToken<*>, factoryHash: () -> Map<Any?, Any?>, factoryTree: () -> Map<Any?, Any?>): TypeAdapter<MutableMap<Any?, Any?>>? {
val p = type.type as? ParameterizedType ?: return null val p = type.type as? ParameterizedType ?: return null
val typeKey = TypeToken.get(p.actualTypeArguments[0]) val typeKey = TypeToken.get(p.actualTypeArguments[0])

View File

@ -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<RGBAColor>() {
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
}
}

View File

@ -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 <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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<T>
}
return null
}
}

View File

@ -247,7 +247,7 @@ private fun materialFootstepSound(context: ExecutionContext, arguments: Argument
} }
private fun materialHealth(context: ExecutionContext, arguments: ArgumentIterator) { 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) { private fun liquidName(context: ExecutionContext, arguments: ArgumentIterator) {

View File

@ -7,19 +7,25 @@ import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.nio.NioEventLoopGroup
import org.apache.logging.log4j.LogManager 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.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import java.io.Closeable import java.io.Closeable
import java.util.* import java.util.*
import kotlin.properties.Delegates 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) abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
var avatar: Avatar? = null var avatar: Avatar? = null
var character: PlayerEntity? = 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 protected set
var isLegacy: Boolean = true 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 legacyValidator = PacketRegistry.LEGACY.Validator(side)
private val legacySerializer = PacketRegistry.LEGACY.Serializer(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") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
if (type == ConnectionType.MEMORY) { if (type == ConnectionType.MEMORY) {
@ -52,7 +59,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
isConnected = true 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") LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol")
if (type == ConnectionType.MEMORY) { if (type == ConnectionType.MEMORY) {
@ -65,8 +73,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
isLegacy = false isLegacy = false
isConnected = true isConnected = true
}
inGame() protected open fun onChannelClosed() {
isConnected = false
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
} }
fun bind(channel: Channel) { fun bind(channel: Channel) {
@ -81,12 +92,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
channel.pipeline().addLast(this) channel.pipeline().addLast(this)
channel.closeFuture().addListener { channel.closeFuture().addListener {
isConnected = false onChannelClosed()
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
} }
} }
protected abstract fun inGame() abstract fun inGame()
fun send(packet: IPacket) { fun send(packet: IPacket) {
channel.write(packet) channel.write(packet)
@ -97,7 +107,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
channel.flush() channel.flush()
} }
fun flush() { open fun flush() {
channel.flush() channel.flush()
} }

View File

@ -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<String>, val arguments: KOptional<JsonElement>) {
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<Entry>()
private val responses = Int2ObjectOpenHashMap<CompletableFuture<JsonElement>>()
private val lock = ReentrantLock()
private val handlers = Object2ObjectOpenHashMap<String, Callback>()
fun add(name: String, callback: Callback): (JsonElement) -> CompletableFuture<JsonElement> {
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<JsonElement> {
lock.withLock {
val id = commandCounter++
val response = CompletableFuture<JsonElement>()
responses[id] = response
pendingWrite.add(Entry(Command.REQUEST, id, KOptional(handler), KOptional(arguments.deepCopy())))
return response
}
}
fun write(): List<Entry>? {
lock.withLock {
if (pendingWrite.isEmpty())
return null
val result = ImmutableList.copyOf(pendingWrite)
pendingWrite.clear()
return result
}
}
fun read(data: List<Entry>) {
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()
}
}

View File

@ -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
}
}
}

View File

@ -13,28 +13,34 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeSignedVarInt 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.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket 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.serverbound.ClientConnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket 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.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket 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.TrackedPositionPacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.io.FilterInputStream
import java.io.InputStream import java.io.InputStream
import java.util.zip.Deflater
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -66,6 +72,10 @@ class PacketRegistry(val isLegacy: Boolean) {
return add(T::class, reader, direction) return add(T::class, reader, direction)
} }
private inline fun <reified T : IPacket> add(value: T, direction: PacketDirection = PacketDirection.get(T::class)): PacketRegistry {
return add(T::class, { _, _ -> value }, direction)
}
private fun skip(amount: Int = 1) { private fun skip(amount: Int = 1) {
for (i in 0 until amount) { for (i in 0 until amount) {
packets.add(null) packets.add(null)
@ -77,6 +87,38 @@ class PacketRegistry(val isLegacy: Boolean) {
packets.add(null) 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() { inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() {
private val backlog = ByteArrayList() private val backlog = ByteArrayList()
private var discardBytes = 0 private var discardBytes = 0
@ -105,17 +147,23 @@ class PacketRegistry(val isLegacy: Boolean) {
val stream: InputStream val stream: InputStream
if (isCompressed) { if (isCompressed) {
stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size))) stream = BufferedInputStream(LimitingInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size))))
} else { } else {
stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size) stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size)
} }
try { // legacy protocol allows to stitch multiple packets of same type together without
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy)) // separate headers for each
} catch (err: Throwable) { while (stream.available() > 0) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) 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() backlog.clear()
readingType = null readingType = null
isCompressed = false isCompressed = false
@ -142,6 +190,9 @@ class PacketRegistry(val isLegacy: Boolean) {
} else if (!type.direction.acceptedOn(side)) { } else if (!type.direction.acceptedOn(side)) {
LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes") LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes")
discardBytes = dataLength.absoluteValue 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 { } else {
LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue) LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue)
readingType = type readingType = type
@ -171,13 +222,39 @@ class PacketRegistry(val isLegacy: Boolean) {
if (isLegacy) if (isLegacy)
check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" } 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) if (stream.length >= 512) {
val stream2 = ByteBufOutputStream(buff) // compress
stream2.writeByte(type.id) val deflater = Deflater(3)
stream2.writeSignedVarInt(stream.length) val buffers = ByteArrayList(1024)
stream2.write(stream.array, 0, stream.length) val buffer = ByteArray(1024)
LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length) deflater.setInput(stream.array, 0, stream.length)
ctx.write(buff, promise)
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 { 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() private val LOGGER = LogManager.getLogger()
val NATIVE = PacketRegistry(false) val NATIVE = PacketRegistry(false)
@ -266,16 +349,16 @@ class PacketRegistry(val isLegacy: Boolean) {
// Packets sent bidirectionally between the universe client and the universe // Packets sent bidirectionally between the universe client and the universe
// server // server
LEGACY.skip("ClientContextUpdate") LEGACY.add(ClientContextUpdatePacket::read)
// Packets sent world server -> world client // Packets sent world server -> world client
LEGACY.add(::WorldStartPacket) // WorldStart LEGACY.add(::WorldStartPacket) // WorldStart
LEGACY.skip("WorldStop") LEGACY.add(::WorldStopPacket)
LEGACY.skip("WorldLayoutUpdate") LEGACY.skip("WorldLayoutUpdate")
LEGACY.skip("WorldParametersUpdate") LEGACY.skip("WorldParametersUpdate")
LEGACY.skip("CentralStructureUpdate") LEGACY.skip("CentralStructureUpdate")
LEGACY.skip("TileArrayUpdate") LEGACY.add(LegacyTileArrayUpdatePacket::read)
LEGACY.skip("TileUpdate") LEGACY.add(LegacyTileUpdatePacket::read)
LEGACY.skip("TileLiquidUpdate") LEGACY.skip("TileLiquidUpdate")
LEGACY.skip("TileDamageUpdate") LEGACY.skip("TileDamageUpdate")
LEGACY.skip("TileModificationFailure") LEGACY.skip("TileModificationFailure")
@ -286,7 +369,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("SetDungeonBreathable") LEGACY.skip("SetDungeonBreathable")
LEGACY.skip("SetPlayerStart") LEGACY.skip("SetPlayerStart")
LEGACY.skip("FindUniqueEntityResponse") LEGACY.skip("FindUniqueEntityResponse")
LEGACY.skip("Pong") LEGACY.add(PongPacket)
// Packets sent world client -> world server // Packets sent world client -> world server
LEGACY.skip("ModifyTileList") LEGACY.skip("ModifyTileList")
@ -299,7 +382,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("WorldClientStateUpdate") LEGACY.skip("WorldClientStateUpdate")
LEGACY.skip("FindUniqueEntity") LEGACY.skip("FindUniqueEntity")
LEGACY.skip("WorldStartAcknowledge") LEGACY.skip("WorldStartAcknowledge")
LEGACY.skip("Ping") LEGACY.add(PingPacket)
// Packets sent bidirectionally between world client and world server // Packets sent bidirectionally between world client and world server
LEGACY.skip("EntityCreate") LEGACY.skip("EntityCreate")

View File

@ -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<JsonRPC.Entry>,
val shipChunks: KOptional<Map<ByteKey, KOptional<ByteArray>>>,
val networkedVars: KOptional<ByteArrayList>
) : 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()) })
}
}
}
}

View File

@ -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)
}
}

View File

@ -1,12 +1,10 @@
package ru.dbotthepony.kstarbound.network.packets package ru.dbotthepony.kstarbound.network.packets
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.UUID
data class ProtocolRequestPacket(val version: Int) : IServerPacket { data class ProtocolRequestPacket(val version: Int) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt()) constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt())

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.client.ClientConnection 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 ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream

View File

@ -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<LegacyNetworkCellState>) : 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<LegacyNetworkCellState>(columns, rows)
for (y in data.columnIndices) {
for (x in data.rowIndices) {
data[y, x] = LegacyNetworkCellState.read(stream)
}
}
return LegacyTileArrayUpdatePacket(origin, data as Object2DArray<LegacyNetworkCellState>)
}
}
}

View File

@ -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")
}
}

View File

@ -15,7 +15,7 @@ import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.io.writeMap import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kommons.io.writeUUID import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.util.KOptional 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.defs.player.ShipUpgrades
import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
@ -58,7 +58,9 @@ data class ClientConnectPacket(
override fun play(connection: ServerConnection) { override fun play(connection: ServerConnection) {
LOGGER.info("Client connection request received from ${connection.channel.remoteAddress()}, Player $playerName/$playerUuid (account '$account')") 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 { companion object {

View File

@ -1,11 +1,15 @@
package ru.dbotthepony.kstarbound.server package ru.dbotthepony.kstarbound.server
import io.netty.channel.ChannelHandlerContext 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.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager 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.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket 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.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IServerPacket 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.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject 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 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 "<no channel>"
val ship = if (::shipWorld.isInitialized) shipWorld.toString() else "<no shipworld>"
return "ServerConnection[ID=$connectionID channel=$channel / $ship]"
}
var trackedPosition: Vector2d = Vector2d.ZERO var trackedPosition: Vector2d = Vector2d.ZERO
set(value) { set(value) {
@ -52,6 +74,25 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
} }
private val shipChunks = Object2ObjectOpenHashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<IChunkSource>()
private set
override fun setupLegacy() {
super.setupLegacy()
shipChunkSource = LegacyChunkSource.memory(shipChunks)
}
override fun setupNative() {
super.setupNative()
}
fun receiveShipChunks(chunks: Map<ByteKey, KOptional<ByteArray>>) {
check(shipChunks.isEmpty()) { "Already has ship chunks" }
shipChunks.putAll(chunks)
}
private val tickets = Object2ObjectOpenHashMap<ChunkPos, ServerWorld.ITicket>() private val tickets = Object2ObjectOpenHashMap<ChunkPos, ServerWorld.ITicket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>() private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
@ -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() { fun onLeaveWorld() {
tickets.values.forEach { it.cancel() } tickets.values.forEach { it.cancel() }
tickets.clear() tickets.clear()
pendingSend.clear() pendingSend.clear()
} }
override fun onChannelClosed() {
super.onChannelClosed()
if (::shipWorld.isInitialized) {
shipWorld.close()
}
}
private fun recomputeTrackedChunks() { private fun recomputeTrackedChunks() {
val world = world ?: return val world = world ?: return
val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition) val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition)
@ -129,7 +193,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
for (pos in itr) { for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue val chunk = world.chunkMap[pos] ?: continue
send(ChunkCellsPacket(chunk))
if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
} else {
send(ChunkCellsPacket(chunk))
}
itr.remove() itr.remove()
} }
} }
@ -149,7 +219,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
} }
override fun inGame() { 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 { companion object {

View File

@ -31,6 +31,8 @@ sealed class StarboundServer(val root: File) : Closeable {
val thread = Thread(spinner, "Starbound Server $serverID") val thread = Thread(spinner, "Starbound Server $serverID")
val universe = ServerUniverse() val universe = ServerUniverse()
val nextConnectionID = AtomicInteger()
val settings = ServerSettings() val settings = ServerSettings()
val channels = ServerChannels(this) val channels = ServerChannels(this)
val lock = ReentrantLock() val lock = ReentrantLock()

View File

@ -22,20 +22,21 @@ import java.io.ByteArrayInputStream
import java.io.DataInputStream import java.io.DataInputStream
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.function.Supplier import java.util.function.Supplier
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
class LegacyChunkSource(val db: BTreeDB5) : IChunkSource { class LegacyChunkSource(val loader: Loader) : IChunkSource {
private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL) fun interface Loader {
operator fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>>
}
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> { override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
val chunkX = pos.x val chunkX = pos.x
val chunkY = pos.y val chunkY = pos.y
val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) 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 { it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater()))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
reader.skipBytes(3) reader.skipBytes(3)
val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE) val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE)
@ -46,6 +47,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
} }
} }
reader.close()
result as Object2DArray<out AbstractCell> result as Object2DArray<out AbstractCell>
} }
} }
@ -56,9 +58,9 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
val chunkY = pos.y val chunkY = pos.y
val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte()) 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 { it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater()))) val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
val i = reader.readVarInt() val i = reader.readVarInt()
val objects = ArrayList<AbstractEntity>() val objects = ArrayList<AbstractEntity>()
@ -76,6 +78,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
} }
} }
reader.close()
objects objects
} }
} }
@ -84,5 +87,15 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
companion object { companion object {
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) } private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val LOGGER = LogManager.getLogger() 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<ByteKey, KOptional<ByteArray>>): LegacyChunkSource {
return LegacyChunkSource { key -> CompletableFuture.completedFuture(backing[key] ?: KOptional()) }
}
} }
} }

View File

@ -7,29 +7,27 @@ import com.google.gson.stream.JsonReader
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.objects.ObjectArrayList import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.collect.chainOptionalFutures import ru.dbotthepony.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.BTreeDB6 import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.util.AABBi import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.CelestialGenerationInformation import ru.dbotthepony.kstarbound.defs.world.CelestialGenerationInformation
import ru.dbotthepony.kstarbound.defs.CelestialNames import ru.dbotthepony.kstarbound.defs.world.CelestialNames
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.util.random.staticRandom64 import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.CoordinateMapper
import ru.dbotthepony.kstarbound.world.Universe import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.time.Duration import java.time.Duration
import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -69,18 +67,39 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
private val sources = ArrayList<UniverseSource>() private val sources = ArrayList<UniverseSource>()
private val closeables = ArrayList<Closeable>() private val closeables = ArrayList<Closeable>()
override fun name(pos: UniversePos): CompletableFuture<KOptional<String>> { override suspend fun parameters(pos: UniversePos): CelestialParameters? {
return getChunk(pos).thenApply { return getChunk(pos)?.parameters(pos)
it.flatMap { it.parameters(pos) }.map { it.name }
}
} }
override fun scanSystems(region: AABBi, includedTypes: Set<String>?): CompletableFuture<List<UniversePos>> { 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<UniversePos> {
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<String>?): List<UniversePos> {
val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null val copy = if (includedTypes != null) ObjectOpenHashSet(includedTypes) else null
val futures = ArrayList<CompletableFuture<List<UniversePos>>>() val futures = ArrayList<CompletableFuture<List<UniversePos>>>()
for (pos in chunkPositions(region)) { for (pos in chunkPositions(region)) {
val f = getChunk(pos).thenApply { val f = getChunkFuture(pos).thenApply {
it.map<List<UniversePos>> { it.map<List<UniversePos>> {
val result = ArrayList<UniversePos>() val result = ArrayList<UniversePos>()
@ -103,23 +122,23 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
futures.add(f) futures.add(f)
} }
return CompletableFuture.allOf(*futures.toTypedArray()) CompletableFuture.allOf(*futures.toTypedArray()).await()
.thenApply { futures.stream().flatMap { it.get().stream() }.toList() } return futures.stream().flatMap { it.get().stream() }.toList()
} }
override fun scanConstellationLines(region: AABBi): CompletableFuture<List<Pair<Vector2i, Vector2i>>> { override suspend fun scanConstellationLines(region: AABBi): List<Pair<Vector2i, Vector2i>> {
val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>() val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>()
for (pos in chunkPositions(region)) { for (pos in chunkPositions(region)) {
val f = getChunk(pos).thenApply { val f = getChunkFuture(pos).thenApply {
it.map<List<Pair<Vector2i, Vector2i>>> { ObjectArrayList(it.constellations) }.orElse(listOf()) it.map<List<Pair<Vector2i, Vector2i>>> { ObjectArrayList(it.constellations) }.orElse(listOf())
} }
futures.add(f) futures.add(f)
} }
return CompletableFuture.allOf(*futures.toTypedArray()) CompletableFuture.allOf(*futures.toTypedArray()).await()
.thenApply { futures.stream().flatMap { it.get().stream() }.toList() } return futures.stream().flatMap { it.get().stream() }.toList()
} }
override fun scanRegionFullyLoaded(region: AABBi): Boolean { override fun scanRegionFullyLoaded(region: AABBi): Boolean {
@ -140,12 +159,22 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
.scheduler(Scheduler.systemScheduler()) .scheduler(Scheduler.systemScheduler())
.build() .build()
fun getChunk(pos: UniversePos): CompletableFuture<KOptional<UniverseChunk>> { fun getChunkFuture(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {
return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } }
}
suspend fun getChunk(pos: UniversePos): UniverseChunk? {
return getChunk(world2chunk(Vector2i(pos.location))) return getChunk(world2chunk(Vector2i(pos.location)))
} }
fun getChunk(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> { suspend fun getChunk(pos: Vector2i): UniverseChunk? {
return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } } val get = getChunkFuture(pos).await()
if (get.isPresent) {
return get.value
} else {
return null
}
} }
init { init {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet 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.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket 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.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.ExecutionSpinner import ru.dbotthepony.kstarbound.util.ExecutionSpinner
@ -19,6 +22,7 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import java.util.Collections import java.util.Collections
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport import java.util.concurrent.locks.LockSupport
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -28,9 +32,8 @@ import kotlin.concurrent.withLock
class ServerWorld( class ServerWorld(
val server: StarboundServer, val server: StarboundServer,
seed: Long,
geometry: WorldGeometry, geometry: WorldGeometry,
) : World<ServerWorld, ServerChunk>(seed, geometry) { ) : World<ServerWorld, ServerChunk>(geometry) {
init { init {
server.worlds.add(this) server.worlds.add(this)
} }
@ -41,9 +44,29 @@ class ServerWorld(
private fun doAcceptPlayer(player: ServerConnection): Boolean { private fun doAcceptPlayer(player: ServerConnection): Boolean {
if (player !in internalPlayers) { if (player !in internalPlayers) {
internalPlayers.add(player) internalPlayers.add(player)
player.onLeaveWorld()
player.world?.removePlayer(player) player.world?.removePlayer(player)
player.world = this 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 return true
} }
@ -51,6 +74,9 @@ class ServerWorld(
} }
fun acceptPlayer(player: ServerConnection): CompletableFuture<Boolean> { fun acceptPlayer(player: ServerConnection): CompletableFuture<Boolean> {
check(!isClosed.get()) { "$this is invalid" }
unpause()
try { try {
return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox) return CompletableFuture.supplyAsync(Supplier { doAcceptPlayer(player) }, mailbox)
} catch (err: RejectedExecutionException) { } catch (err: RejectedExecutionException) {
@ -71,12 +97,10 @@ class ServerWorld(
} }
val spinner = ExecutionSpinner(mailbox::executeQueuedTasks, ::spin, Starbound.TICK_TIME_ADVANCE_NANOS) 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() val ticketListLock = ReentrantLock()
@Volatile private val isClosed = AtomicBoolean()
var isClosed: Boolean = false
private set
init { init {
thread.isDaemon = true thread.isDaemon = true
@ -89,10 +113,18 @@ class ServerWorld(
chunkProviders.add(source) chunkProviders.add(source)
} }
fun pause() {
if (!isClosed.get()) spinner.pause()
}
fun unpause() {
if (!isClosed.get()) spinner.unpause()
}
override fun close() { override fun close() {
if (!isClosed) { if (isClosed.compareAndSet(false, true)) {
super.close() super.close()
isClosed = true spinner.unpause()
lock.withLock { lock.withLock {
internalPlayers.forEach { internalPlayers.forEach {
@ -105,7 +137,7 @@ class ServerWorld(
} }
private fun spin(): Boolean { private fun spin(): Boolean {
if (isClosed) return false if (isClosed.get()) return false
try { try {
think() think()
@ -124,7 +156,7 @@ class ServerWorld(
} }
override fun thinkInner() { override fun thinkInner() {
internalPlayers.forEach { if (!isClosed) it.tick() } internalPlayers.forEach { if (!isClosed.get()) it.tick() }
ticketListLock.withLock { ticketListLock.withLock {
ticketLists.removeIf { ticketLists.removeIf {

View File

@ -18,11 +18,10 @@ import ru.dbotthepony.kommons.gson.getArray
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.defs.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.CelestialPlanet import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet
import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairAdapter
import ru.dbotthepony.kstarbound.json.pairListAdapter import ru.dbotthepony.kstarbound.json.pairListAdapter
import ru.dbotthepony.kstarbound.json.pairSetAdapter
import ru.dbotthepony.kstarbound.world.UniversePos import ru.dbotthepony.kstarbound.world.UniversePos
class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
@ -31,15 +30,15 @@ class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) {
val systems = Object2ObjectOpenHashMap<Vector3i, System>() val systems = Object2ObjectOpenHashMap<Vector3i, System>()
val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>() val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>()
fun parameters(coordinate: UniversePos): KOptional<CelestialParameters> { fun parameters(coordinate: UniversePos): CelestialParameters? {
val system = systems[coordinate.location] ?: return KOptional() val system = systems[coordinate.location] ?: return null
if (coordinate.isSystem) if (coordinate.isSystem)
return KOptional(system.parameters) return system.parameters
else if (coordinate.isPlanet) else if (coordinate.isPlanet)
return KOptional.ofNullable(system.planets[coordinate.planetOrbit]?.parameters) return system.planets[coordinate.planetOrbit]?.parameters
else if (coordinate.isSatellite) 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 else
throw RuntimeException("unreachable code") throw RuntimeException("unreachable code")
} }

View File

@ -17,13 +17,14 @@ import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kommons.vector.Vector3i import ru.dbotthepony.kommons.vector.Vector3i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.defs.CelestialPlanet import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet
import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.JsonDriven
import ru.dbotthepony.kstarbound.io.BTreeDB5 import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.json.readJsonElement import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.Line2d import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.util.binnedChoice
import ru.dbotthepony.kstarbound.util.paddedNumber import ru.dbotthepony.kstarbound.util.paddedNumber
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64 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 { 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() { override fun close() {
carried.shutdown() carried.shutdown()
@ -168,9 +169,7 @@ class NativeUniverseSource(private val db: BTreeDB6?, private val universe: Serv
val type = universe.generationInformation.systemTypeBins val type = universe.generationInformation.systemTypeBins
.stream() .stream()
.sorted { o1, o2 -> o2.first.compareTo(o1.first) } .binnedChoice(typeSelector).orElse("")
.filter { it.first <= typeSelector }
.findFirst().map { it.second }.orElse("")
if (type.isBlank()) if (type.isBlank())
return null 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 reader: UniverseSource = Reader()
val generator: UniverseSource = Generator() val generator: UniverseSource = Generator()

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.util package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import java.io.File
object AssetPathStack { object AssetPathStack {
private val _stack = object : ThreadLocal<ArrayDeque<String>>() { private val _stack = object : ThreadLocal<ArrayDeque<String>>() {
@ -49,4 +50,11 @@ object AssetPathStack {
fun remapSafe(path: String): String { fun remapSafe(path: String): String {
return remap(last() ?: return path, path) 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"
}
} }

Some files were not shown because too many files have changed in this diff Show More