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

View File

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

View File

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

View File

@ -1,14 +1,17 @@
package ru.dbotthepony.kstarbound
import it.unimi.dsi.fastutil.floats.FloatArrayList
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.future
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager
import org.lwjgl.Version
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParameters
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.server.IntegratedStarboundServer
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource
@ -23,7 +26,6 @@ import java.io.DataInputStream
import java.io.File
import java.net.InetSocketAddress
import java.util.*
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
@ -38,10 +40,28 @@ fun main() {
val data = ServerUniverse()
val t = System.nanoTime()
val result = data.scanConstellationLines(AABBi(Vector2i(-100, -100), Vector2i(100, 100))).get()
val result = Starbound.COROUTINES.future {
val systems = data.scanSystems(AABBi(Vector2i(-50, -50), Vector2i(50, 50)), setOf("whitestar"))
for (system in systems) {
for (children in data.children(system)) {
if (children.isPlanet) {
val params = data.parameters(children)!!
if (params.visitableParameters != null) {
//val write = params.visitableParameters!!.toJson(false)
//println(write)
//println(Starbound.gson.fromJson(write, VisitableWorldParameters::class.java))
}
}
}
}
systems
}.get()
println(System.nanoTime() - t)
println(result)
data.close()
return
@ -74,8 +94,8 @@ fun main() {
val server = IntegratedStarboundServer(File("./"))
val client = StarboundClient.create().get()
//val client2 = StarboundClient.create().get()
val world = ServerWorld(server, 0L, WorldGeometry(Vector2i(3000, 2000), true, false))
world.addChunkSource(LegacyChunkSource(db))
val world = ServerWorld(server, WorldGeometry(Vector2i(3000, 2000), true, false))
world.addChunkSource(LegacyChunkSource.file(db))
world.thread.start()
//Starbound.addFilePath(File("./unpacked_assets/"))

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 elements = Starbound.gson.getAdapter(JsonElement::class.java)
val recipes = Starbound.gson.getAdapter(RecipeDefinition::class.java)
return files.map { listedFile ->
executor.submit {
Starbound.EXECUTOR.submit {
try {
val json = elements.read(listedFile.jsonReader())
val value = recipes.fromJsonTree(json)

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

@ -36,9 +36,8 @@ import kotlin.concurrent.withLock
class ClientWorld(
val client: StarboundClient,
seed: Long,
geometry: WorldGeometry,
) : World<ClientWorld, ClientChunk>(seed, geometry) {
) : World<ClientWorld, ClientChunk>(geometry) {
private fun determineChunkSize(cells: Int): Int {
for (i in 64 downTo 1) {
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.stream.JsonReader
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.ObjectArrayList
import ru.dbotthepony.kommons.util.KOptional
import java.lang.reflect.ParameterizedType
import java.util.random.RandomGenerator
@ -46,6 +47,39 @@ class WeightedList<E>(val parent: ImmutableList<Pair<Double, E>>) {
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 {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.lang.reflect.ParameterizedType
@ -27,23 +29,23 @@ data class AssetReference<V>(val path: String?, val fullPath: String?, val value
if (type.rawType == AssetReference::class.java) {
val param = type.type as? ParameterizedType ?: return null
return object : TypeAdapter<AssetReference<Any>>() {
private val cache = ConcurrentHashMap<String, Pair<Any, JsonElement>>()
private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<Any>
return object : TypeAdapter<AssetReference<T>>() {
private val cache = Collections.synchronizedMap(Object2ObjectOpenHashMap<String, Pair<T, JsonElement>>())
private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
private val strings = gson.getAdapter(String::class.java)
private val jsons = gson.getAdapter(JsonElement::class.java)
private val missing = Collections.synchronizedSet(ObjectOpenHashSet<String>())
private val logger = LogManager.getLogger()
override fun write(out: JsonWriter, value: AssetReference<Any>?) {
override fun write(out: JsonWriter, value: AssetReference<T>?) {
if (value == null)
out.nullValue()
else
out.value(value.fullPath)
}
override fun read(`in`: JsonReader): AssetReference<Any>? {
if (`in`.peek() == JsonToken.NULL) {
override fun read(`in`: JsonReader): AssetReference<T>? {
if (`in`.consumeNull()) {
return null
} else if (`in`.peek() == JsonToken.STRING) {
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)
return null
val file = Starbound.locate(fullPath)
val json = Starbound.loadJsonAsset(fullPath)
if (!file.exists) {
logger.error("File does not exist: ${file.computeFullPath()}")
if (json == null) {
logger.error("JSON asset does not exist: $fullPath")
missing.add(fullPath)
return AssetReference(path, fullPath, null, null)
}
val reader = file.reader()
val json = jsons.read(JsonReader(reader).also {
it.isLenient = true
})
val value = AssetPathStack(fullPath.substringBefore(':').substringBeforeLast('/')) {
adapter.read(JsonTreeReader(json))
adapter.fromJsonTree(json)
}
if (value == null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,14 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class PerlinNoiseParameters(
val type: Type = Type.PERLIN,
val seed: Long? = null,
val scale: Int = 512,
val scale: Int = DEFAULT_SCALE,
val octaves: Int = 1,
val gain: Double = 2.0,
val offset: Double = 1.0,
@ -20,9 +22,21 @@ data class PerlinNoiseParameters(
require(scale >= 16) { "Too little perlin noise scale" }
}
enum class Type {
PERLIN,
BILLOW,
RIDGED_MULTI;
enum class Type(val jsonName: String) : IStringSerializable {
PERLIN("perlin"),
BILLOW("billow"),
RIDGED_MULTI("ridgedmulti");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
}
companion object {
const val DEFAULT_SCALE = 512
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.ImmutableMap
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import ru.dbotthepony.kommons.io.readVector2i
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.world.UniversePos
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class CelestialNames(
val systemNames: WeightedList<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
data class CelestialGenerationInformation(
@ -127,7 +74,4 @@ data class CelestialGenerationInformation(
it.value.typeName = it.key
}
}
}
@JsonFactory
data class CelestialParameters(val coordinate: UniversePos, val seed: Long, val name: String, val parameters: JsonObject)
}

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.IStruct2i
import ru.dbotthepony.kommons.io.readDouble
import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readLong
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeLong
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkPos
@ -40,3 +43,14 @@ fun InputStream.readHeader(header: String) {
fun InputStream.readChunkPos(): ChunkPos {
return ChunkPos(readSignedVarInt(), readSignedVarInt())
}
fun OutputStream.writeColor(color: RGBAColor) {
writeFloat(color.red)
writeFloat(color.green)
writeFloat(color.blue)
writeFloat(color.alpha)
}
fun InputStream.readColor(): RGBAColor {
return RGBAColor(readFloat(), readFloat(), readFloat(), readFloat())
}

View File

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

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.json
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
@ -21,6 +22,10 @@ inline fun <reified C : Collection<E>, reified E> Gson.collectionAdapter(): Type
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>> {
return collectionAdapter()
}

View File

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

View File

@ -65,7 +65,6 @@ annotation class JsonBuilder
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonFactory(
val storesJson: Boolean = false,
val asList: Boolean = false,
val logMisses: Boolean = false,
)
@ -89,6 +88,10 @@ annotation class JsonFactory(
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonImplementation(val implementingClass: KClass<*>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class JsonSingleton
object JsonImplementationTypeFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
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.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.util.sbIntern
import java.util.Arrays
import java.util.*
import java.util.stream.Stream
import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
@ -25,27 +26,22 @@ interface IStringSerializable {
fun write(out: JsonWriter)
}
@Suppress("FunctionName")
inline fun <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)
}
@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)
}
@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)
}
@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)
}
@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?>() {
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 mapping: ImmutableMap<String, T>
private val areCustom = IStringSerializable::class.java.isAssignableFrom(enum.java)
private val misses = ObjectOpenHashSet<String>()
private val misses = Collections.synchronizedSet(ObjectOpenHashSet<String>())
init {
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?) {
if (value == null) {
out.nullValue()
} else if (value is IStringSerializable) {
value.write(out)
} else {
out.value(value.name)
}
@ -95,7 +93,7 @@ class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> =
@Suppress("unchecked_cast")
override fun read(`in`: JsonReader): T? {
if (`in`.peek() == JsonToken.NULL) {
if (`in`.consumeNull()) {
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.JsonToken
import com.google.gson.stream.JsonWriter
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntArrayList
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
@ -48,7 +49,6 @@ class FactoryAdapter<T : Any> private constructor(
val types: ImmutableList<ReferencedProperty<T, *>>,
aliases: Map<String, List<String>>,
val asJsonArray: Boolean,
val storesJson: Boolean,
val stringInterner: Interner<String>,
val logMisses: Boolean,
private val elements: TypeAdapter<JsonElement>
@ -78,10 +78,7 @@ class FactoryAdapter<T : Any> private constructor(
* Обычный конструктор класса (без флагов "значения по умолчанию")
*/
private val regularFactory: KFunction<T> = clazz.constructors.firstOrNull first@{
var requiredSize = types.size
if (storesJson)
requiredSize++
val requiredSize = types.size
if (it.parameters.size == requiredSize) {
val iterator = types.iterator()
@ -99,20 +96,6 @@ class FactoryAdapter<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
}
@ -125,12 +108,6 @@ class FactoryAdapter<T : Any> private constructor(
private val syntheticFactory: Constructor<T>? = try {
val typelist = types.map { (it.type.classifier as KClass<*>).java }.toMutableList()
if (storesJson)
if (asJsonArray)
typelist.add(List::class.java)
else
typelist.add(Map::class.java)
for (i in 0 until (if (types.size % 31 == 0) types.size / 31 else types.size / 31 + 1))
typelist.add(Int::class.java)
@ -141,7 +118,7 @@ class FactoryAdapter<T : Any> private constructor(
null
}
private val syntheticPrimitives = Int2ObjectOpenHashMap<Any>()
private val syntheticPrimitives = Int2ObjectAVLTreeMap<Any>()
init {
if (syntheticFactory != null) {
@ -201,27 +178,14 @@ class FactoryAdapter<T : Any> private constructor(
return null
// таблица присутствия значений (если значение true то на i было значение внутри json)
val presentValues = BooleanArray(types.size + (if (storesJson) 1 else 0))
var readValues = arrayOfNulls<Any>(types.size + (if (storesJson) 1 else 0))
if (storesJson)
presentValues[presentValues.size - 1] = true
val presentValues = BooleanArray(types.size)
var readValues = arrayOfNulls<Any>(types.size)
@Suppress("name_shadowing")
var reader = reader
// Если нам необходимо читать объект как набор данных массива, то давай
if (asJsonArray) {
if (storesJson) {
val readArray = elements.read(reader)
if (readArray !is JsonArray)
throw JsonParseException("Expected JSON element to be an Array, ${readArray::class.qualifiedName} given")
reader = JsonTreeReader(readArray)
readValues[readValues.size - 1] = enrollList(flattenJsonElement(readArray, stringInterner) as List<Any>, stringInterner)
}
reader.beginArray()
val iterator = types.iterator()
var fieldId = 0
@ -230,7 +194,7 @@ class FactoryAdapter<T : Any> private constructor(
if (!iterator.hasNext()) {
val name = fieldId.toString()
if (!storesJson && logMisses && loggedMisses.add(name)) {
if (logMisses && loggedMisses.add(name)) {
LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
}
@ -255,7 +219,7 @@ class FactoryAdapter<T : Any> private constructor(
} else {
var json: JsonObject by Delegates.notNull()
if (storesJson || types.any { it.isFlat }) {
if (types.any { it.isFlat }) {
val readMap = elements.read(reader)
if (readMap !is JsonObject)
@ -263,9 +227,6 @@ class FactoryAdapter<T : Any> private constructor(
json = readMap
reader = JsonTreeReader(readMap)
if (storesJson)
readValues[readValues.size - 1] = enrollMap(flattenJsonElement(readMap, stringInterner) as Map<String, Any>, stringInterner)
}
reader.beginObject()
@ -275,7 +236,7 @@ class FactoryAdapter<T : Any> private constructor(
val fields = name2index[name]
if (fields == null || fields.size == 0) {
if (!storesJson && logMisses && loggedMisses.add(name)) {
if (logMisses && loggedMisses.add(name)) {
LOGGER.warn("${clazz.qualifiedName} has no property for storing $name")
}
@ -393,7 +354,7 @@ class FactoryAdapter<T : Any> private constructor(
if (readValues[i] != null) continue
val param = regularFactory.parameters[i]
if (param.isOptional && !presentValues[i]) {
if (param.isOptional && (!presentValues[i] || readValues[i] == null && i in syntheticPrimitives)) {
readValues[i] = syntheticPrimitives[i]
} else if (!param.isOptional) {
if (!presentValues[i]) throw JsonSyntaxException("Field ${field.name} of ${clazz.qualifiedName} is missing")
@ -401,7 +362,11 @@ class FactoryAdapter<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,
types = ImmutableList.copyOf(types.also { it.forEach{ it.resolve(gson) } }),
asJsonArray = asList,
storesJson = storesJson,
stringInterner = stringInterner,
aliases = aliases,
logMisses = logMisses,
@ -448,19 +412,6 @@ class FactoryAdapter<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> {
types.add(ReferencedProperty(field, isFlat = isFlat, isMarkedNullable = isMarkedNullable))
return this
@ -517,7 +468,6 @@ class FactoryAdapter<T : Any> private constructor(
builder.inputAsList()
}
builder.storesJson(config.storesJson)
builder.stringInterner = stringInterner
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 params = foundConstructor.parameters
val lastIndex = if (config.storesJson) params.size - 1 else params.size
val lastIndex = params.size
for (i in 0 until lastIndex) {
val argument = params[i]

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.JsonToken
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>>() {
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>? {
if (reader.peek() == JsonToken.NULL)
if (reader.consumeNull())
return null
reader.beginArray()

View File

@ -6,6 +6,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableList<E>>() {
override fun write(out: JsonWriter, value: ImmutableList<E>?) {
@ -14,11 +15,6 @@ class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdap
return
}
if (value.size == 1) {
elementAdapter.write(out, value[0])
return
}
out.beginArray()
for (v in value) {
@ -29,14 +25,9 @@ class ImmutableListTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdap
}
override fun read(reader: JsonReader): ImmutableList<E>? {
if (reader.peek() == JsonToken.NULL)
if (reader.consumeNull())
return null
if (reader.peek() != JsonToken.BEGIN_ARRAY) {
// не массив, возможно упрощение структуры "a": [value] -> "a": value
return ImmutableList.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls"))
}
reader.beginArray()
val builder = ImmutableList.Builder<E>()

View File

@ -8,6 +8,7 @@ import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableMapTypeAdapter<V>(val stringInterner: Interner<String>, val elementAdapter: TypeAdapter<V>) : TypeAdapter<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>? {
if (reader.peek() == JsonToken.NULL)
if (reader.consumeNull())
return null
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.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapter<ImmutableSet<E>>() {
override fun write(out: JsonWriter, value: ImmutableSet<E>?) {
@ -15,11 +16,6 @@ class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapt
return
}
if (value.size == 1) {
elementAdapter.write(out, value.first())
return
}
out.beginArray()
for (v in value) {
@ -30,14 +26,9 @@ class ImmutableSetTypeAdapter<E>(val elementAdapter: TypeAdapter<E>) : TypeAdapt
}
override fun read(reader: JsonReader): ImmutableSet<E>? {
if (reader.peek() == JsonToken.NULL)
if (reader.consumeNull())
return null
if (reader.peek() != JsonToken.BEGIN_ARRAY) {
// не массив, возможно упрощение структуры "a": [value] -> "a": value
return ImmutableSet.of(elementAdapter.read(reader) ?: throw JsonSyntaxException("List does not accept nulls"))
}
reader.beginArray()
val builder = ImmutableSet.Builder<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.google.gson.Gson
@ -11,7 +11,7 @@ import it.unimi.dsi.fastutil.objects.*
import ru.dbotthepony.kommons.gson.consumeNull
import java.lang.reflect.ParameterizedType
class FastutilTypeAdapterFactory(private val interner: Interner<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?>>? {
val p = type.type as? ParameterizedType ?: return null
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) {
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) {

View File

@ -7,19 +7,25 @@ import io.netty.channel.ChannelInboundHandlerAdapter
import io.netty.channel.ChannelOption
import io.netty.channel.nio.NioEventLoopGroup
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.player.Avatar
import ru.dbotthepony.kstarbound.world.entities.PlayerEntity
import java.io.Closeable
import java.util.*
import kotlin.properties.Delegates
abstract class Connection(val side: ConnectionSide, val type: ConnectionType, val localUUID: UUID) : ChannelInboundHandlerAdapter(), Closeable {
abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable {
abstract override fun channelRead(ctx: ChannelHandlerContext, msg: Any)
var avatar: Avatar? = null
var character: PlayerEntity? = null
val rpc = JsonRPC()
var channel: Channel by Delegates.notNull()
var connectionID: Int = -1
val hasChannel get() = ::channel.isInitialized
lateinit var channel: Channel
protected set
var isLegacy: Boolean = true
@ -37,7 +43,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
private val legacyValidator = PacketRegistry.LEGACY.Validator(side)
private val legacySerializer = PacketRegistry.LEGACY.Serializer(side)
fun setupLegacy() {
open fun setupLegacy() {
if (isConnected) throw IllegalStateException("Already connected")
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using legacy protocol")
if (type == ConnectionType.MEMORY) {
@ -52,7 +59,8 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
isConnected = true
}
fun setupNative() {
open fun setupNative() {
if (isConnected) throw IllegalStateException("Already connected")
LOGGER.info("Handshake successful on ${channel.remoteAddress()}, channel is using native protocol")
if (type == ConnectionType.MEMORY) {
@ -65,8 +73,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
isLegacy = false
isConnected = true
}
inGame()
protected open fun onChannelClosed() {
isConnected = false
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
}
fun bind(channel: Channel) {
@ -81,12 +92,11 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
channel.pipeline().addLast(this)
channel.closeFuture().addListener {
isConnected = false
LOGGER.info("Connection to ${channel.remoteAddress()} is closed")
onChannelClosed()
}
}
protected abstract fun inGame()
abstract fun inGame()
fun send(packet: IPacket) {
channel.write(packet)
@ -97,7 +107,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType, va
channel.flush()
}
fun flush() {
open fun flush() {
channel.flush()
}

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 org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
import ru.dbotthepony.kstarbound.client.network.packets.JoinWorldPacket
import ru.dbotthepony.kstarbound.client.network.packets.SpawnWorldObjectPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.PingPacket
import ru.dbotthepony.kstarbound.network.packets.PongPacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientConnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ConnectSuccessPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.HandshakeChallengePacket
import ru.dbotthepony.kstarbound.network.packets.serverbound.HandshakeResponsePacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolRequestPacket
import ru.dbotthepony.kstarbound.network.packets.ProtocolResponsePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.ServerDisconnectPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedPositionPacket
import ru.dbotthepony.kstarbound.server.network.packets.TrackedSizePacket
import java.io.BufferedInputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.FilterInputStream
import java.io.InputStream
import java.util.zip.Deflater
import java.util.zip.InflaterInputStream
import kotlin.math.absoluteValue
import kotlin.reflect.KClass
@ -66,6 +72,10 @@ class PacketRegistry(val isLegacy: Boolean) {
return add(T::class, reader, direction)
}
private inline fun <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) {
for (i in 0 until amount) {
packets.add(null)
@ -77,6 +87,38 @@ class PacketRegistry(val isLegacy: Boolean) {
packets.add(null)
}
// avoid zip bomb
private class LimitingInputStream(inputStream: InputStream) : FilterInputStream(inputStream) {
private var read = 0L
override fun read(): Int {
if (read >= MAX_PACKET_SIZE)
return -1
read++
return super.read()
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
if (read >= MAX_PACKET_SIZE)
return -1
val actual = super.read(b, off, len.coerceAtMost((MAX_PACKET_SIZE - read).coerceAtMost(Int.MAX_VALUE.toLong()).toInt()))
if (actual > 0)
read += actual
return actual
}
override fun available(): Int {
if (read >= MAX_PACKET_SIZE)
return 0
else
return super.available()
}
}
inner class Serializer(val side: ConnectionSide) : ChannelDuplexHandler() {
private val backlog = ByteArrayList()
private var discardBytes = 0
@ -105,17 +147,23 @@ class PacketRegistry(val isLegacy: Boolean) {
val stream: InputStream
if (isCompressed) {
stream = BufferedInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size)))
stream = BufferedInputStream(LimitingInputStream(InflaterInputStream(FastByteArrayInputStream(backlog.elements(), 0, backlog.size))))
} else {
stream = FastByteArrayInputStream(backlog.elements(), 0, backlog.size)
}
try {
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy))
} catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err)
// legacy protocol allows to stitch multiple packets of same type together without
// separate headers for each
while (stream.available() > 0) {
try {
ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy))
} catch (err: Throwable) {
LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err)
}
}
stream.close()
backlog.clear()
readingType = null
isCompressed = false
@ -142,6 +190,9 @@ class PacketRegistry(val isLegacy: Boolean) {
} else if (!type.direction.acceptedOn(side)) {
LOGGER.error("Packet type $packetType (${type.type}) can not be accepted on side $side! Discarding ${dataLength.absoluteValue} bytes")
discardBytes = dataLength.absoluteValue
} else if (dataLength.absoluteValue >= MAX_PACKET_SIZE) {
LOGGER.error("Packet ($packetType/${type.type}) of ${dataLength.absoluteValue} bytes is bigger than maximum allowed $MAX_PACKET_SIZE bytes")
discardBytes = dataLength.absoluteValue
} else {
LOGGER.debug("Packet type {} ({}) received on {} (size {} bytes)", packetType, type.type, side, dataLength.absoluteValue)
readingType = type
@ -171,13 +222,39 @@ class PacketRegistry(val isLegacy: Boolean) {
if (isLegacy)
check(stream.length > 0) { "Packet $msg didn't write any data to network, this is not allowed by legacy protocol" }
val buff = ctx.alloc().buffer(stream.length + 5)
val stream2 = ByteBufOutputStream(buff)
stream2.writeByte(type.id)
stream2.writeSignedVarInt(stream.length)
stream2.write(stream.array, 0, stream.length)
LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length)
ctx.write(buff, promise)
if (stream.length >= 512) {
// compress
val deflater = Deflater(3)
val buffers = ByteArrayList(1024)
val buffer = ByteArray(1024)
deflater.setInput(stream.array, 0, stream.length)
while (!deflater.needsInput()) {
val deflated = deflater.deflate(buffer)
if (deflated > 0)
buffers.addElements(buffers.size, buffer, 0, deflated)
else
break
}
val buff = ctx.alloc().buffer(buffers.size + 5)
val stream2 = ByteBufOutputStream(buff)
stream2.writeByte(type.id)
stream2.writeSignedVarInt(-buffers.size)
stream2.write(buffers.elements(), 0, buffers.size)
LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes / COMPRESSED size {} bytes)", type.id, type.type, side, stream.length, buffers.size)
ctx.write(buff, promise)
} else {
// send as-is
val buff = ctx.alloc().buffer(stream.length + 5)
val stream2 = ByteBufOutputStream(buff)
stream2.writeByte(type.id)
stream2.writeSignedVarInt(stream.length)
stream2.write(stream.array, 0, stream.length)
LOGGER.debug("Packet type {} ({}) sent from {} (size {} bytes)", type.id, type.type, side, stream.length)
ctx.write(buff, promise)
}
}
}
}
@ -213,6 +290,12 @@ class PacketRegistry(val isLegacy: Boolean) {
}
companion object {
const val MAX_PACKET_SIZE = 64L * 1024L * 1024L // 64 MiB
// this includes both compressed and uncompressed
// Original game allows 16 mebibyte packets
// but it doesn't account for compression bomb (packets are fully uncompressed
// right away without limiting decompressed output size)
private val LOGGER = LogManager.getLogger()
val NATIVE = PacketRegistry(false)
@ -266,16 +349,16 @@ class PacketRegistry(val isLegacy: Boolean) {
// Packets sent bidirectionally between the universe client and the universe
// server
LEGACY.skip("ClientContextUpdate")
LEGACY.add(ClientContextUpdatePacket::read)
// Packets sent world server -> world client
LEGACY.add(::WorldStartPacket) // WorldStart
LEGACY.skip("WorldStop")
LEGACY.add(::WorldStopPacket)
LEGACY.skip("WorldLayoutUpdate")
LEGACY.skip("WorldParametersUpdate")
LEGACY.skip("CentralStructureUpdate")
LEGACY.skip("TileArrayUpdate")
LEGACY.skip("TileUpdate")
LEGACY.add(LegacyTileArrayUpdatePacket::read)
LEGACY.add(LegacyTileUpdatePacket::read)
LEGACY.skip("TileLiquidUpdate")
LEGACY.skip("TileDamageUpdate")
LEGACY.skip("TileModificationFailure")
@ -286,7 +369,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("SetDungeonBreathable")
LEGACY.skip("SetPlayerStart")
LEGACY.skip("FindUniqueEntityResponse")
LEGACY.skip("Pong")
LEGACY.add(PongPacket)
// Packets sent world client -> world server
LEGACY.skip("ModifyTileList")
@ -299,7 +382,7 @@ class PacketRegistry(val isLegacy: Boolean) {
LEGACY.skip("WorldClientStateUpdate")
LEGACY.skip("FindUniqueEntity")
LEGACY.skip("WorldStartAcknowledge")
LEGACY.skip("Ping")
LEGACY.add(PingPacket)
// Packets sent bidirectionally between world client and world server
LEGACY.skip("EntityCreate")

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
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.server.ServerConnection
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
data class ProtocolRequestPacket(val version: Int) : IServerPacket {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt())

View File

@ -5,7 +5,7 @@ import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeUUID
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kstarbound.client.ClientConnection
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.network.IClientPacket
import java.io.DataInputStream
import java.io.DataOutputStream

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

View File

@ -1,11 +1,15 @@
package ru.dbotthepony.kstarbound.server
import io.netty.channel.ChannelHandlerContext
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.ByteKey
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.client.network.packets.ForgetChunkPacket
import ru.dbotthepony.kstarbound.client.network.packets.ChunkCellsPacket
import ru.dbotthepony.kstarbound.client.network.packets.ForgetEntityPacket
@ -14,16 +18,34 @@ import ru.dbotthepony.kstarbound.network.Connection
import ru.dbotthepony.kstarbound.network.ConnectionSide
import ru.dbotthepony.kstarbound.network.ConnectionType
import ru.dbotthepony.kstarbound.network.IServerPacket
import ru.dbotthepony.kstarbound.network.packets.ClientContextUpdatePacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.LegacyTileArrayUpdatePacket
import ru.dbotthepony.kstarbound.server.world.IChunkSource
import ru.dbotthepony.kstarbound.server.world.LegacyChunkSource
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.api.ImmutableCell
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.WorldObject
import java.util.*
import kotlin.properties.Delegates
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type, UUID(0L, 0L)) {
// serverside part of connection
class ServerConnection(val server: StarboundServer, type: ConnectionType) : Connection(ConnectionSide.SERVER, type) {
var world: ServerWorld? = null
lateinit var shipWorld: ServerWorld
private set
init {
connectionID = server.nextConnectionID.incrementAndGet()
}
override fun toString(): String {
val channel = if (hasChannel) channel.remoteAddress().toString() else "<no channel>"
val ship = if (::shipWorld.isInitialized) shipWorld.toString() else "<no shipworld>"
return "ServerConnection[ID=$connectionID channel=$channel / $ship]"
}
var trackedPosition: Vector2d = Vector2d.ZERO
set(value) {
@ -52,6 +74,25 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
}
private val shipChunks = Object2ObjectOpenHashMap<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 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() {
tickets.values.forEach { it.cancel() }
tickets.clear()
pendingSend.clear()
}
override fun onChannelClosed() {
super.onChannelClosed()
if (::shipWorld.isInitialized) {
shipWorld.close()
}
}
private fun recomputeTrackedChunks() {
val world = world ?: return
val trackedPositionChunk = world.geometry.chunkFromCell(trackedPosition)
@ -129,7 +193,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
for (pos in itr) {
val chunk = world.chunkMap[pos] ?: continue
send(ChunkCellsPacket(chunk))
if (isLegacy) {
send(LegacyTileArrayUpdatePacket(chunk))
} else {
send(ChunkCellsPacket(chunk))
}
itr.remove()
}
}
@ -149,7 +219,13 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
}
override fun inGame() {
server.playerInGame(this)
// server.playerInGame(this)
LOGGER.info("Initializing ship world for $this")
shipWorld = ServerWorld(server, WorldGeometry(Vector2i(2048, 2048), false, false))
shipWorld.addChunkSource(shipChunkSource)
shipWorld.thread.start()
shipWorld.acceptPlayer(this)
}
companion object {

View File

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

View File

@ -22,20 +22,21 @@ import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.util.concurrent.CompletableFuture
import java.util.function.Supplier
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
private val carrier = CarriedExecutor(Starbound.STORAGE_IO_POOL)
class LegacyChunkSource(val loader: Loader) : IChunkSource {
fun interface Loader {
operator fun invoke(at: ByteKey): CompletableFuture<KOptional<ByteArray>>
}
override fun getTiles(pos: ChunkPos): CompletableFuture<KOptional<Object2DArray<out AbstractCell>>> {
val chunkX = pos.x
val chunkY = pos.y
val key = ByteKey(1, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carrier).thenApplyAsync {
return loader(key).thenApplyAsync {
it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater())))
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
reader.skipBytes(3)
val result = Object2DArray.nulls<ImmutableCell>(CHUNK_SIZE, CHUNK_SIZE)
@ -46,6 +47,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
}
}
reader.close()
result as Object2DArray<out AbstractCell>
}
}
@ -56,9 +58,9 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
val chunkY = pos.y
val key = ByteKey(2, (chunkX shr 8).toByte(), chunkX.toByte(), (chunkY shr 8).toByte(), chunkY.toByte())
return CompletableFuture.supplyAsync(Supplier { db.read(key) }, carrier).thenApplyAsync {
return loader(key).thenApplyAsync {
it.map {
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it), Inflater())))
val reader = DataInputStream(BufferedInputStream(InflaterInputStream(ByteArrayInputStream(it))))
val i = reader.readVarInt()
val objects = ArrayList<AbstractEntity>()
@ -76,6 +78,7 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
}
}
reader.close()
objects
}
}
@ -84,5 +87,15 @@ class LegacyChunkSource(val db: BTreeDB5) : IChunkSource {
companion object {
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
private val LOGGER = LogManager.getLogger()
fun file(file: BTreeDB5): LegacyChunkSource {
val carrier = CarriedExecutor(Starbound.IO_EXECUTOR)
val loader = Loader { key -> CompletableFuture.supplyAsync(Supplier { file.read(key) }, carrier) }
return LegacyChunkSource(loader)
}
fun memory(backing: Map<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.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import kotlinx.coroutines.future.await
import ru.dbotthepony.kommons.collect.chainOptionalFutures
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.io.BTreeDB6
import ru.dbotthepony.kommons.util.AABBi
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.CelestialGenerationInformation
import ru.dbotthepony.kstarbound.defs.CelestialNames
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialGenerationInformation
import ru.dbotthepony.kstarbound.defs.world.CelestialNames
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.io.BTreeDB5
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.world.CoordinateMapper
import ru.dbotthepony.kstarbound.world.Universe
import ru.dbotthepony.kstarbound.world.UniversePos
import ru.dbotthepony.kstarbound.world.positiveModulo
import java.io.Closeable
import java.io.File
import java.io.InputStreamReader
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList
@ -69,18 +67,39 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
private val sources = ArrayList<UniverseSource>()
private val closeables = ArrayList<Closeable>()
override fun name(pos: UniversePos): CompletableFuture<KOptional<String>> {
return getChunk(pos).thenApply {
it.flatMap { it.parameters(pos) }.map { it.name }
}
override suspend fun parameters(pos: UniversePos): CelestialParameters? {
return getChunk(pos)?.parameters(pos)
}
override fun scanSystems(region: AABBi, includedTypes: Set<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 futures = ArrayList<CompletableFuture<List<UniversePos>>>()
for (pos in chunkPositions(region)) {
val f = getChunk(pos).thenApply {
val f = getChunkFuture(pos).thenApply {
it.map<List<UniversePos>> {
val result = ArrayList<UniversePos>()
@ -103,23 +122,23 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
futures.add(f)
}
return CompletableFuture.allOf(*futures.toTypedArray())
.thenApply { futures.stream().flatMap { it.get().stream() }.toList() }
CompletableFuture.allOf(*futures.toTypedArray()).await()
return futures.stream().flatMap { it.get().stream() }.toList()
}
override fun scanConstellationLines(region: AABBi): CompletableFuture<List<Pair<Vector2i, Vector2i>>> {
override suspend fun scanConstellationLines(region: AABBi): List<Pair<Vector2i, Vector2i>> {
val futures = ArrayList<CompletableFuture<List<Pair<Vector2i, Vector2i>>>>()
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())
}
futures.add(f)
}
return CompletableFuture.allOf(*futures.toTypedArray())
.thenApply { futures.stream().flatMap { it.get().stream() }.toList() }
CompletableFuture.allOf(*futures.toTypedArray()).await()
return futures.stream().flatMap { it.get().stream() }.toList()
}
override fun scanRegionFullyLoaded(region: AABBi): Boolean {
@ -140,12 +159,22 @@ class ServerUniverse private constructor(marker: Nothing?) : Universe(), Closeab
.scheduler(Scheduler.systemScheduler())
.build()
fun getChunk(pos: UniversePos): CompletableFuture<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)))
}
fun getChunk(pos: Vector2i): CompletableFuture<KOptional<UniverseChunk>> {
return chunkCache.get(pos) { p -> chainOptionalFutures(sources) { it.getChunk(p) } }
suspend fun getChunk(pos: Vector2i): UniverseChunk? {
val get = getChunkFuture(pos).await()
if (get.isPresent) {
return get.value
} else {
return null
}
}
init {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.util
import ru.dbotthepony.kstarbound.Starbound
import java.io.File
object AssetPathStack {
private val _stack = object : ThreadLocal<ArrayDeque<String>>() {
@ -49,4 +50,11 @@ object AssetPathStack {
fun remapSafe(path: String): String {
return remap(last() ?: return path, path)
}
fun relativeTo(base: String, path: String): String {
if (path.isNotEmpty() && path[0] == '/')
return path
return if (base.endsWith('/')) "$base$path" else "${base.substringBeforeLast('/')}/$path"
}
}

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