Plant entities?

This commit is contained in:
DBotThePony 2024-04-21 22:26:02 +07:00
parent 151470f76d
commit 135f8e9728
Signed by: DBot
GPG Key ID: DCC23B5715498507
50 changed files with 1417 additions and 222 deletions

View File

@ -10,7 +10,7 @@ This document briefly documents what have been added (or removed) regarding modd
### Worldgen ### Worldgen
* Where applicable, Perlin noise now can have custom seed specified * Where applicable, Perlin noise now can have custom seed specified
* Change above allows to explicitly specify universe seed (as `celestial.config:systemTypePerlin:seed`) * 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, 2048>=x>=16) * `treasurechests` now can specify `treasurePool` as array
#### Terrain #### Terrain
* All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them * All composing terrain selectors (such as `min`, `displacement`, `rotate`, etc) now can reference other terrain selectors by name (the `.terrain` files) instead of embedding entire config inside them
@ -143,6 +143,7 @@ val color: TileColor = TileColor.DEFAULT
### Worldgen ### Worldgen
* Major dungeon placement on planets is now deterministic * Major dungeon placement on planets is now deterministic
* Container item population in dungeons is now deterministic and is based on dungeon seed * Container item population in dungeons is now deterministic and is based on dungeon seed
* However, this might backfire, if you specify `seed` inside `/instance_worlds.config`; since that will set dungeon's contents in stone (don't do this, remove seed from your dungeon data, please. Both original and new engines will provide random seed for you on each world generation if you remove your own seed from data)
#### Dungeons #### Dungeons
* All brushes are now deterministic * All brushes are now deterministic

View File

@ -14,7 +14,7 @@ import ru.dbotthepony.kstarbound.defs.WorldServerConfig
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig import ru.dbotthepony.kstarbound.defs.item.ItemDropConfig
import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig import ru.dbotthepony.kstarbound.defs.item.ItemGlobalConfig
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig
import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation import ru.dbotthepony.kstarbound.defs.world.CelestialBaseInformation
@ -30,7 +30,6 @@ import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.json.mapAdapter import ru.dbotthepony.kstarbound.json.mapAdapter
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.properties.Delegates import kotlin.properties.Delegates
import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KMutableProperty0
@ -62,16 +61,16 @@ object Globals {
var dungeonWorlds by Delegates.notNull<ImmutableMap<String, DungeonWorldsConfig>>() var dungeonWorlds by Delegates.notNull<ImmutableMap<String, DungeonWorldsConfig>>()
private set private set
var grassDamage by Delegates.notNull<TileDamageConfig>() var grassDamage by Delegates.notNull<TileDamageParameters>()
private set private set
var treeDamage by Delegates.notNull<TileDamageConfig>() var treeDamage by Delegates.notNull<TileDamageParameters>()
private set private set
var bushDamage by Delegates.notNull<TileDamageConfig>() var bushDamage by Delegates.notNull<TileDamageParameters>()
private set private set
var tileDamage by Delegates.notNull<TileDamageConfig>() var tileDamage by Delegates.notNull<TileDamageParameters>()
private set private set
var sky by Delegates.notNull<SkyGlobalConfig>() var sky by Delegates.notNull<SkyGlobalConfig>()

View File

@ -3,12 +3,10 @@ package ru.dbotthepony.kstarbound
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
@ -28,11 +26,12 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.RenderParameters import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.BushVariant
@ -76,6 +75,7 @@ object Registries {
val projectiles = Registry<ProjectileDefinition>("projectile").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 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 treasurePools = Registry<TreasurePoolDefinition>("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val treasureChests = Registry<TreasureChestDefinition>("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) } val 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 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 worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
@ -177,6 +177,7 @@ object Registries {
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(treasureChests, fileTree["treasurechests"] ?: listOf(), patchTree) { name = it })
tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it }) tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })
return tasks return tasks
@ -271,7 +272,7 @@ object Registries {
renderParameters = RenderParameters.META, renderParameters = RenderParameters.META,
isConnectable = def.isConnectable, isConnectable = def.isConnectable,
supportsMods = def.supportsMods, supportsMods = def.supportsMods,
damageTable = AssetReference(TileDamageConfig( damageTable = AssetReference(TileDamageParameters(
damageFactors = ImmutableMap.of(), damageFactors = ImmutableMap.of(),
damageRecovery = Double.MAX_VALUE, damageRecovery = Double.MAX_VALUE,
maximumEffectTime = 0.0, maximumEffectTime = 0.0,

View File

@ -18,13 +18,16 @@ inline fun <reified S : Any> Registry<S>.adapter(): TypeAdapterFactory {
class RegistryTypeAdapterFactory<S : Any>(private val registry: Registry<S>, private val clazz: KClass<S>) : TypeAdapterFactory { 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>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val subtype = type.type as? ParameterizedType ?: return null if (type.rawType == Registry.Entry::class.java || type.rawType == Registry.Ref::class.java) {
if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != clazz.java) return null val subtype = type.type as? ParameterizedType ?: throw IllegalArgumentException("Non-parametized registry reference type: $type")
if (subtype.actualTypeArguments.size != 1) throw RuntimeException(type.toString())
if (subtype.actualTypeArguments[0] != clazz.java) return null
if (type.rawType == Registry.Entry::class.java) { if (type.rawType == Registry.Entry::class.java) {
return EntryImpl(gson) as TypeAdapter<T> return EntryImpl(gson) as TypeAdapter<T>
} else if (type.rawType == Registry.Ref::class.java) { } else if (type.rawType == Registry.Ref::class.java) {
return RefImpl(gson) as TypeAdapter<T> return RefImpl(gson) as TypeAdapter<T>
}
} }
return null return null

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@ -10,6 +11,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableMap import ru.dbotthepony.kommons.guava.immutableMap
import ru.dbotthepony.kstarbound.json.builder.JsonImplementation import ru.dbotthepony.kstarbound.json.builder.JsonImplementation
@ -92,6 +94,16 @@ data class ThingDescription(
} }
} }
fun toJsonObject(): JsonObject {
return JsonObject().apply {
this["description"] = description
racialDescription.forEach { t, u ->
this[t] = u
}
}
}
fun fixDescription(newDescription: String): ThingDescription { fun fixDescription(newDescription: String): ThingDescription {
return copy( return copy(
shortdescription = if (shortdescription == "...") newDescription else shortdescription, shortdescription = if (shortdescription == "...") newDescription else shortdescription,

View File

@ -8,7 +8,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
data class PerlinNoiseParameters( data class PerlinNoiseParameters(
val type: Type = Type.PERLIN, val type: Type = Type.PERLIN,
val seed: Long? = null, val seed: Long? = null,
val scale: Int = DEFAULT_SCALE,
val octaves: Int = 1, val octaves: Int = 1,
val gain: Double = 2.0, val gain: Double = 2.0,
val offset: Double = 1.0, val offset: Double = 1.0,
@ -18,11 +17,6 @@ data class PerlinNoiseParameters(
val amplitude: Double = 1.0, val amplitude: Double = 1.0,
val bias: Double = 0.0, val bias: Double = 0.0,
) { ) {
init {
require(scale >= 16) { "Too little perlin noise scale: $scale" }
require(scale <= 2048) { "Absurd noise scale: $scale" }
}
enum class Type(override val jsonName: String) : IStringSerializable { enum class Type(override val jsonName: String) : IStringSerializable {
UNITIALIZED("uninitialized"), UNITIALIZED("uninitialized"),
PERLIN("perlin"), PERLIN("perlin"),
@ -35,8 +29,4 @@ data class PerlinNoiseParameters(
return name.lowercase() == lower return name.lowercase() == lower
} }
} }
companion object {
const val DEFAULT_SCALE = 512
}
} }

View File

@ -3,12 +3,14 @@ package ru.dbotthepony.kstarbound.defs.dungeon
import com.google.gson.JsonObject import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
@ -35,6 +37,7 @@ import java.util.Collections
import java.util.LinkedHashSet import java.util.LinkedHashSet
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.function.Consumer import java.util.function.Consumer
import java.util.function.Supplier
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
// Facade world for generating dungeons, so generation can be performed without affecting world state, // Facade world for generating dungeons, so generation can be performed without affecting world state,
@ -529,6 +532,26 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
} }
.filter { it.first != null } .filter { it.first != null }
val biomeItemsFutures = biomeItems.map {
CompletableFuture.supplyAsync(Supplier {
parent.template.potentialBiomeItemsAt(it.x, it.y).surfaceBiomeItems to it
}, Starbound.EXECUTOR)
}
val biomeItems = ArrayList<() -> Unit>()
for (biomeItem in biomeItemsFutures) {
try {
val (placeables, pos) = biomeItem.await()
for (placeable in placeables) {
biomeItems.add(placeable.item.createPlacementFunc(parent, random, pos))
}
} catch (err: Throwable) {
LOGGER.error("Exception while evaluating dungeon biome placeables", err)
}
}
// wait for all chunks to be loaded (and cell changes to be applied) // wait for all chunks to be loaded (and cell changes to be applied)
// if any of cell change operation fails, entire generation fails... leaving world in inconsistent state, // if any of cell change operation fails, entire generation fails... leaving world in inconsistent state,
// but also limiting damage by exiting early. // but also limiting damage by exiting early.
@ -554,6 +577,15 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar
} }
} }
// place biome items
for (placement in biomeItems) {
try {
placement()
} catch (err: Throwable) {
LOGGER.error("Exception while placing biome items for dungeon", err)
}
}
// objects are placed, now place wiring // objects are placed, now place wiring
for (wiring in localWires) { for (wiring in localWires) {
try { try {

View File

@ -3,8 +3,10 @@ package ru.dbotthepony.kstarbound.defs.image
import com.github.benmanes.caffeine.cache.AsyncLoadingCache import com.github.benmanes.caffeine.cache.AsyncLoadingCache
import com.github.benmanes.caffeine.cache.CacheLoader import com.github.benmanes.caffeine.cache.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import com.github.benmanes.caffeine.cache.Scheduler import com.github.benmanes.caffeine.cache.Scheduler
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonNull import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
@ -37,6 +39,7 @@ import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.getObject import ru.dbotthepony.kommons.gson.getObject
import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.lang.ref.Reference import java.lang.ref.Reference
@ -104,26 +107,8 @@ class Image private constructor(
} }
} }
val data: CompletableFuture<ByteBuffer> get() { val data: ByteBuffer
var get = dataRef?.get() get() = dataCache.get(source)
if (get != null)
return CompletableFuture.completedFuture(get)
synchronized(lock) {
get = dataRef?.get()
if (get != null)
return CompletableFuture.completedFuture(get)
val f = dataCache.get(source)
if (f.isDone)
dataRef = WeakReference(f.get())
return f.copy()
}
}
val texture: GLTexture2D get() { val texture: GLTexture2D get() {
//val get = _texture.get()?.get() //val get = _texture.get()?.get()
@ -140,12 +125,10 @@ class Image private constructor(
client.named2DTextures1.get(this) { client.named2DTextures1.get(this) {
val tex = GLTexture2D(width, height, GL45.GL_RGBA8) val tex = GLTexture2D(width, height, GL45.GL_RGBA8)
data.thenApplyAsync({ tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, data)
tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, it)
tex.textureMinFilter = GL45.GL_NEAREST tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST tex.textureMagFilter = GL45.GL_NEAREST
}, client)
tex tex
} }
@ -187,7 +170,12 @@ class Image private constructor(
return whole.worldSpaces(pixelOffset, spaceScan, flip) return whole.worldSpaces(pixelOffset, spaceScan, flip)
} }
fun worldSpaces(pixelOffset: Vector2d, spaceScan: Double, flip: Boolean): Set<Vector2i> {
return whole.worldSpaces(Vector2i(pixelOffset.x.toInt(), pixelOffset.y.toInt()), spaceScan, flip)
}
private data class DataSprite(val name: String, val coordinates: Vector4i) private data class DataSprite(val name: String, val coordinates: Vector4i)
private data class SpaceScanKey(val sprite: Sprite, val pixelOffset: Vector2i, val spaceScan: Double, val flip: Boolean)
inner class Sprite(val name: String, val x: Int, val y: Int, val width: Int, val height: Int) : IUVCoordinates { inner class Sprite(val name: String, val x: Int, val y: Int, val width: Int, val height: Int) : IUVCoordinates {
// flip coordinates to account for opengl // flip coordinates to account for opengl
@ -204,7 +192,7 @@ class Image private constructor(
require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" } require(x in 0 until width && y in 0 until height) { "Position out of bounds: $x $y" }
val offset = (this.y + y) * this@Image.width * 4 + (this.x + x) * 4 val offset = (this.y + y) * this@Image.width * 4 + (this.x + x) * 4
val data = data.join() val data = data
return data[offset].toInt().and(0xFF) or // red return data[offset].toInt().and(0xFF) or // red
data[offset + 1].toInt().and(0xFF).shl(8) or // green data[offset + 1].toInt().and(0xFF).shl(8) or // green
@ -265,6 +253,12 @@ class Image private constructor(
} }
fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> { fun worldSpaces(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip)) {
ImmutableSet.copyOf(worldSpaces0(pixelOffset, spaceScan, flip))
}
}
private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set<Vector2i> {
val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi val minX = pixelOffset.x / PIXELS_IN_STARBOUND_UNITi
val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi val minY = pixelOffset.y / PIXELS_IN_STARBOUND_UNITi
val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi val maxX = (width + pixelOffset.x + PIXELS_IN_STARBOUND_UNITi - 1) / PIXELS_IN_STARBOUND_UNITi
@ -340,19 +334,24 @@ class Image private constructor(
return ReadDirectData(data, getWidth[0], getHeight[0], components[0]) return ReadDirectData(data, getWidth[0], getHeight[0], components[0])
} }
private val dataCache: AsyncLoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder() private val dataCache: LoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1)) .expireAfterAccess(Duration.ofMinutes(1))
.weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() } .weigher<IStarboundFile, ByteBuffer> { key, value -> value.capacity() }
.maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */)) .maximumWeight((Runtime.getRuntime().maxMemory() / 4L).coerceIn(1_024L * 1_024L * 32L /* 32 МиБ */, 1_024L * 1_024L * 256L /* 256 МиБ */))
.scheduler(Starbound) .scheduler(Starbound)
.executor(Starbound.EXECUTOR) // SCREENED_EXECUTOR shouldn't be used here .executor(Starbound.EXECUTOR) // SCREENED_EXECUTOR shouldn't be used here
.buildAsync(CacheLoader { .build { readImageDirect(it).data }
readImageDirect(it).data
}) private val spaceScanCache = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(30))
.softValues()
.scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR)
.build<SpaceScanKey, ImmutableSet<Vector2i>>()
@JvmStatic @JvmStatic
fun get(path: String): Image? { fun get(path: String): Image? {
return imageCache.computeIfAbsent(path) { return imageCache.computeIfAbsent(path.substringBefore(':').substringBefore('?')) {
try { try {
val file = Starbound.locate(it) val file = Starbound.locate(it)

View File

@ -0,0 +1,51 @@
package ru.dbotthepony.kstarbound.defs.item
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.annotations.JsonAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.getAdapter
import java.util.stream.Stream
import kotlin.properties.Delegates
@JsonAdapter(TreasureChestDefinition.Adapter::class)
data class TreasureChestDefinition(
val variants: ImmutableList<Variant>,
) {
var name: String by Delegates.notNull()
@JsonFactory
data class Variant(
val containers: ImmutableSet<Registry.Ref<ObjectDefinition>>,
val treasurePool: Either<ImmutableSet<Registry.Ref<TreasurePoolDefinition>>, Registry.Ref<TreasurePoolDefinition>>,
val minimumLevel: Double = 0.0
) {
val validContainers: ImmutableSet<Registry.Entry<ObjectDefinition>> by lazy {
containers.stream().filter { it.isPresent }.map { it.entry!! }.collect(ImmutableSet.toImmutableSet())
}
val validTreasurePools: ImmutableSet<Registry.Entry<TreasurePoolDefinition>> by lazy {
treasurePool.map({ it.stream() }, { Stream.of(it) }).filter { it.isPresent }.map { it.entry!! }.collect(ImmutableSet.toImmutableSet())
}
}
class Adapter(gson: Gson) : TypeAdapter<TreasureChestDefinition>() {
private val variants = gson.getAdapter<ImmutableList<Variant>>()
override fun write(out: JsonWriter, value: TreasureChestDefinition) {
variants.write(out, value.variants)
}
override fun read(`in`: JsonReader): TreasureChestDefinition {
return TreasureChestDefinition(variants.read(`in`))
}
}
}

View File

@ -19,7 +19,7 @@ import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.defs.JsonReference import ru.dbotthepony.kstarbound.defs.JsonReference
import ru.dbotthepony.kstarbound.defs.actor.StatModifier import ru.dbotthepony.kstarbound.defs.actor.StatModifier
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.listAdapter
@ -81,7 +81,7 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false, val biomePlaced: Boolean = false,
val printable: Boolean = false, val printable: Boolean = false,
val smashOnBreak: Boolean = false, val smashOnBreak: Boolean = false,
val damageConfig: TileDamageConfig, val damageConfig: TileDamageParameters,
val flickerPeriod: PeriodicFunction? = null, val flickerPeriod: PeriodicFunction? = null,
val orientations: ImmutableList<ObjectOrientation>, val orientations: ImmutableList<ObjectOrientation>,
) { ) {
@ -153,7 +153,7 @@ data class ObjectDefinition(
private val objectRef = gson.getAdapter(JsonReference.Object::class.java) private val objectRef = gson.getAdapter(JsonReference.Object::class.java)
private val basic = gson.getAdapter(PlainData::class.java) private val basic = gson.getAdapter(PlainData::class.java)
private val damageConfig = gson.getAdapter(TileDamageConfig::class.java) private val damageConfig = gson.getAdapter(TileDamageParameters::class.java)
private val damageTeam = gson.getAdapter(DamageTeam::class.java) private val damageTeam = gson.getAdapter(DamageTeam::class.java)
private val orientations = gson.getAdapter(ObjectOrientation::class.java) private val orientations = gson.getAdapter(ObjectOrientation::class.java)
private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java) private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java)

View File

@ -2,8 +2,6 @@ package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMap
import it.unimi.dsi.fastutil.objects.Object2DoubleMaps
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
@ -131,7 +129,7 @@ object BuiltinMetaMaterials {
isMeta = true, isMeta = true,
supportsMods = false, supportsMods = false,
collisionKind = collisionType, collisionKind = collisionType,
damageTable = AssetReference(TileDamageConfig( damageTable = AssetReference(TileDamageParameters(
damageFactors = ImmutableMap.of(), damageFactors = ImmutableMap.of(),
damageRecovery = Double.MAX_VALUE, damageRecovery = Double.MAX_VALUE,
maximumEffectTime = 0.0, maximumEffectTime = 0.0,

View File

@ -3,16 +3,32 @@ package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.io.readMap
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeMap
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory @JsonFactory
data class TileDamageConfig( data class TileDamageParameters(
val damageFactors: ImmutableMap<String, Double> = ImmutableMap.of(), val damageFactors: ImmutableMap<String, Double> = ImmutableMap.of(),
val damageRecovery: Double = 1.0, val damageRecovery: Double = 1.0,
val harvestLevel: Int = 1,
val maximumEffectTime: Double = 1.5, val maximumEffectTime: Double = 1.5,
val totalHealth: Double = 1.0, val totalHealth: Double = 1.0,
val harvestLevel: Int = 1,
) { ) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
ImmutableMap.copyOf(stream.readMap({ TileDamageType.entries[readUnsignedByte()].jsonName }, { readDouble(isLegacy) })),
stream.readDouble(isLegacy),
stream.readInt(),
stream.readDouble(isLegacy),
stream.readDouble(isLegacy),
)
val damageFactorsMapped: ImmutableMap<TileDamageType, Double> = damageFactors.entries.stream().map { val damageFactorsMapped: ImmutableMap<TileDamageType, Double> = damageFactors.entries.stream().map {
var find = TileDamageType.entries.firstOrNull { e -> e.match(it.key) } var find = TileDamageType.entries.firstOrNull { e -> e.match(it.key) }
@ -28,7 +44,15 @@ data class TileDamageConfig(
return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount return (damageFactorsMapped[damage.type] ?: 1.0) * damage.amount
} }
operator fun plus(other: TileDamageConfig): TileDamageConfig { fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeMap(damageFactorsMapped, { writeByte(it.ordinal) }, { writeDouble(it, isLegacy) })
stream.writeDouble(damageRecovery, isLegacy)
stream.writeInt(harvestLevel)
stream.writeDouble(maximumEffectTime, isLegacy)
stream.writeDouble(totalHealth, isLegacy)
}
operator fun plus(other: TileDamageParameters): TileDamageParameters {
val damageRecovery = damageRecovery + other.damageRecovery val damageRecovery = damageRecovery + other.damageRecovery
val maximumEffectTime = maximumEffectTime.coerceAtLeast(other.maximumEffectTime) val maximumEffectTime = maximumEffectTime.coerceAtLeast(other.maximumEffectTime)
val totalHealth = totalHealth + other.totalHealth val totalHealth = totalHealth + other.totalHealth
@ -51,11 +75,11 @@ data class TileDamageConfig(
} }
} }
return TileDamageConfig(builder.build(), damageRecovery, maximumEffectTime, totalHealth, harvestLevel) return TileDamageParameters(builder.build(), damageRecovery, harvestLevel, maximumEffectTime, totalHealth)
} }
companion object { companion object {
val EMPTY = TileDamageConfig() val EMPTY = TileDamageParameters()
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
} }

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.defs.tile
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
// uint8_t
enum class TileDamageType(override val jsonName: String, val isPenetrating: Boolean) : IStringSerializable { enum class TileDamageType(override val jsonName: String, val isPenetrating: Boolean) : IStringSerializable {
// Damage done that will not actually kill the target // Damage done that will not actually kill the target
PROTECTED("protected", false), PROTECTED("protected", false),

View File

@ -27,7 +27,7 @@ data class TileDefinition(
val category: String, val category: String,
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
val damageTable: AssetReference<TileDamageConfig> = AssetReference(Globals::tileDamage), val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals::tileDamage),
val health: Double? = null, val health: Double? = null,
val requiredHarvestLevel: Int? = null, val requiredHarvestLevel: Int? = null,
@ -61,8 +61,8 @@ data class TileDefinition(
return !isMeta && !modifier.value.isMeta && supportsMods return !isMeta && !modifier.value.isMeta && supportsMods
} }
val actualDamageTable: TileDamageConfig by lazy { val actualDamageTable: TileDamageParameters by lazy {
val dmg = damageTable.value ?: TileDamageConfig.EMPTY val dmg = damageTable.value ?: TileDamageParameters.EMPTY
return@lazy if (health == null && requiredHarvestLevel == null) { return@lazy if (health == null && requiredHarvestLevel == null) {
dmg dmg

View File

@ -25,7 +25,7 @@ data class TileModifierDefinition(
val miningSounds: ImmutableList<String> = ImmutableList.of(), val miningSounds: ImmutableList<String> = ImmutableList.of(),
@Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable"))
val damageTable: AssetReference<TileDamageConfig> = AssetReference(Globals::tileDamage), val damageTable: AssetReference<TileDamageParameters> = AssetReference(Globals::tileDamage),
@JsonFlat @JsonFlat
val descriptionData: ThingDescription, val descriptionData: ThingDescription,
@ -42,8 +42,8 @@ data class TileModifierDefinition(
require(modId == null || modId > 0) { "Invalid tile modifier ID $modId" } require(modId == null || modId > 0) { "Invalid tile modifier ID $modId" }
} }
val actualDamageTable: TileDamageConfig by lazy { val actualDamageTable: TileDamageParameters by lazy {
val dmg = damageTable.value ?: TileDamageConfig.EMPTY val dmg = damageTable.value ?: TileDamageParameters.EMPTY
return@lazy if (health == null && requiredHarvestLevel == null) { return@lazy if (health == null && requiredHarvestLevel == null) {
dmg dmg

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableSet
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter import com.google.gson.TypeAdapter
@ -12,8 +13,10 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.stream import ru.dbotthepony.kommons.gson.stream
import ru.dbotthepony.kommons.gson.value import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
@ -23,15 +26,26 @@ import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.json.getAdapter
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.json.listAdapter import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.util.random.staticRandomInt import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.util.random.RandomGenerator
import java.util.stream.Stream import java.util.stream.Stream
@JsonFactory @JsonFactory
@ -73,6 +87,8 @@ data class BiomePlaceables(
abstract fun toJson(): JsonElement abstract fun toJson(): JsonElement
abstract fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit
companion object : TypeAdapterFactory { companion object : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? { override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Item::class.java.isAssignableFrom(type.rawType)) { if (Item::class.java.isAssignableFrom(type.rawType)) {
@ -80,7 +96,6 @@ data class BiomePlaceables(
private val grassVariant = gson.getAdapter(GrassVariant::class.java) private val grassVariant = gson.getAdapter(GrassVariant::class.java)
private val bushVariant = gson.getAdapter(BushVariant::class.java) private val bushVariant = gson.getAdapter(BushVariant::class.java)
private val trees = gson.listAdapter<TreeVariant>() private val trees = gson.listAdapter<TreeVariant>()
private val objects = gson.getAdapter(PoolTypeToken)
override fun write(out: JsonWriter, value: Item?) { override fun write(out: JsonWriter, value: Item?) {
if (value == null) if (value == null)
@ -114,12 +129,12 @@ data class BiomePlaceables(
// and world storage data at Chucklefish. // and world storage data at Chucklefish.
// Truly our hero here. // Truly our hero here.
val obj = when (val type = `in`.nextString()) { val obj = when (val type = `in`.nextString()) {
"treasureBoxSet" -> TreasureBox(`in`.nextString()) "treasureBoxSet" -> TreasureBox(Registries.treasureChests.ref(`in`.nextString()))
"microDungeon" -> MicroDungeon(Starbound.ELEMENTS_ADAPTER.arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet())) "microDungeon" -> MicroDungeon(Starbound.ELEMENTS_ADAPTER.arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet()))
"grass" -> Grass(grassVariant.read(`in`)) "grass" -> Grass(grassVariant.read(`in`))
"bush" -> Bush(bushVariant.read(`in`)) "bush" -> Bush(bushVariant.read(`in`))
"treePair" -> Tree(trees.read(`in`)) "treePair" -> Tree(trees.read(`in`))
"objectPool" -> Object(objects.read(`in`)) "objectPool" -> Object(objectPoolAdapter.read(`in`))
else -> throw JsonSyntaxException("Unknown biome placement item $type") else -> throw JsonSyntaxException("Unknown biome placement item $type")
} }
@ -143,14 +158,63 @@ data class BiomePlaceables(
microdungeons.forEach { j.add(JsonPrimitive(it.key.left())) } microdungeons.forEach { j.add(JsonPrimitive(it.key.left())) }
} }
} }
override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
return { }
}
} }
data class TreasureBox(val pool: String) : Item() { data class TreasureBox(val pool: Registry.Ref<TreasureChestDefinition>) : Item() {
override val type: BiomePlacementItemType override val type: BiomePlacementItemType
get() = BiomePlacementItemType.TREASURE_BOX_SET get() = BiomePlacementItemType.TREASURE_BOX_SET
override fun toJson(): JsonElement { override fun toJson(): JsonElement {
return JsonPrimitive(pool) return JsonPrimitive(pool.key.left())
}
override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
if (pool.isEmpty) {
LOGGER.error("Tried to place treasure chest '${pool.key.left()}' at ${position}, however, no such treasure chest exist")
} else {
val pools = pool.value!!.variants.filter { it.minimumLevel <= world.template.threatLevel }
if (pools.isEmpty())
return {}
val pool = pools.random(random)
if (pool.validContainers.isEmpty()) {
LOGGER.error("Tried to place treasure chest '${this.pool.key.left()}' at ${position}, however, no valid container objects exist for it (candidates: ${pool.containers})")
return {}
}
if (pool.validTreasurePools.isEmpty()) {
LOGGER.error("Tried to place treasure chest '${this.pool.key.left()}' at ${position}, however, no valid treasure pools exist for it (candidates: ${pool.treasurePool})")
return {}
}
val create = WorldObject.create(pool.validContainers.random(random), position, JsonObject().apply {
this["treasurePools"] = jsonArrayOf(pool.validTreasurePools.random(random).key)
this["treasureSeed"] = random.nextLong() // this value is ignored if created object is an actual container
// because of call to randomize()
})
if (create != null) {
val direction = Direction.entries[random.nextInt(2)]
create.randomize(random, world.template.threatLevel)
return {
val orientation = create.config.value.findValidOrientation(world, position, direction)
if (orientation != -1) {
create.orientationIndex = orientation.toLong()
create.joinWorld(world)
}
}
}
}
return { }
} }
} }
@ -161,6 +225,11 @@ data class BiomePlaceables(
override fun toJson(): JsonElement { override fun toJson(): JsonElement {
return Starbound.gson.toJsonTree(value) return Starbound.gson.toJsonTree(value)
} }
override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
val plant = PlantEntity(value, random)
return { plant.plant(world, position) }
}
} }
data class Bush(val value: BushVariant) : Item() { data class Bush(val value: BushVariant) : Item() {
@ -170,6 +239,11 @@ data class BiomePlaceables(
override fun toJson(): JsonElement { override fun toJson(): JsonElement {
return Starbound.gson.toJsonTree(value) return Starbound.gson.toJsonTree(value)
} }
override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
val plant = PlantEntity(value, random)
return { plant.plant(world, position) }
}
} }
data class Tree(val trees: ImmutableList<TreeVariant>) : Item() { data class Tree(val trees: ImmutableList<TreeVariant>) : Item() {
@ -182,19 +256,48 @@ data class BiomePlaceables(
TreeVariant::class.java TreeVariant::class.java
).type) ).type)
} }
}
private object PoolTypeToken : TypeToken<WeightedList<Pair<String, JsonElement>>>() override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
val plant = PlantEntity(trees.random(random), random)
return { plant.plant(world, position) }
}
}
// This structure sucks, but at least it allows unique parameters per // This structure sucks, but at least it allows unique parameters per
// each object (lmao, whos gonna write world json by hand anyway???? // each object (lmao, whos gonna write world json by hand anyway????
// considering this is world generation data.) // considering this is world generation data.)
data class Object(val pool: WeightedList<Pair<String, JsonElement>>) : Item() { data class Object(val pool: WeightedList<Pair<Registry.Ref<ObjectDefinition>, JsonObject>>) : Item() {
override val type: BiomePlacementItemType override val type: BiomePlacementItemType
get() = BiomePlacementItemType.OBJECT get() = BiomePlacementItemType.OBJECT
override fun toJson(): JsonElement { override fun toJson(): JsonElement {
return Starbound.gson.toJsonTree(pool, PoolTypeToken.type) return objectPoolAdapter.toJsonTree(pool)
}
override fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit {
pool.sample(random).ifPresent { (ref, parameters) ->
if (ref.isEmpty) {
LOGGER.error("Tried to place object '${ref.key}' at ${position}, however, no such object exist")
} else {
val create = WorldObject.create(ref.entry!!, position, parameters)
if (create != null) {
val direction = Direction.entries[random.nextInt(2)]
create.randomize(random, world.template.threatLevel)
return {
val orientation = create.config.value.findValidOrientation(world, position, direction)
if (orientation != -1) {
create.orientationIndex = orientation.toLong()
create.joinWorld(world)
}
}
}
}
}
return { }
} }
} }
@ -256,4 +359,21 @@ data class BiomePlaceables(
return null return null
} }
} }
companion object {
private val LOGGER = LogManager.getLogger()
// required because object : TypeToken<> will compile into wildcard type (because Pair<> is)
private val typeToken = TypeToken.getParameterized(
WeightedList::class.java,
TypeToken.getParameterized(
Pair::class.java,
TypeToken.getParameterized(Registry.Ref::class.java, ObjectDefinition::class.java).type,
JsonObject::class.java
).type
) as TypeToken<WeightedList<Pair<Registry.Ref<ObjectDefinition>, JsonObject>>>
private val objectPoolAdapter by lazy {
Starbound.gson.getAdapter(typeToken)
}
}
} }

View File

@ -5,12 +5,15 @@ import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kommons.collect.filterNotNull import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
@ -73,12 +76,23 @@ data class BiomePlaceablesDefinition(
} }
@JsonFactory @JsonFactory
data class TreasureBox(val treasureBoxSets: ImmutableSet<String> = ImmutableSet.of()) : DistributionItemData() { data class TreasureBox(val treasureBoxSets: ImmutableSet<Registry.Ref<TreasureChestDefinition>> = ImmutableSet.of()) : DistributionItemData() {
override val type: BiomePlacementItemType override val type: BiomePlacementItemType
get() = BiomePlacementItemType.MICRO_DUNGEON get() = BiomePlacementItemType.MICRO_DUNGEON
val validTreasureBoxSets: ImmutableSet<Registry.Entry<TreasureChestDefinition>> by lazy {
treasureBoxSets.stream().filter { it.isPresent }.map { it.entry!! }.collect(ImmutableSet.toImmutableSet())
}
override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item { override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
return BiomePlaceables.TreasureBox(treasureBoxSets.random(biome.random)) // this is quite ugly solution to cases where someone fucked up and specified all treasure chests wrong
if (treasureBoxSets.isEmpty()) {
return BiomePlaceables.TreasureBox(Registries.treasureChests.emptyRef)
} else if (validTreasureBoxSets.isEmpty()) {
return BiomePlaceables.TreasureBox(treasureBoxSets.random(biome.random))
}
return BiomePlaceables.TreasureBox(validTreasureBoxSets.random(biome.random).ref)
} }
} }
@ -94,7 +108,7 @@ data class BiomePlaceablesDefinition(
throw NoSuchElementException("None of grass variants are valid (candidates: $grasses)") throw NoSuchElementException("None of grass variants are valid (candidates: $grasses)")
} }
return BiomePlaceables.Grass(GrassVariant.Companion.create(valid.random(biome.random), biome.hueShift)) return BiomePlaceables.Grass(GrassVariant.create(valid.random(biome.random), biome.hueShift))
} }
} }
@ -225,7 +239,7 @@ data class BiomePlaceablesDefinition(
} }
@JsonFactory @JsonFactory
data class ObjectPool(val pool: ImmutableList<Pair<Double, String>> = ImmutableList.of(), val parameters: JsonElement = JsonObject()) data class ObjectPool(val pool: ImmutableList<Pair<Double, Registry.Ref<ObjectDefinition>>> = ImmutableList.of(), val parameters: JsonObject = JsonObject())
@JsonFactory @JsonFactory
data class Object(val objectSets: ImmutableList<ObjectPool>) : DistributionItemData() { data class Object(val objectSets: ImmutableList<ObjectPool>) : DistributionItemData() {
@ -284,7 +298,6 @@ data class BiomePlaceablesDefinition(
val densityOffset: Double = 2.0, val densityOffset: Double = 2.0,
val typePeriod: Double = 10.0, val typePeriod: Double = 10.0,
val noiseType: PerlinNoiseParameters.Type = PerlinNoiseParameters.Type.PERLIN, val noiseType: PerlinNoiseParameters.Type = PerlinNoiseParameters.Type.PERLIN,
val noiseScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
) : DistributionData() { ) : DistributionData() {
override val type: BiomePlacementDistributionType override val type: BiomePlacementDistributionType
get() = BiomePlacementDistributionType.PERIODIC get() = BiomePlacementDistributionType.PERIODIC
@ -303,7 +316,6 @@ data class BiomePlaceablesDefinition(
densityFunction = AbstractPerlinNoise.of( densityFunction = AbstractPerlinNoise.of(
PerlinNoiseParameters( PerlinNoiseParameters(
type = noiseType, type = noiseType,
scale = noiseScale,
octaves = octaves, octaves = octaves,
alpha = alpha, alpha = alpha,
beta = beta, beta = beta,
@ -318,7 +330,6 @@ data class BiomePlaceablesDefinition(
modulusDistortion = AbstractPerlinNoise.of( modulusDistortion = AbstractPerlinNoise.of(
PerlinNoiseParameters( PerlinNoiseParameters(
type = noiseType, type = noiseType,
scale = noiseScale,
octaves = octaves, octaves = octaves,
alpha = alpha, alpha = alpha,
beta = beta, beta = beta,
@ -336,7 +347,6 @@ data class BiomePlaceablesDefinition(
it to AbstractPerlinNoise.of( it to AbstractPerlinNoise.of(
PerlinNoiseParameters( PerlinNoiseParameters(
type = noiseType, type = noiseType,
scale = noiseScale,
octaves = octaves, octaves = octaves,
alpha = alpha, alpha = alpha,
beta = beta, beta = beta,

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@ -29,7 +29,7 @@ class BushVariant(
val ceiling: Boolean, val ceiling: Boolean,
val ephemeral: Boolean, val ephemeral: Boolean,
val tileDamageParameters: TileDamageConfig, val tileDamageParameters: TileDamageParameters,
) { ) {
@JsonFactory(asList = true) @JsonFactory(asList = true)
data class Shape(val image: String, val mods: ImmutableList<String>) data class Shape(val image: String, val mods: ImmutableList<String>)
@ -46,7 +46,7 @@ class BushVariant(
val mods: ImmutableSet<String> = ImmutableSet.of(), val mods: ImmutableSet<String> = ImmutableSet.of(),
val ceiling: Boolean = false, val ceiling: Boolean = false,
val ephemeral: Boolean = true, val ephemeral: Boolean = true,
val damageTable: AssetReference<TileDamageConfig>? = null, val damageTable: AssetReference<TileDamageParameters>? = null,
val health: Double = 1.0, val health: Double = 1.0,
) )

View File

@ -7,7 +7,7 @@ import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@ -20,7 +20,7 @@ data class GrassVariant(
val descriptions: ImmutableMap<String, String> = ImmutableMap.of(), val descriptions: ImmutableMap<String, String> = ImmutableMap.of(),
val ceiling: Boolean, val ceiling: Boolean,
val ephemeral: Boolean, val ephemeral: Boolean,
val tileDamageParameters: TileDamageConfig, val tileDamageParameters: TileDamageParameters,
) { ) {
@JsonFactory @JsonFactory
data class Data( data class Data(
@ -31,7 +31,7 @@ data class GrassVariant(
val ceiling: Boolean = false, val ceiling: Boolean = false,
val ephemeral: Boolean = true, val ephemeral: Boolean = true,
val description: String = name, val description: String = name,
val damageTable: AssetReference<TileDamageConfig>? = null, val damageTable: AssetReference<TileDamageParameters>? = null,
val health: Double = 1.0 val health: Double = 1.0
) { ) {
init { init {

View File

@ -2,14 +2,13 @@ package ru.dbotthepony.kstarbound.defs.world
import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableMap
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.ThingDescription import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat import ru.dbotthepony.kstarbound.json.builder.JsonFlat
@ -21,15 +20,15 @@ data class TreeVariant(
val stemDirectory: String, val stemDirectory: String,
// may i fucking ask you why do you embed ENTIRE FUCKING FILE in // may i fucking ask you why do you embed ENTIRE FUCKING FILE in
// this struct, Chucklefuck??????? // this struct, Chucklefuck???????
val stemSettings: JsonElement, val stemSettings: JsonObject = JsonObject(),
val stemHueShift: Double, val stemHueShift: Double,
val foliageDirectory: String, val foliageDirectory: String,
// AGAIN. // AGAIN.
val foliageSettings: JsonElement, val foliageSettings: JsonObject = JsonObject(),
val foliageHueShift: Double, val foliageHueShift: Double,
val descriptions: ImmutableMap<String, String> = ImmutableMap.of(), val descriptions: JsonObject = JsonObject(),
val ceiling: Boolean, val ceiling: Boolean,
val ephemeral: Boolean, val ephemeral: Boolean,
@ -37,7 +36,7 @@ data class TreeVariant(
val stemDropConfig: JsonElement, val stemDropConfig: JsonElement,
val foliageDropConfig: JsonElement, val foliageDropConfig: JsonElement,
val tileDamageParameters: TileDamageConfig, val tileDamageParameters: TileDamageParameters,
) { ) {
@JsonFactory @JsonFactory
data class StemData( data class StemData(
@ -55,7 +54,7 @@ data class TreeVariant(
@JsonFlat @JsonFlat
val descriptions: ThingDescription, val descriptions: ThingDescription,
val damageTable: AssetReference<TileDamageConfig>? = null, val damageTable: AssetReference<TileDamageParameters>? = null,
val health: Double = 1.0, val health: Double = 1.0,
) )
@ -75,11 +74,11 @@ data class TreeVariant(
fun create(data: Registry.Entry<StemData>, stemHueShift: Double): TreeVariant { fun create(data: Registry.Entry<StemData>, stemHueShift: Double): TreeVariant {
return TreeVariant( return TreeVariant(
stemDirectory = data.file?.computeDirectory() ?: "/", stemDirectory = data.file?.computeDirectory() ?: "/",
stemSettings = data.json.deepCopy(), stemSettings = data.json.asJsonObject.deepCopy(),
stemHueShift = stemHueShift, stemHueShift = stemHueShift,
ceiling = data.value.ceiling, ceiling = data.value.ceiling,
stemDropConfig = data.value.dropConfig.deepCopy(), stemDropConfig = data.value.dropConfig.deepCopy(),
descriptions = data.value.descriptions.fixDescription(data.key).toMap(), descriptions = data.value.descriptions.fixDescription(data.key).toJsonObject(),
ephemeral = data.value.ephemeral, ephemeral = data.value.ephemeral,
tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health), tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health),
@ -102,15 +101,15 @@ data class TreeVariant(
fun create(data: Registry.Entry<StemData>, stemHueShift: Double, fdata: Registry.Entry<FoliageData>, foliageHueShift: Double): TreeVariant { fun create(data: Registry.Entry<StemData>, stemHueShift: Double, fdata: Registry.Entry<FoliageData>, foliageHueShift: Double): TreeVariant {
return TreeVariant( return TreeVariant(
stemDirectory = data.file?.computeDirectory() ?: "/", stemDirectory = data.file?.computeDirectory() ?: "/",
stemSettings = data.json.deepCopy(), stemSettings = data.json.asJsonObject.deepCopy(),
stemHueShift = stemHueShift, stemHueShift = stemHueShift,
ceiling = data.value.ceiling, ceiling = data.value.ceiling,
stemDropConfig = data.value.dropConfig.deepCopy(), stemDropConfig = data.value.dropConfig.deepCopy(),
descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}").toMap(), descriptions = data.value.descriptions.fixDescription("${data.key} with ${fdata.key}").toJsonObject(),
ephemeral = data.value.ephemeral, ephemeral = data.value.ephemeral,
tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health), tileDamageParameters = (data.value.damageTable?.value ?: Globals.treeDamage).copy(totalHealth = data.value.health),
foliageSettings = fdata.json, foliageSettings = fdata.json.asJsonObject.deepCopy(),
foliageDropConfig = fdata.value.dropConfig.deepCopy(), foliageDropConfig = fdata.value.dropConfig.deepCopy(),
foliageName = fdata.key, foliageName = fdata.key,
foliageDirectory = fdata.file?.computeDirectory() ?: "/", foliageDirectory = fdata.file?.computeDirectory() ?: "/",

View File

@ -89,8 +89,6 @@ class WorldLayout {
WorldGeometry(worldSize, loopX, loopY) WorldGeometry(worldSize, loopX, loopY)
} }
private object StartingRegionsToken : TypeToken<ArrayList<AABBi>>()
@JsonFactory @JsonFactory
data class SerializedLayer( data class SerializedLayer(
val yStart: Int, val yStart: Int,

View File

@ -171,8 +171,6 @@ class WorldTemplate(val geometry: WorldGeometry) {
return geometry.size.y / 2 return geometry.size.y / 2
} }
fun seedFor(x: Int, y: Int) = staticRandom64(geometry.x.cell(x), geometry.y.cell(y), seed, "Block")
class PotentialBiomeItems( class PotentialBiomeItems(
// Potential items that would spawn at the given block assuming it is at // Potential items that would spawn at the given block assuming it is at
val surfaceBiomeItems: List<BiomePlaceables.Placement>, val surfaceBiomeItems: List<BiomePlaceables.Placement>,

View File

@ -1,5 +1,7 @@
package ru.dbotthepony.kstarbound.io package ru.dbotthepony.kstarbound.io
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.DelegateSyncher import ru.dbotthepony.kommons.io.DelegateSyncher
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kommons.io.readBinaryString
@ -8,8 +10,11 @@ import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readInt import ru.dbotthepony.kommons.io.readInt
import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeDouble import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeStruct2i import ru.dbotthepony.kommons.io.writeStruct2i
@ -263,3 +268,27 @@ fun DelegateSyncher.vec4d(value: Vector4d, setter: DelegateSetter<Vector4d> = De
ListenableDelegate.maskSmart(value, getter, setter), Vector4dCodec) ListenableDelegate.maskSmart(value, getter, setter), Vector4dCodec)
fun DelegateSyncher.vec4f(value: Vector4f, setter: DelegateSetter<Vector4f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4f> = DelegateGetter.passthrough()) = Slot( fun DelegateSyncher.vec4f(value: Vector4f, setter: DelegateSetter<Vector4f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4f> = DelegateGetter.passthrough()) = Slot(
ListenableDelegate.maskSmart(value, getter, setter), Vector4fCodec) ListenableDelegate.maskSmart(value, getter, setter), Vector4fCodec)
fun OutputStream.writeEnumStupid(index: Int, isLegacy: Boolean) {
if (isLegacy) writeInt(index) else write(index)
}
fun InputStream.readEnumStupid(isLegacy: Boolean): Int {
return if (isLegacy) readInt() else readUnsignedByte()
}
fun OutputStream.writeIntStupid(index: Int, isLegacy: Boolean) {
if (isLegacy) writeInt(index) else writeSignedVarInt(index)
}
fun InputStream.readIntStupid(isLegacy: Boolean): Int {
return if (isLegacy) readInt() else readSignedVarInt()
}
fun OutputStream.writeByteArray(array: ByteArrayList) {
writeByteArray(array.elements(), 0, array.size)
}
fun OutputStream.writeByteArray(array: FastByteArrayOutputStream) {
writeByteArray(array.array, 0, array.length)
}

View File

@ -4,6 +4,7 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator
interface IContainer { interface IContainer {
var size: Int var size: Int
@ -49,6 +50,17 @@ interface IContainer {
return count return count
} }
fun shuffle(random: RandomGenerator) {
for (i in 0 until size) {
val rand = random.nextInt(size)
val a = this[i]
val b = this[rand]
this[rand] = a
this[i] = b
}
}
// puts item into container, returns remaining not put items // puts item into container, returns remaining not put items
fun add(item: ItemStack, simulate: Boolean = false): ItemStack { fun add(item: ItemStack, simulate: Boolean = false): ItemStack {
val copy = item.copy() val copy = item.copy()

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.item
import com.google.common.collect.ImmutableSet import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.get
@ -18,6 +19,7 @@ import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.fromJson
import ru.dbotthepony.kstarbound.json.JsonPatch import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future import java.util.concurrent.Future
@ -44,12 +46,24 @@ object ItemRegistry {
val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null) val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null)
private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet<String>())
init { init {
entries[""] = AIR entries[""] = AIR
} }
operator fun get(name: String): Entry { operator fun get(name: String): Entry {
return entries[name] ?: AIR val entry = entries[name]
if (entry == null) {
if (loggedMisses.add(name)) {
LOGGER.warn("No such item '$name'")
}
return AIR
}
return entry
} }
@JsonFactory @JsonFactory

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readString import ru.dbotthepony.kommons.io.readString
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.io.readInternedString
import java.io.DataInputStream import java.io.DataInputStream
import java.io.EOFException import java.io.EOFException
import java.io.InputStream import java.io.InputStream
@ -30,7 +31,7 @@ fun DataInputStream.readJsonElement(): JsonElement {
BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble()) BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble())
BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean()) BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean())
BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong()) BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong())
BinaryJsonReader.TYPE_STRING -> JsonPrimitive(Starbound.STRINGS.intern(readBinaryString())) BinaryJsonReader.TYPE_STRING -> JsonPrimitive(readInternedString())
BinaryJsonReader.TYPE_ARRAY -> readJsonArray() BinaryJsonReader.TYPE_ARRAY -> readJsonArray()
BinaryJsonReader.TYPE_OBJECT -> readJsonObject() BinaryJsonReader.TYPE_OBJECT -> readJsonObject()
else -> throw JsonParseException("Unknown element type $id") else -> throw JsonParseException("Unknown element type $id")

View File

@ -87,3 +87,7 @@ annotation class JsonImplementation(val implementingClass: KClass<*>)
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class JsonSingleton annotation class JsonSingleton
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class EnumAsInt

View File

@ -9,6 +9,8 @@ import org.classdump.luna.LuaType
import org.classdump.luna.StateContext import org.classdump.luna.StateContext
import org.classdump.luna.Table import org.classdump.luna.Table
import org.classdump.luna.Variable import org.classdump.luna.Variable
import org.classdump.luna.compiler.CompilerChunkLoader
import org.classdump.luna.compiler.CompilerSettings
import org.classdump.luna.env.RuntimeEnvironments import org.classdump.luna.env.RuntimeEnvironments
import org.classdump.luna.exec.DirectCallExecutor import org.classdump.luna.exec.DirectCallExecutor
import org.classdump.luna.impl.DefaultTable import org.classdump.luna.impl.DefaultTable
@ -29,6 +31,7 @@ import ru.dbotthepony.kstarbound.defs.AssetPath
import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings import ru.dbotthepony.kstarbound.lua.bindings.provideRootBindings
import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings import ru.dbotthepony.kstarbound.lua.bindings.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.atomic.AtomicLong
class LuaEnvironment : StateContext { class LuaEnvironment : StateContext {
private var nilMeta: Table? = null private var nilMeta: Table? = null
@ -280,7 +283,7 @@ class LuaEnvironment : StateContext {
return true return true
} }
fun invokeGlobal(name: String, vararg arguments: Any?): Array<out Any?> { fun invokeGlobal(name: String, vararg arguments: Any?): Array<Any?> {
if (errorState) if (errorState)
return arrayOf() return arrayOf()
@ -299,7 +302,15 @@ class LuaEnvironment : StateContext {
return arrayOf() return arrayOf()
} }
private val loader by lazy { CompilerChunkLoader.of(CompilerSettings.defaultNoAccountingSettings(), "sb_lua${COUNTER.getAndIncrement()}_") }
// leaks memory until LuaEnvironment goes out of scope. Too bad!
fun eval(chunk: String, name: String = "eval"): Array<Any?> {
return executor.call(this, loader.compileTextChunk(chunk, name).newInstance(Variable(globals)))
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val COUNTER = AtomicLong()
} }
} }

View File

@ -0,0 +1,9 @@
package ru.dbotthepony.kstarbound.lua.bindings
import org.classdump.luna.Table
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
import ru.dbotthepony.kstarbound.server.world.ServerWorld
fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) {
}

View File

@ -586,7 +586,3 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) {
provideServerWorldBindings(self, callbacks, lua) provideServerWorldBindings(self, callbacks, lua)
} }
} }
private fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) {
}

View File

@ -1,22 +1,28 @@
package ru.dbotthepony.kstarbound.network.syncher package ru.dbotthepony.kstarbound.network.syncher
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.RandomListIterator import ru.dbotthepony.kstarbound.collect.RandomListIterator
import ru.dbotthepony.kstarbound.collect.RandomSubList import ru.dbotthepony.kstarbound.collect.RandomSubList
import ru.dbotthepony.kstarbound.io.writeByteArray
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
// original engine does not have "networked list", so it is always networked // original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol // the dumb way on legacy protocol
// "extraStupid" will wrap data in extra byte array on legacy protocol
class NetworkedList<E>( class NetworkedList<E>(
val codec: StreamCodec<E>, val codec: StreamCodec<E>,
val legacyCodec: StreamCodec<E> = codec, val legacyCodec: StreamCodec<E> = codec,
private val maxBacklogSize: Int = 100, private val maxBacklogSize: Int = 100,
private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList,
private val extraStupid: Boolean = false,
) : NetworkedElement(), MutableList<E> { ) : NetworkedElement(), MutableList<E> {
private val backlog = ArrayDeque<Pair<Long, Entry<E>>>() private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
private val elements = elementsFactory(10) private val elements = elementsFactory(10)
@ -48,6 +54,16 @@ class NetworkedList<E>(
listeners.add(listener) listeners.add(listener)
} }
/**
* re-networks element at [index]
*/
fun markDirtyAtIndex(index: Int) {
val element = this[index]
backlog.add(currentVersion() to Entry(index))
backlog.add(currentVersion() to Entry(index, element))
purgeBacklog()
}
private fun purgeBacklog() { private fun purgeBacklog() {
while (backlog.size >= maxBacklogSize) { while (backlog.size >= maxBacklogSize) {
backlog.removeFirst() backlog.removeFirst()
@ -75,17 +91,18 @@ class NetworkedList<E>(
queue.clear() queue.clear()
elements.clear() elements.clear()
val count = data.readVarInt() val stream = if (isLegacy && extraStupid) DataInputStream(FastByteArrayInputStream(data.readByteArray())) else data
val count = stream.readVarInt()
if (isLegacy) { if (isLegacy) {
for (i in 0 until count) { for (i in 0 until count) {
val read = legacyCodec.read(data) val read = legacyCodec.read(stream)
elements.add(read) elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read)) backlog.add(currentVersion() to Entry(elements.size - 1, read))
} }
} else { } else {
for (i in 0 until count) { for (i in 0 until count) {
val read = codec.read(data) val read = codec.read(stream)
elements.add(read) elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read)) backlog.add(currentVersion() to Entry(elements.size - 1, read))
} }
@ -97,12 +114,23 @@ class NetworkedList<E>(
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
val latest = latestState() val latest = latestState()
data.writeVarInt(latest.size)
if (isLegacy) { if (isLegacy && extraStupid) {
latest.forEach { legacyCodec.write(data, it) } val stream = FastByteArrayOutputStream()
val dstream = DataOutputStream(stream)
dstream.writeVarInt(latest.size)
latest.forEach { legacyCodec.write(dstream, it) }
data.writeByteArray(stream)
} else { } else {
latest.forEach { codec.write(data, it) } data.writeVarInt(latest.size)
if (isLegacy) {
latest.forEach { legacyCodec.write(data, it) }
} else {
latest.forEach { codec.write(data, it) }
}
} }
} }

View File

@ -1,16 +1,19 @@
package ru.dbotthepony.kstarbound.server.world package ru.dbotthepony.kstarbound.server.world
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.arrays.Object2DArray import ru.dbotthepony.kommons.arrays.Object2DArray
import ru.dbotthepony.kommons.gson.JsonArray
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableList import ru.dbotthepony.kommons.guava.immutableList
import ru.dbotthepony.kstarbound.math.AABBi import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
@ -33,14 +36,18 @@ import ru.dbotthepony.kstarbound.defs.world.Biome
import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables import ru.dbotthepony.kstarbound.defs.world.BiomePlaceables
import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket import ru.dbotthepony.kstarbound.network.packets.clientbound.TileDamageUpdatePacket
import ru.dbotthepony.kstarbound.util.ExecutionTimePacer
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.util.random.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.api.AbstractCell import ru.dbotthepony.kstarbound.world.api.AbstractCell
@ -50,17 +57,19 @@ import ru.dbotthepony.kstarbound.world.api.MutableTileState
import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.api.TileColor
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate import java.util.function.Predicate
import java.util.function.Supplier import java.util.function.Supplier
import java.util.random.RandomGenerator
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.math.min
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) { class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
override var state: ChunkState = ChunkState.FRESH override var state: ChunkState = ChunkState.FRESH
@ -185,10 +194,15 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await() CompletableFuture.runAsync(Runnable { finalizeCells() }, Starbound.EXECUTOR).await()
// skip if we have no layout // skip if we have no layout
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) { if (world.template.worldLayout != null) {
placeGrass() placeGrass()
} }
// skip if we have no layout, or it is a floating dungeon world
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
placeBiomeItems()
}
signalChunkContentsUpdated() signalChunkContentsUpdated()
} }
@ -717,13 +731,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF) pos.tile + Vector2i(width + CHUNK_SIZE_FF, height + CHUNK_SIZE_FF)
) )
val pacer = ExecutionTimePacer(500_000L, 40L)
val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "microdungeon placement"))
for (placement in placements) { for (placement in placements) {
if (placement.item is BiomePlaceables.MicroDungeon) { if (placement.item is BiomePlaceables.MicroDungeon) {
if (placement.item.microdungeons.isEmpty()) if (placement.item.microdungeons.isEmpty())
continue // ??? continue // ???
val seed = world.template.seedFor(placement.position.x, placement.position.y)
val random = random(seed)
val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size)) val dungeon = placement.item.microdungeons.elementAt(random.nextInt(placement.item.microdungeons.size))
if (dungeon.isEmpty) { if (dungeon.isEmpty) {
@ -763,7 +778,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
// some breathing room for other code, since placement checking is performance intense operation // some breathing room for other code, since placement checking is performance intense operation
if (!world.isInPreparation && world.clients.isNotEmpty()) if (!world.isInPreparation && world.clients.isNotEmpty())
delay(min(60L, anchor.reader.size.x * anchor.reader.size.y / 100L)) pacer.measureAndSuspend()
} }
} }
} }
@ -1087,6 +1102,43 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
} }
} }
private suspend fun placeBiomeItems() {
val placements = CompletableFuture.supplyAsync(Supplier {
val placements = ArrayList<BiomePlaceables.Placement>()
for (x in 0 until width) {
for (y in 0 until height) {
if (cells.value[x, y].dungeonId == NO_DUNGEON_ID) {
placements.addAll(world.template.validBiomeItemsAt(pos.tileX + x, pos.tileY + y))
}
}
}
placements.sortByDescending { it.priority }
val random = random(staticRandom64(world.template.seed, pos.x, pos.y, "biome placement"))
val funcs = ArrayList<() -> Unit>()
for (placement in placements) {
try {
funcs.add(placement.item.createPlacementFunc(world, random, placement.position))
} catch (err: Throwable) {
LOGGER.error("Exception while evaluating biome placeables for chunk $pos in $world", err)
}
}
funcs
}, Starbound.EXECUTOR).await()
for (placement in placements) {
try {
placement()
} catch (err: Throwable) {
LOGGER.error("Exception while placing biome placeables for chunk $pos in $world", err)
}
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()

View File

@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.objects.ObjectArraySet
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
@ -37,7 +36,7 @@ import ru.dbotthepony.kstarbound.server.StarboundServer
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.BlockableEventLoop import ru.dbotthepony.kstarbound.util.BlockableEventLoop
import ru.dbotthepony.kstarbound.util.Pacer import ru.dbotthepony.kstarbound.util.ActionPacer
import ru.dbotthepony.kstarbound.util.random.random import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState import ru.dbotthepony.kstarbound.world.ChunkState
@ -164,7 +163,7 @@ class ServerWorld private constructor(
/** /**
* this method does not block if pacer is null (safe to use with runBlocking {}) * this method does not block if pacer is null (safe to use with runBlocking {})
*/ */
suspend fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null, pacer: Pacer? = null): TileDamageResult { suspend fun damageTiles(positions: Collection<IStruct2i>, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null, pacer: ActionPacer? = null): TileDamageResult {
if (damage.amount <= 0.0) if (damage.amount <= 0.0)
return TileDamageResult.NONE return TileDamageResult.NONE
@ -235,7 +234,7 @@ class ServerWorld private constructor(
return runBlocking { applyTileModifications(modifications, allowEntityOverlap, ignoreTileProtection, null) } return runBlocking { applyTileModifications(modifications, allowEntityOverlap, ignoreTileProtection, null) }
} }
suspend fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: Pacer?): List<Pair<Vector2i, TileModification>> { suspend fun applyTileModifications(modifications: Collection<Pair<Vector2i, TileModification>>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: ActionPacer?): List<Pair<Vector2i, TileModification>> {
val unapplied = ArrayList(modifications) val unapplied = ArrayList(modifications)
var size: Int var size: Int

View File

@ -9,7 +9,6 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
@ -37,7 +36,7 @@ import ru.dbotthepony.kstarbound.network.packets.clientbound.TileModificationFai
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStartPacket
import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket import ru.dbotthepony.kstarbound.network.packets.clientbound.WorldStopPacket
import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.server.ServerConnection
import ru.dbotthepony.kstarbound.util.Pacer import ru.dbotthepony.kstarbound.util.ActionPacer
import ru.dbotthepony.kstarbound.world.ChunkPos import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
@ -76,8 +75,8 @@ class ServerWorldTracker(val world: ServerWorld, val client: ServerConnection, p
} }
private data class DamageTileEntry(val positions: Collection<Vector2i>, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: AbstractEntity? = null) private data class DamageTileEntry(val positions: Collection<Vector2i>, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: AbstractEntity? = null)
private val damageTilesQueue = Channel<DamageTileEntry>(64) // 64 pending tile damages should be enough private val damageTilesQueue = Channel<DamageTileEntry>(64) // 64 pending tile group damage requests should be more than enough
private val tileModificationBudget = Pacer.actionsPerSecond(actions = 512, handicap = 2048) // TODO: make this configurable private val tileModificationBudget = ActionPacer(actions = 512, handicap = 2048) // TODO: make this configurable
private val modifyTilesQueue = Channel<Pair<Collection<Pair<Vector2i, TileModification>>, Boolean>>(64) private val modifyTilesQueue = Channel<Pair<Collection<Pair<Vector2i, TileModification>>, Boolean>>(64)
private suspend fun damageTilesLoop() { private suspend fun damageTilesLoop() {

View File

@ -0,0 +1,26 @@
package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
/**
* Allows to perform up to certain amount of actions per given time window,
* otherwise starts throttling
*/
class ActionPacer(actions: Int, handicap: Int = 0) {
private val delayBetween = 1_000_000_000L / actions
private val maxBackwardNanos = handicap * delayBetween
private var currentTime = System.nanoTime() - maxBackwardNanos
suspend fun consume(actions: Int = 1) {
require(actions >= 1) { "Invalid amount of actions to consume: $actions" }
val time = System.nanoTime()
if (time - currentTime > maxBackwardNanos)
currentTime = time - maxBackwardNanos
currentTime += delayBetween * (actions - 1)
val diff = (currentTime - time) / 1_000_000L
currentTime += delayBetween
if (diff > 0L) delay(diff)
}
}

View File

@ -221,7 +221,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
} }
fun ensureSameThread() { fun ensureSameThread() {
check(this === currentThread()) { "Performing non-threadsafe operation outside of event loop thread" } check(this === currentThread()) { "Performing non-threadsafe operation outside of event loop thread $this" }
} }
fun isSameThread() = this === currentThread() fun isSameThread() = this === currentThread()

View File

@ -0,0 +1,14 @@
package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
class ExecutionTimePacer(private val budget: Long, private val pause: Long) {
private var origin = System.nanoTime()
suspend fun measureAndSuspend() {
if (System.nanoTime() - origin >= budget) {
delay(pause)
origin = System.nanoTime()
}
}
}

View File

@ -1,31 +0,0 @@
package ru.dbotthepony.kstarbound.util
import kotlinx.coroutines.delay
/**
* Allows to perform up to [maxForward] actions per given time window,
* otherwise pauses execution
*/
class Pacer(val maxForward: Int, val delayBetween: Long) {
private val maxForwardNanos = maxForward * delayBetween
private var currentTime = System.nanoTime() - maxForwardNanos
suspend fun consume(actions: Int = 1) {
require(actions >= 1) { "Invalid amount of actions to consume: $actions" }
val time = System.nanoTime()
if (time - currentTime > maxForwardNanos)
currentTime = time - maxForwardNanos
currentTime += delayBetween * (actions - 1)
val diff = (currentTime - time) / 1_000_000L
currentTime += delayBetween
if (diff > 0L) delay(diff)
}
companion object {
fun actionsPerSecond(actions: Int, handicap: Int = 0): Pacer {
return Pacer(handicap, 1_000_000_000L / actions)
}
}
}

View File

@ -36,16 +36,16 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double) protected data class Setup(val b0: Int, val b1: Int, val r0: Double, val r1: Double)
protected val p by lazy(LazyThreadSafetyMode.NONE) { IntArray(parameters.scale * 2 + 2) } protected val p by lazy(LazyThreadSafetyMode.NONE) { IntArray(SCALE * 2 + 2) }
protected val g1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
// flat arrays for performance // flat arrays for performance
protected val g2_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g2_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
protected val g2_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g2_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
protected val g3_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g3_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
protected val g3_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g3_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
protected val g3_2 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) } protected val g3_2 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
private var init = false private var init = false
private val initLock = Any() private val initLock = Any()
@ -99,17 +99,17 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
val random = random(seed) val random = random(seed)
for (i in 0 until parameters.scale) { for (i in 0 until SCALE) {
p[i] = i p[i] = i
g1[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g1[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g2_0[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g2_0[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g2_1[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g2_1[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g3_0[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g3_0[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g3_1[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g3_1[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g3_2[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble() g3_2[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
val l2 = sqrt(g2_0[i] * g2_0[i] + g2_1[i] * g2_1[i]) val l2 = sqrt(g2_0[i] * g2_0[i] + g2_1[i] * g2_1[i])
val l3 = sqrt(g3_0[i] * g3_0[i] + g3_1[i] * g3_1[i] + g3_2[i] * g3_2[i]) val l3 = sqrt(g3_0[i] * g3_0[i] + g3_1[i] * g3_1[i] + g3_2[i] * g3_2[i])
@ -133,23 +133,23 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
} }
} }
for (i in parameters.scale downTo 1) { for (i in SCALE downTo 1) {
val k = p[i] val k = p[i]
val j = random.nextInt(0, parameters.scale - 1) val j = random.nextInt(0, SCALE - 1)
p[i] = p[j] p[i] = p[j]
p[j] = k p[j] = k
} }
for (i in 0 until parameters.scale + 2) { for (i in 0 until SCALE + 2) {
p[parameters.scale + i] = p[i] p[SCALE + i] = p[i]
g1[parameters.scale + i] = g1[i] g1[SCALE + i] = g1[i]
g2_0[parameters.scale + i] = g2_0[i] g2_0[SCALE + i] = g2_0[i]
g2_1[parameters.scale + i] = g2_1[i] g2_1[SCALE + i] = g2_1[i]
g3_0[parameters.scale + i] = g3_0[i] g3_0[SCALE + i] = g3_0[i]
g3_1[parameters.scale + i] = g3_1[i] g3_1[SCALE + i] = g3_1[i]
g3_2[parameters.scale + i] = g3_2[i] g3_2[SCALE + i] = g3_2[i]
} }
} }
@ -163,8 +163,8 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
val iv: Int = floor(value).toInt() val iv: Int = floor(value).toInt()
val fv: Double = value - iv val fv: Double = value - iv
val b0 = iv and (parameters.scale - 1) val b0 = iv and (SCALE - 1)
val b1 = (iv + 1) and (parameters.scale - 1) val b1 = (iv + 1) and (SCALE - 1)
val r1 = fv - 1.0 val r1 = fv - 1.0
return Setup(b0, b1, fv, r1) return Setup(b0, b1, fv, r1)
@ -260,6 +260,8 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
} }
companion object : TypeAdapterFactory { companion object : TypeAdapterFactory {
const val SCALE = 512
fun of(parameters: PerlinNoiseParameters): AbstractPerlinNoise { fun of(parameters: PerlinNoiseParameters): AbstractPerlinNoise {
return when (parameters.type) { return when (parameters.type) {
PerlinNoiseParameters.Type.PERLIN -> PerlinNoise(parameters) PerlinNoiseParameters.Type.PERLIN -> PerlinNoise(parameters)

View File

@ -235,6 +235,10 @@ fun RandomGenerator.nextRange(range: IStruct2i): Int {
return if (range.component1() == range.component2()) return range.component1() else nextInt(range.component1(), range.component2()) return if (range.component1() == range.component2()) return range.component1() else nextInt(range.component1(), range.component2())
} }
fun RandomGenerator.nextRange(min: Int, max: Int): Int {
return if (min == max) return min else nextInt(min, max)
}
fun RandomGenerator.nextRange(range: IStruct2d): Double { fun RandomGenerator.nextRange(range: IStruct2d): Double {
return if (range.component1() == range.component2()) return range.component1() else nextDouble(range.component1(), range.component2()) return if (range.component1() == range.component2()) return range.component1() else nextDouble(range.component1(), range.component2())
} }

View File

@ -8,7 +8,7 @@ import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.defs.tile.TileDamageType import ru.dbotthepony.kstarbound.defs.tile.TileDamageType
import ru.dbotthepony.kstarbound.math.Interpolator import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -81,7 +81,7 @@ sealed class TileHealth() {
damageEffectTimeFactor = 0.0 damageEffectTimeFactor = 0.0
} }
fun damage(config: TileDamageConfig, source: Vector2d, damage: TileDamage) { fun damage(config: TileDamageParameters, source: Vector2d, damage: TileDamage) {
val actualDamage = config.damageDone(damage) / config.totalHealth val actualDamage = config.damageDone(damage) / config.totalHealth
damagePercent = (damagePercent + actualDamage).coerceAtMost(1.0) damagePercent = (damagePercent + actualDamage).coerceAtMost(1.0)
isHarvested = damage.harvestLevel >= config.harvestLevel isHarvested = damage.harvestLevel >= config.harvestLevel
@ -97,7 +97,7 @@ sealed class TileHealth() {
val isTicking: Boolean val isTicking: Boolean
get() = !isHealthy && !isDead get() = !isHealthy && !isDead
fun tick(config: TileDamageConfig, delta: Double): Boolean { fun tick(config: TileDamageParameters, delta: Double): Boolean {
if (isDead || isHealthy) if (isDead || isHealthy)
return false return false

View File

@ -1,8 +1,10 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
import ru.dbotthepony.kstarbound.world.physics.CollisionType import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.io.DataInputStream import java.io.DataInputStream
@ -29,6 +31,10 @@ sealed class AbstractCell {
abstract val biomeTransition: Boolean abstract val biomeTransition: Boolean
// If set, a plant or object is rooted to the tile and tile damage
// should be redirected to this position
abstract val rootSource: Vector2i?
abstract fun immutable(): ImmutableCell abstract fun immutable(): ImmutableCell
abstract fun mutable(): MutableCell abstract fun mutable(): MutableCell
@ -53,14 +59,19 @@ sealed class AbstractCell {
background.write(stream) background.write(stream)
liquid.write(stream) liquid.write(stream)
stream.write(0) // collisionMap stream.write(foreground.material.value.collisionKind.ordinal)
stream.writeShort(dungeonId) stream.writeShort(dungeonId)
stream.writeByte(blockBiome) stream.writeByte(blockBiome)
stream.writeByte(envBiome) stream.writeByte(envBiome)
stream.writeBoolean(biomeTransition) stream.writeBoolean(biomeTransition)
stream.write(0) // unknown if (rootSource == null) {
stream.writeBoolean(false)
} else {
stream.writeBoolean(true)
stream.writeStruct2i(rootSource!!)
}
} }
companion object { companion object {

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.api package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
data class ImmutableCell( data class ImmutableCell(
@ -12,6 +13,7 @@ data class ImmutableCell(
override val blockBiome: Int = 0, override val blockBiome: Int = 0,
override val envBiome: Int = 0, override val envBiome: Int = 0,
override val biomeTransition: Boolean = false, override val biomeTransition: Boolean = false,
override val rootSource: Vector2i? = null,
) : AbstractCell() { ) : AbstractCell() {
override fun immutable(): ImmutableCell { override fun immutable(): ImmutableCell {
return this return this

View File

@ -2,6 +2,7 @@ package ru.dbotthepony.kstarbound.world.api
import ru.dbotthepony.kstarbound.io.readVector2i import ru.dbotthepony.kstarbound.io.readVector2i
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import java.io.DataInputStream import java.io.DataInputStream
data class MutableCell( data class MutableCell(
@ -13,6 +14,7 @@ data class MutableCell(
override var blockBiome: Int = 0, override var blockBiome: Int = 0,
override var envBiome: Int = 0, override var envBiome: Int = 0,
override var biomeTransition: Boolean = false, override var biomeTransition: Boolean = false,
override var rootSource: Vector2i? = null,
) : AbstractCell() { ) : AbstractCell() {
fun readLegacy(stream: DataInputStream, version: Int = 419): MutableCell { fun readLegacy(stream: DataInputStream, version: Int = 419): MutableCell {
foreground.read(stream) foreground.read(stream)
@ -33,10 +35,12 @@ data class MutableCell(
if (version < 418) { if (version < 418) {
stream.skipNBytes(1) // leftover stream.skipNBytes(1) // leftover
rootSource = null
} else { } else {
// TODO: root source
if (stream.readBoolean()) { if (stream.readBoolean()) {
stream.readVector2i() rootSource = stream.readVector2i()
} else {
rootSource = null
} }
} }

View File

@ -72,8 +72,8 @@ abstract class AbstractEntity : Comparable<AbstractEntity> {
abstract val type: EntityType abstract val type: EntityType
open val isEphemeral: Boolean var isEphemeral: Boolean = false
get() = false protected set
/** /**
* If set, then the entity will be discoverable by its unique id and will be * If set, then the entity will be discoverable by its unique id and will be

View File

@ -143,6 +143,8 @@ class ContainerObject(config: Registry.Entry<ObjectDefinition>) : WorldObject(co
} }
} }
} }
items.shuffle(random)
} }
override fun randomize(random: RandomGenerator, threatLevel: Double) { override fun randomize(random: RandomGenerator, threatLevel: Double) {

View File

@ -0,0 +1,751 @@
package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.JsonArrayCollector
import ru.dbotthepony.kommons.gson.contains
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.EntityType
import ru.dbotthepony.kstarbound.defs.image.Image
import ru.dbotthepony.kstarbound.defs.tile.NO_DUNGEON_ID
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDamageParameters
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isMetaTile
import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile
import ru.dbotthepony.kstarbound.defs.tile.isNullTile
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.io.readDouble
import ru.dbotthepony.kstarbound.io.readEnumStupid
import ru.dbotthepony.kstarbound.io.readIntStupid
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeEnumStupid
import ru.dbotthepony.kstarbound.io.writeIntStupid
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.network.syncher.NetworkedList
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT
import ru.dbotthepony.kstarbound.world.TileHealth
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.*
import java.util.random.RandomGenerator
import kotlin.math.absoluteValue
class PlantEntity() : TileEntity() {
@JsonFactory
data class Piece(
val image: String,
val offset: Vector2d,
var segmentIdx: Int,
val isStructuralSegment: Boolean,
val kind: Kind,
val rotationType: Rotation = Rotation.DONT_ROTATE,
val rotationOffset: Double = 0.0,
val flip: Boolean = false,
) {
// no need to serialize
var imageSize: Vector2i = Vector2i.ZERO
var spaces: Set<Vector2i> = setOf()
var zLevel: Double = 0.0
// int32_t
enum class Kind {
NONE, STEM, FOLIAGE
}
// int32_t
enum class Rotation(override val jsonName: String) : IStringSerializable {
DONT_ROTATE("dontRotate"),
ROTATE_BRANCH("rotateBranch"),
ROTATE_LEAVES("rotateLeaves"),
ROTATE_CROWN_BRANCH("rotateCrownBranch"),
ROTATE_CROWN_LEAVES("rotateCrownLeaves")
}
// lmao this order of writing has almost zero correlation with how those
// fields are declared in struct {} itself
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(image)
stream.writeStruct2d(offset, isLegacy)
stream.writeEnumStupid(rotationType.ordinal, isLegacy)
stream.writeDouble(rotationOffset, isLegacy)
stream.writeBoolean(isStructuralSegment)
stream.writeEnumStupid(kind.ordinal, isLegacy)
stream.writeIntStupid(segmentIdx, isLegacy)
stream.writeBoolean(flip)
}
companion object {
val CODEC = nativeCodec(::read, Piece::write)
val LEGACY_CODEC = legacyCodec(::read, Piece::write)
val ADAPTER: TypeAdapter<Piece> by lazy { Starbound.gson.getAdapter(Piece::class.java) }
fun read(stream: DataInputStream, isLegacy: Boolean): Piece {
val image = stream.readInternedString()
val offset = stream.readVector2d(isLegacy)
val rotationType = Rotation.entries[stream.readEnumStupid(isLegacy)]
val rotationOffset = stream.readDouble(isLegacy)
val isStructuralSegment = stream.readBoolean()
val kind = Kind.entries[stream.readEnumStupid(isLegacy)]
val segmentIndex = stream.readIntStupid(isLegacy)
val flip = stream.readBoolean()
return Piece(
image = image,
offset = offset,
rotationType = rotationType,
rotationOffset = rotationOffset,
isStructuralSegment = isStructuralSegment,
kind = kind,
segmentIdx = segmentIndex,
flip = flip,
)
}
}
}
override fun deserialize(data: JsonObject) {
super.deserialize(data)
isCeiling = data.get("ceiling", false)
stemDropConfig = data["stemDropConfig"] as? JsonObject ?: JsonObject()
foliageDropConfig = data["foliageDropConfig"] as? JsonObject ?: JsonObject()
saplingDropConfig = data["saplingDropConfig"] as? JsonObject ?: JsonObject()
descriptions = data["descriptions"] as? JsonObject ?: JsonObject()
isEphemeral = data.get("ephemeral", false)
fallsWhenDead = data.get("fallsWhenDead", false)
tileDamageParameters = data.get("tileDamageParameters", damageParameters)
this.piecesInternal.clear()
for (v in data.get("pieces", JsonArray())) {
this.piecesInternal.add(Piece.ADAPTER.fromJsonTree((v as JsonObject).apply {
val kind = this["kind"]
// holy shiiiiieeeet
if (kind is JsonPrimitive && kind.isNumber) {
this["kind"] = Piece.Kind.entries[kind.asInt].name
}
}))
}
scanSpacesAndRoots()
}
override fun serialize(): JsonObject {
val data = super.serialize()
data["ceiling"] = isCeiling
data["stemDropConfig"] = stemDropConfig.deepCopy()
data["foliageDropConfig"] = foliageDropConfig.deepCopy()
data["saplingDropConfig"] = saplingDropConfig.deepCopy()
data["descriptions"] = descriptions.deepCopy()
data["ephemeral"] = isEphemeral
data["fallsWhenDead"] = fallsWhenDead
data["tileDamageParameters"] = damageParameters.toJsonTree(tileDamageParameters)
// holy shiiiiieeeet
data["pieces"] = piecesInternal.stream()
.map { Piece.ADAPTER.toJsonTree(it) as JsonObject }
.peek { it["kind"] = Piece.Kind.valueOf(it["kind"].asString).ordinal }
.collect(JsonArrayCollector)
return data
}
override val type: EntityType
get() = EntityType.PLANT
val health = TileHealth.TileEntity().also { networkGroup.upstream.add(it.networkGroup) }
private val piecesInternal = NetworkedList(Piece.CODEC, Piece.LEGACY_CODEC, extraStupid = true).also { networkGroup.upstream.add(it) }
private var piecesDirty = false
init {
piecesInternal.addListener(Runnable {
piecesDirty = true
})
}
val pieces: List<Piece> = Collections.unmodifiableList(piecesInternal)
var tileDamageX by networkedFloat().also { networkGroup.upstream.add(it) }
private set
var tileDamageY by networkedFloat().also { networkGroup.upstream.add(it) }
private set
val tileDamageEvent = networkedEventCounter().also { networkGroup.upstream.add(it) }
var isCeiling = false
private set
var fallsWhenDead = false
private set
var stemDropConfig: JsonObject = JsonObject()
private set
var foliageDropConfig: JsonObject = JsonObject()
private set
var saplingDropConfig: JsonObject = JsonObject()
private set
var descriptions: JsonObject = JsonObject()
private set
var tileDamageParameters = TileDamageParameters.EMPTY
private set
constructor(config: TreeVariant, random: RandomGenerator) : this() {
isCeiling = config.ceiling
stemDropConfig = (config.stemDropConfig as? JsonObject)?.deepCopy() ?: JsonObject()
foliageDropConfig = (config.foliageDropConfig as? JsonObject)?.deepCopy() ?: JsonObject()
if (stemDropConfig.isJsonNull)
stemDropConfig = JsonObject()
if (foliageDropConfig.isJsonNull)
foliageDropConfig = JsonObject()
stemDropConfig["hueshift"] = config.stemHueShift
foliageDropConfig["hueshift"] = config.foliageHueShift
val saplingDropConfig = JsonObject()
saplingDropConfig["stemName"] = config.stemName
saplingDropConfig["stemHueShift"] = config.stemHueShift
// original engine has "always true" condition because it checks against field "foliageDropConfig"
// which is coalesced to json object if config.foliageDropConfig is null
if (!config.foliageDropConfig.isJsonNull && config.foliageName.isNotBlank()) {
saplingDropConfig["foliageName"] = config.foliageName
saplingDropConfig["foliageHueShift"] = config.foliageHueShift
}
this.saplingDropConfig = saplingDropConfig
var xOffset = 0.0
var yOffset = 0.0
val roffset = random.nextDouble(0.5)
descriptions = config.descriptions.deepCopy()
isEphemeral = config.ephemeral
tileDamageParameters = config.tileDamageParameters
var segment = 0
fun leaf(key: String, leaves: JsonObject, xOff: Double = xOffset, yOff: Double = yOffset, rotationOffset: Double? = null, rotationType: Piece.Rotation = Piece.Rotation.ROTATE_LEAVES) {
if (key in leaves) {
val settings = leaves[key].asJsonObject
val attachment = settings.get("attachment", JsonObject())
val xOf = xOff + attachment.get("bx", 0.0) / PIXELS_IN_STARBOUND_UNIT
val yOf = yOff + attachment.get("by", 0.0) / PIXELS_IN_STARBOUND_UNIT
if ("image" in settings && settings["image"].asString.isNotBlank()) {
val file = AssetPathStack.relativeTo(config.foliageDirectory, settings["image"].asString)
piecesInternal.add(Piece(
image = "$file?hueshift=${config.foliageHueShift.toInt()}",
offset = Vector2d(xOf, yOf),
segmentIdx = segment,
isStructuralSegment = false,
kind = Piece.Kind.FOLIAGE,
rotationType = if (isCeiling) Piece.Rotation.DONT_ROTATE else rotationType,
rotationOffset = rotationOffset ?: (random.nextDouble() + roffset)
).apply { zLevel = 3.0 })
}
if ("backimage" in settings && settings["backimage"].asString.isNotBlank()) {
val file = AssetPathStack.relativeTo(config.foliageDirectory, settings["backimage"].asString)
piecesInternal.add(Piece(
image = "$file?hueshift=${config.foliageHueShift.toInt()}",
offset = Vector2d(xOf, yOf),
segmentIdx = segment,
isStructuralSegment = false,
kind = Piece.Kind.FOLIAGE,
rotationType = if (isCeiling) Piece.Rotation.DONT_ROTATE else rotationType,
rotationOffset = rotationOffset ?: (random.nextDouble() + roffset)
).apply { zLevel = -1.0 })
}
}
}
// base
run {
val bases = config.stemSettings.asJsonObject["base"].asJsonObject
val baseKey = bases.keySet().random(random)
val baseSettings = bases[baseKey].asJsonObject
val attachmentSettings = baseSettings.get("attachment", JsonObject())
xOffset += attachmentSettings.get("bx", 0.0) / PIXELS_IN_STARBOUND_UNIT
yOffset += attachmentSettings.get("by", 0.0) / PIXELS_IN_STARBOUND_UNIT
val baseFile = AssetPathStack.relativeTo(config.stemDirectory, baseSettings["image"].asString)
if (isCeiling) {
val img = Image.get(baseFile)
if (img == null) {
LOGGER.error("Unable to load Tree's base stem image $baseFile, expect bad things to happen!")
return
}
yOffset = 1.0 - img.size.y / PIXELS_IN_STARBOUND_UNIT
}
piecesInternal.add(Piece(
image = "$baseFile?hueshift=${config.stemHueShift.toInt()}",
offset = Vector2d(xOffset, yOffset),
segmentIdx = segment,
isStructuralSegment = true,
kind = Piece.Kind.STEM,
rotationType = Piece.Rotation.DONT_ROTATE,
rotationOffset = random.nextDouble() + roffset
))
// base leaves
leaf(baseKey, config.foliageSettings.get("baseLeaves", JsonObject()))
xOffset += attachmentSettings.get("x", 0.0) / PIXELS_IN_STARBOUND_UNIT
yOffset += attachmentSettings.get("y", 0.0) / PIXELS_IN_STARBOUND_UNIT // trunk height
segment++
}
var branchYOffset = yOffset
// trunk
run {
val middles = config.stemSettings.get("middle").asJsonObject
val middleHeight = random.nextRange(config.stemSettings.get("middleMinSize", 1), config.stemSettings.get("middleMaxSize", 6))
val branches = config.stemSettings["branch"]?.asJsonObject ?: JsonObject()
for (i in 0 until middleHeight) {
val middleKey = middles.keySet().random(random)
val middleSettings = middles[middleKey].asJsonObject
val attachmentSettings = middleSettings.get("attachment", JsonObject())
xOffset += attachmentSettings.get("bx", 0.0) / PIXELS_IN_STARBOUND_UNIT
yOffset += attachmentSettings.get("by", 0.0) / PIXELS_IN_STARBOUND_UNIT
val middleFile = AssetPathStack.relativeTo(config.stemDirectory, middleSettings["image"].asString)
piecesInternal.add(Piece(
image = "$middleFile?hueshift=${config.stemHueShift.toInt()}",
offset = Vector2d(xOffset, yOffset),
segmentIdx = segment,
isStructuralSegment = true,
kind = Piece.Kind.STEM,
rotationType = Piece.Rotation.DONT_ROTATE,
rotationOffset = random.nextDouble() + roffset
).apply { zLevel = 1.0 })
// trunk leaves
leaf(middleKey, config.foliageSettings.get("trunkLeaves", JsonObject()))
xOffset += attachmentSettings.get("x", 0.0) / PIXELS_IN_STARBOUND_UNIT
yOffset += attachmentSettings.get("y", 0.0) / PIXELS_IN_STARBOUND_UNIT
// branch
while (branches.size() != 0 && yOffset >= branchYOffset && middleHeight - i > 0) {
val branchKey = branches.keySet().random(random)
val branchSettings = branches[branchKey].asJsonObject
val attachmentSettings = branchSettings.get("attachment", JsonObject())
val h = attachmentSettings.get("h", 0.0) / PIXELS_IN_STARBOUND_UNIT
if (yOffset < branchYOffset + h / 2.0)
break
val xO = xOffset + attachmentSettings.get("bx", 0.0) / PIXELS_IN_STARBOUND_UNIT
val yO = branchYOffset + attachmentSettings.get("by", 0.0) / PIXELS_IN_STARBOUND_UNIT
if (config.stemSettings.get("alwaysBranch", false) || random.nextInt(2 + i) != 0) {
val boffset = random.nextDouble() + roffset
val branchFile = AssetPathStack.relativeTo(config.stemDirectory, branchSettings["image"].asString)
piecesInternal.add(Piece(
image = "$branchFile?hueshift=${config.stemHueShift.toInt()}",
offset = Vector2d(xO, yO),
segmentIdx = segment,
isStructuralSegment = false,
kind = Piece.Kind.STEM,
rotationType = if (isCeiling) Piece.Rotation.DONT_ROTATE else Piece.Rotation.ROTATE_BRANCH,
rotationOffset = boffset
))
branchYOffset += h
// branch leaves
leaf(branchKey, config.foliageSettings.get("branchLeaves", JsonObject()), xO, yO, boffset)
} else {
branchYOffset += h / random.nextDouble(1.0, 4.0)
}
}
segment++
}
}
// crown
run {
val crowns = config.stemSettings.get("crown", JsonObject())
if (crowns.size() != 0) {
val crownKey = crowns.keySet().random(random)
val crownSettings = crowns[crownKey].asJsonObject
val attachmentSettings = crownSettings.get("attachment", JsonObject())
xOffset += attachmentSettings.get("bx", 0.0) / PIXELS_IN_STARBOUND_UNIT
yOffset += attachmentSettings.get("by", 0.0) / PIXELS_IN_STARBOUND_UNIT
val coffset = random.nextDouble() + roffset
val crownFile = AssetPathStack.relativeTo(config.stemDirectory, crownSettings["image"].asString)
piecesInternal.add(Piece(
image = "$crownFile?hueshift=${config.stemHueShift.toInt()}",
offset = Vector2d(xOffset, yOffset),
segmentIdx = segment,
isStructuralSegment = false,
kind = Piece.Kind.STEM,
rotationType = if (isCeiling) Piece.Rotation.DONT_ROTATE else Piece.Rotation.ROTATE_CROWN_BRANCH,
rotationOffset = coffset
))
// crown leaves
leaf(crownKey, config.foliageSettings.get("crownLeaves", JsonObject()), rotationOffset = coffset, rotationType = Piece.Rotation.ROTATE_CROWN_LEAVES)
}
}
piecesInternal.sortBy { it.zLevel }
scanSpacesAndRoots()
}
constructor(config: BushVariant, random: RandomGenerator) : this() {
val shape = config.shapes.random(random)
val shapeImageName = AssetPathStack.relativeTo(config.directory, shape.image)
var offset = Vector2d.ZERO
isCeiling = config.ceiling
if (isCeiling) {
val img = Image.get(shapeImageName)
if (img == null) {
LOGGER.error("Unable to load Bush variant's image $shapeImageName, expect bad things to happen!")
return
}
offset = Vector2d(y = 1.0 - img.size.y / PIXELS_IN_STARBOUND_UNIT)
}
piecesInternal.add(Piece(
image = "$shapeImageName?hueshift=${config.baseHueShift.toInt()}",
offset = offset,
segmentIdx = 0,
isStructuralSegment = true,
kind = Piece.Kind.NONE
))
if (shape.mods.isNotEmpty()) {
val mod = shape.mods.random(random)
piecesInternal.add(Piece(
image = "${AssetPathStack.relativeTo(config.directory, mod)}?hueshift=${config.modHueShift.toInt()}",
offset = offset,
segmentIdx = 0,
isStructuralSegment = false,
kind = Piece.Kind.NONE
))
}
scanSpacesAndRoots()
}
constructor(config: GrassVariant, random: RandomGenerator) : this() {
var offset = Vector2d.ZERO
val image = AssetPathStack.relativeTo(config.directory, config.images.random(random))
isCeiling = config.ceiling
if (isCeiling) {
// If this is a ceiling plant, offset the image so that the [0, 0] space is at the top
val img = Image.get(image)
if (img == null) {
LOGGER.error("Unable to load Grass variant's image $image, expect bad things to happen!")
return
}
offset = Vector2d(y = 1.0 - img.size.y / PIXELS_IN_STARBOUND_UNIT)
}
val piece = Piece(
image = "$image?hueshift=${config.hueShift.toInt()}",
offset = offset,
segmentIdx = 0,
isStructuralSegment = true,
kind = Piece.Kind.NONE,
)
piecesInternal.add(piece)
scanSpacesAndRoots()
}
constructor(stream: DataInputStream, isLegacy: Boolean) : this() {
xTilePosition = stream.readSignedVarInt()
yTilePosition = stream.readSignedVarInt()
isCeiling = stream.readBoolean()
stemDropConfig = stream.readJsonElement() as JsonObject
foliageDropConfig = stream.readJsonElement() as JsonObject
saplingDropConfig = stream.readJsonElement() as JsonObject
descriptions = stream.readJsonElement() as JsonObject
isEphemeral = stream.readBoolean()
tileDamageParameters = TileDamageParameters(stream, isLegacy)
fallsWhenDead = stream.readBoolean()
health.read(stream, isLegacy)
val readPieces = if (isLegacy) {
DataInputStream(FastByteArrayInputStream(stream.readByteArray()))
} else {
stream
}
piecesInternal.readInitial(stream, isLegacy)
scanSpacesAndRoots()
}
constructor(data: JsonObject) : this() {
deserialize(data)
}
override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeSignedVarInt(xTilePosition)
stream.writeSignedVarInt(yTilePosition)
stream.writeBoolean(isCeiling)
stream.writeJsonElement(stemDropConfig)
stream.writeJsonElement(foliageDropConfig)
stream.writeJsonElement(saplingDropConfig)
stream.writeJsonElement(descriptions)
stream.writeBoolean(isEphemeral)
tileDamageParameters.write(stream, isLegacy)
stream.writeBoolean(fallsWhenDead)
health.write(stream, isLegacy)
piecesInternal.writeInitial(stream, isLegacy)
}
private var calculatedBoundingBox = AABB.ZERO
private var calculatedOccupySpaces: Set<Vector2i> = setOf()
private var calculatedRoots: Set<Vector2i> = setOf()
override var metaBoundingBox: AABB = calculatedBoundingBox
private set
override var occupySpaces: ImmutableSet<Vector2i> = ImmutableSet.of()
private set
override val materialSpaces: Collection<Pair<Vector2i, Registry.Ref<TileDefinition>>>
get() = setOf()
override var roots: ImmutableSet<Vector2i> = ImmutableSet.of()
private set
val primaryRoot: Vector2i
get() = if (isCeiling) Vector2i(xTilePosition, yTilePosition + 1) else Vector2i(xTilePosition, yTilePosition - 1)
private fun scanSpacesAndRoots() {
if (!piecesDirty) return
piecesDirty = false
val spaces = ObjectArraySet<Vector2i>()
spaces.add(Vector2i.ZERO)
for (piece in piecesInternal) {
val image = Image.get(piece.image)
if (image == null) {
LOGGER.error("Unable to load image ${piece.image} for $this, expect bad things to happen!")
continue
}
piece.imageSize = image.size
piece.spaces = image.worldSpaces(piece.offset * PIXELS_IN_STARBOUND_UNIT, 0.1, piece.flip)
spaces.addAll(piece.spaces)
}
this.calculatedOccupySpaces = spaces
val minX = spaces.minOf { it.x }
val maxX = spaces.maxOf { it.x }
val minY = spaces.minOf { it.y }
val maxY = spaces.maxOf { it.y }
this.calculatedBoundingBox = AABB(
Vector2d(minX - 1.0, minY - 1.0),
Vector2d(maxX + 2.0, maxY + 2.0),
)
val roots = ObjectArraySet<Vector2i>()
for (space in spaces) {
if (space.y == 0) {
if (isCeiling) {
roots.add(Vector2i(space.x, 1))
} else {
roots.add(Vector2i(space.x, -1))
}
}
}
this.calculatedRoots = roots
moveSpaces()
}
override fun tick(delta: Double) {
super.tick(delta)
if (world.isServer && piecesInternal.isEmpty()) {
remove(RemovalReason.REMOVED)
}
}
private fun moveSpaces() {
scanSpacesAndRoots()
this.metaBoundingBox = this.calculatedBoundingBox + this.position
this.occupySpaces = this.calculatedOccupySpaces.stream().map { it + tilePosition }.collect(ImmutableSet.toImmutableSet())
this.roots = this.calculatedRoots.stream().map { it + tilePosition }.collect(ImmutableSet.toImmutableSet())
markSpacesDirty()
}
override fun onPositionUpdated() {
super.onPositionUpdated()
moveSpaces()
}
override fun damage(damageSpaces: List<Vector2i>, source: Vector2d, damage: TileDamage): Boolean {
// TODO
return false
}
fun plant(world: ServerWorld, position: Vector2i, ignoreDungeonID: Int = NO_DUNGEON_ID): Boolean {
if (isInWorld)
throw IllegalStateException("Already in world")
world.eventLoop.ensureSameThread()
tilePosition = position
val primaryCell = world.getCell(position)
val adjustBackground = primaryCell.background.material.isEmptyTile
// Bail out if we don't have at least one free space, and root in the primary
// root position, or if we're in a dungeon region.
val rootCell = world.getCell(primaryRoot).mutable()
if (
primaryCell.dungeonId != ignoreDungeonID ||
rootCell.dungeonId != ignoreDungeonID ||
primaryCell.foreground.material.value.isConnectable ||
!rootCell.foreground.material.value.isConnectable
) return false
// First bail out if we can't fit anything we're not adjusting
for (space in occupySpaces) {
// TODO: conditions seems to be inverted
if (withinAdjustments(space, position) && world.entityIndex.tileEntitiesAt(space).any { it is PlantEntity }) {
return false
}
// Bail out if we hit a different plant's root tile, or if we're not in the
// adjustment space and we hit a non-empty tile.
val cell = world.getCell(space)
if (cell.rootSource != null || (!withinAdjustments(space, position) && cell.foreground.material.isNotEmptyTile)) {
return false
}
}
// Check all the roots outside of the adjustment limit
for (root in roots) {
if (!withinAdjustments(root, position) && !world.getCell(root).foreground.material.value.isConnectable) {
return false
}
}
// Clear all the necessary blocks within the adjustment limit
for (space in occupySpaces) {
if (!withinAdjustments(space, position))
continue
var cell = world.getCell(space).mutable()
if (cell.foreground.material.value.isConnectable) {
cell = primaryCell.mutable()
}
if (adjustBackground) {
cell.background.empty()
}
world.setCell(space, cell)
}
// Make all the root blocks a real material based on the primary root.
for (root in roots) {
val cell = world.getCell(root)
if (cell.foreground.material.isMetaTile) {
// what the hell original engine does here?
world.setCell(root, rootCell.copy(rootSource = tilePosition))
}
}
joinWorld(world)
return true
}
companion object {
private fun withinAdjustments(root: Vector2i, position: Vector2i): Boolean {
return (root.x - position.x).absoluteValue <= PLANT_ADJUSTMENT_LIMIT && (root.y - position.y).absoluteValue <= PLANT_ADJUSTMENT_LIMIT
}
const val PLANT_ADJUSTMENT_LIMIT = 2
private val LOGGER = LogManager.getLogger()
private val damageParameters by lazy { Starbound.gson.getAdapter(TileDamageParameters::class.java) }
}
}

View File

@ -1,12 +1,16 @@
package ru.dbotthepony.kstarbound.world.entities.tile package ru.dbotthepony.kstarbound.world.entities.tile
import com.google.gson.JsonObject
import kotlinx.coroutines.future.await import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.get
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage import ru.dbotthepony.kstarbound.defs.tile.TileDamage
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
@ -23,6 +27,16 @@ import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
* (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid * (Hopefully) Static world entities (Plants, Objects, etc), which reside on cell grid
*/ */
abstract class TileEntity : AbstractEntity() { abstract class TileEntity : AbstractEntity() {
open fun deserialize(data: JsonObject) {
tilePosition = data.get("tilePosition", vectors)
}
open fun serialize(): JsonObject {
val into = JsonObject()
into["tilePosition"] = vectors.toJsonTree(tilePosition)
return into
}
protected val xTilePositionNet = networkedSignedInt() protected val xTilePositionNet = networkedSignedInt()
protected val yTilePositionNet = networkedSignedInt() protected val yTilePositionNet = networkedSignedInt()
@ -228,5 +242,6 @@ abstract class TileEntity : AbstractEntity() {
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val vectors by lazy { Starbound.gson.getAdapter(Vector2i::class.java) }
} }
} }

View File

@ -81,23 +81,25 @@ import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.TileHealth
import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import java.io.DataOutputStream import java.io.DataOutputStream
import java.util.Collections import java.util.Collections
import java.util.HashMap import java.util.HashMap
import java.util.random.RandomGenerator import java.util.random.RandomGenerator
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() { open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity {
open fun deserialize(data: JsonObject) { override fun deserialize(data: JsonObject) {
super.deserialize(data)
direction = data.get("direction", directions) { Direction.LEFT } direction = data.get("direction", directions) { Direction.LEFT }
orientationIndex = data.get("orientationIndex", -1).toLong() orientationIndex = data.get("orientationIndex", -1).toLong()
isInteractive = data.get("interactive", false) isInteractive = data.get("interactive", false)
tilePosition = data.get("tilePosition", vectors)
lua.globals["storage"] = lua.from(data.get("scriptStorage") { JsonObject() }) lua.globals["storage"] = lua.from(data.get("scriptStorage") { JsonObject() })
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
loadParameters(data.get("parameters") { JsonObject() }) loadParameters(data.get("parameters") { JsonObject() })
if ("uniqueId" in data)
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
} }
open fun loadParameters(parameters: JsonObject) { open fun loadParameters(parameters: JsonObject) {
@ -110,10 +112,9 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
} }
} }
open fun serialize(): JsonObject { override fun serialize(): JsonObject {
val into = JsonObject() val into = super.serialize()
into["name"] = config.key into["name"] = config.key
into["tilePosition"] = vectors.toJsonTree(tilePosition)
into["direction"] = directions.toJsonTree(direction) into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex into["orientationIndex"] = orientationIndex
into["interactive"] = isInteractive into["interactive"] = isInteractive
@ -619,6 +620,14 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
return tileHealth.isDead return tileHealth.isDead
} }
override fun callScript(fnName: String, vararg arguments: Any?): Array<Any?> {
return lua.invokeGlobal(fnName, *arguments)
}
override fun evalScript(code: String): Array<Any?> {
return lua.eval(code)
}
companion object { companion object {
private val lightColorPath = JsonPath("lightColor") private val lightColorPath = JsonPath("lightColor")
private val lightColorsPath = JsonPath("lightColors") private val lightColorsPath = JsonPath("lightColors")

View File

@ -13,7 +13,6 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
@JsonFactory @JsonFactory
data class Data( data class Data(
val xType: PerlinNoiseParameters.Type, val xType: PerlinNoiseParameters.Type,
val xScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
val xOctaves: Int, val xOctaves: Int,
val xFreq: Double, val xFreq: Double,
val xAmp: Double, val xAmp: Double,
@ -22,7 +21,6 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
val xBeta: Double = 2.0, val xBeta: Double = 2.0,
val yType: PerlinNoiseParameters.Type, val yType: PerlinNoiseParameters.Type,
val yScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
val yOctaves: Int, val yOctaves: Int,
val yFreq: Double, val yFreq: Double,
val yAmp: Double, val yAmp: Double,
@ -56,7 +54,6 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
xFn = AbstractPerlinNoise.of(PerlinNoiseParameters( xFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
type = data.xType, type = data.xType,
scale = data.xScale,
octaves = data.xOctaves, octaves = data.xOctaves,
frequency = data.xFreq, frequency = data.xFreq,
amplitude = data.xAmp, amplitude = data.xAmp,
@ -67,7 +64,6 @@ class DisplacementTerrainSelector(data: Data, parameters: TerrainSelectorParamet
yFn = AbstractPerlinNoise.of(PerlinNoiseParameters( yFn = AbstractPerlinNoise.of(PerlinNoiseParameters(
type = data.yType, type = data.yType,
scale = data.yScale,
octaves = data.yOctaves, octaves = data.yOctaves,
frequency = data.yFreq, frequency = data.yFreq,
amplitude = data.yAmp, amplitude = data.yAmp,