WorldTemplate, WorldLayout, VisitableWorldParameters, Biomes, BiomeDefinitions, Trees, Grass, Bushes, Terrain Selectors
RenderDirectives, JsonRPC, Json adapters fixes, DispatchingTypeAdapter
This commit is contained in:
parent
73cf5f596c
commit
d37bad79c6
20
ADDITIONS.md
Normal file
20
ADDITIONS.md
Normal 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.
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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/"))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) })
|
||||
|
@ -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()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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 }
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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() != "")
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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?) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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>>()
|
||||
|
@ -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,
|
||||
)
|
||||
))
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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>)
|
||||
}
|
@ -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}" }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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(),
|
||||
)
|
@ -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(),
|
||||
)
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>)
|
@ -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}" }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
161
src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt
Normal file
161
src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
165
src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt
Normal file
165
src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Sky.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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>,
|
||||
)
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package ru.dbotthepony.kstarbound.defs.world
|
||||
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
|
||||
@JsonFactory
|
||||
data class WorldTemplateConfig(
|
||||
val playerStartSearchYRange: Int,
|
||||
)
|
@ -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]"
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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>()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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>()
|
||||
|
@ -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) {
|
||||
|
@ -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>()
|
||||
|
@ -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])
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
137
src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt
Normal file
137
src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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>)
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user