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
* Where applicable, Perlin noise now can have custom seed specified
* Change above allows to explicitly specify universe seed (as `celestial.config:systemTypePerlin:seed`)
* Perlin noise now can be of arbitrary scale (defaults to `512`, specified with `scale` key, integer type, 2048>=x>=16)
* `treasurechests` now can specify `treasurePool` as array
#### 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
@ -143,6 +143,7 @@ val color: TileColor = TileColor.DEFAULT
### Worldgen
* Major dungeon placement on planets is now deterministic
* 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
* 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.item.ItemDropConfig
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.AsteroidWorldsConfig
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.util.AssetPathStack
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ForkJoinTask
import java.util.concurrent.Future
import kotlin.properties.Delegates
import kotlin.reflect.KMutableProperty0
@ -62,16 +61,16 @@ object Globals {
var dungeonWorlds by Delegates.notNull<ImmutableMap<String, DungeonWorldsConfig>>()
private set
var grassDamage by Delegates.notNull<TileDamageConfig>()
var grassDamage by Delegates.notNull<TileDamageParameters>()
private set
var treeDamage by Delegates.notNull<TileDamageConfig>()
var treeDamage by Delegates.notNull<TileDamageParameters>()
private set
var bushDamage by Delegates.notNull<TileDamageConfig>()
var bushDamage by Delegates.notNull<TileDamageParameters>()
private set
var tileDamage by Delegates.notNull<TileDamageConfig>()
var tileDamage by Delegates.notNull<TileDamageParameters>()
private set
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.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
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.animation.ParticleConfig
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.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
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.TileDefinition
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 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 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 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()) }
@ -177,6 +177,7 @@ object Registries {
tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: 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 })
return tasks
@ -271,7 +272,7 @@ object Registries {
renderParameters = RenderParameters.META,
isConnectable = def.isConnectable,
supportsMods = def.supportsMods,
damageTable = AssetReference(TileDamageConfig(
damageTable = AssetReference(TileDamageParameters(
damageFactors = ImmutableMap.of(),
damageRecovery = Double.MAX_VALUE,
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 {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
val subtype = type.type as? ParameterizedType ?: return null
if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != clazz.java) return null
if (type.rawType == Registry.Entry::class.java || type.rawType == Registry.Ref::class.java) {
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) {
return EntryImpl(gson) as TypeAdapter<T>
} else if (type.rawType == Registry.Ref::class.java) {
return RefImpl(gson) as TypeAdapter<T>
if (type.rawType == Registry.Entry::class.java) {
return EntryImpl(gson) as TypeAdapter<T>
} else if (type.rawType == Registry.Ref::class.java) {
return RefImpl(gson) as TypeAdapter<T>
}
}
return null

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableMap
import com.github.benmanes.caffeine.cache.Interner
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
@ -10,6 +11,7 @@ import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.guava.immutableMap
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 {
return copy(
shortdescription = if (shortdescription == "...") newDescription else shortdescription,

View File

@ -8,7 +8,6 @@ import ru.dbotthepony.kstarbound.json.builder.JsonFactory
data class PerlinNoiseParameters(
val type: Type = Type.PERLIN,
val seed: Long? = null,
val scale: Int = DEFAULT_SCALE,
val octaves: Int = 1,
val gain: Double = 2.0,
val offset: Double = 1.0,
@ -18,11 +17,6 @@ data class PerlinNoiseParameters(
val amplitude: Double = 1.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 {
UNITIALIZED("uninitialized"),
PERLIN("perlin"),
@ -35,8 +29,4 @@ data class PerlinNoiseParameters(
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 it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kstarbound.math.AABBi
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
@ -35,6 +37,7 @@ import java.util.Collections
import java.util.LinkedHashSet
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer
import java.util.function.Supplier
import java.util.random.RandomGenerator
// 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 }
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)
// if any of cell change operation fails, entire generation fails... leaving world in inconsistent state,
// 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
for (wiring in localWires) {
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.CacheLoader
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
import com.github.benmanes.caffeine.cache.Scheduler
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonArray
import com.google.gson.JsonNull
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.getObject
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import java.io.BufferedInputStream
import java.io.FileNotFoundException
import java.lang.ref.Reference
@ -104,26 +107,8 @@ class Image private constructor(
}
}
val data: CompletableFuture<ByteBuffer> get() {
var get = dataRef?.get()
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 data: ByteBuffer
get() = dataCache.get(source)
val texture: GLTexture2D get() {
//val get = _texture.get()?.get()
@ -140,12 +125,10 @@ class Image private constructor(
client.named2DTextures1.get(this) {
val tex = GLTexture2D(width, height, GL45.GL_RGBA8)
data.thenApplyAsync({
tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, it)
tex.upload(GL45.GL_RGBA, GL45.GL_UNSIGNED_BYTE, data)
tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST
}, client)
tex.textureMinFilter = GL45.GL_NEAREST
tex.textureMagFilter = GL45.GL_NEAREST
tex
}
@ -187,7 +170,12 @@ class Image private constructor(
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 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 {
// 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" }
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
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> {
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 minY = pixelOffset.y / 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])
}
private val dataCache: AsyncLoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
private val dataCache: LoadingCache<IStarboundFile, ByteBuffer> = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(1))
.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 МиБ */))
.scheduler(Starbound)
.executor(Starbound.EXECUTOR) // SCREENED_EXECUTOR shouldn't be used here
.buildAsync(CacheLoader {
readImageDirect(it).data
})
.build { readImageDirect(it).data }
private val spaceScanCache = Caffeine.newBuilder()
.expireAfterAccess(Duration.ofMinutes(30))
.softValues()
.scheduler(Starbound)
.executor(Starbound.SCREENED_EXECUTOR)
.build<SpaceScanKey, ImmutableSet<Vector2i>>()
@JvmStatic
fun get(path: String): Image? {
return imageCache.computeIfAbsent(path) {
return imageCache.computeIfAbsent(path.substringBefore(':').substringBefore('?')) {
try {
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.actor.StatModifier
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.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.json.listAdapter
@ -81,7 +81,7 @@ data class ObjectDefinition(
val biomePlaced: Boolean = false,
val printable: Boolean = false,
val smashOnBreak: Boolean = false,
val damageConfig: TileDamageConfig,
val damageConfig: TileDamageParameters,
val flickerPeriod: PeriodicFunction? = null,
val orientations: ImmutableList<ObjectOrientation>,
) {
@ -153,7 +153,7 @@ data class ObjectDefinition(
private val objectRef = gson.getAdapter(JsonReference.Object::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 orientations = gson.getAdapter(ObjectOrientation::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.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.util.KOptional
import ru.dbotthepony.kstarbound.Registries
@ -131,7 +129,7 @@ object BuiltinMetaMaterials {
isMeta = true,
supportsMods = false,
collisionKind = collisionType,
damageTable = AssetReference(TileDamageConfig(
damageTable = AssetReference(TileDamageParameters(
damageFactors = ImmutableMap.of(),
damageRecovery = Double.MAX_VALUE,
maximumEffectTime = 0.0,

View File

@ -3,16 +3,32 @@ package ru.dbotthepony.kstarbound.defs.tile
import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
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 java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class TileDamageConfig(
data class TileDamageParameters(
val damageFactors: ImmutableMap<String, Double> = ImmutableMap.of(),
val damageRecovery: Double = 1.0,
val harvestLevel: Int = 1,
val maximumEffectTime: Double = 1.5,
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 {
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
}
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 maximumEffectTime = maximumEffectTime.coerceAtLeast(other.maximumEffectTime)
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 {
val EMPTY = TileDamageConfig()
val EMPTY = TileDamageParameters()
private val LOGGER = LogManager.getLogger()
}

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
@ -12,8 +13,10 @@ import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.set
import ru.dbotthepony.kommons.gson.stream
import ru.dbotthepony.kommons.gson.value
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.defs.PerlinNoiseParameters
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.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
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.server.world.ServerChunk
import ru.dbotthepony.kstarbound.server.world.ServerWorld
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.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
@JsonFactory
@ -73,6 +87,8 @@ data class BiomePlaceables(
abstract fun toJson(): JsonElement
abstract fun createPlacementFunc(world: ServerWorld, random: RandomGenerator, position: Vector2i): () -> Unit
companion object : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Item::class.java.isAssignableFrom(type.rawType)) {
@ -80,7 +96,6 @@ data class BiomePlaceables(
private val grassVariant = gson.getAdapter(GrassVariant::class.java)
private val bushVariant = gson.getAdapter(BushVariant::class.java)
private val trees = gson.listAdapter<TreeVariant>()
private val objects = gson.getAdapter(PoolTypeToken)
override fun write(out: JsonWriter, value: Item?) {
if (value == null)
@ -114,12 +129,12 @@ data class BiomePlaceables(
// and world storage data at Chucklefish.
// Truly our hero here.
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()))
"grass" -> Grass(grassVariant.read(`in`))
"bush" -> Bush(bushVariant.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")
}
@ -143,14 +158,63 @@ data class BiomePlaceables(
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
get() = BiomePlacementItemType.TREASURE_BOX_SET
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 {
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() {
@ -170,6 +239,11 @@ data class BiomePlaceables(
override fun toJson(): JsonElement {
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() {
@ -182,19 +256,48 @@ data class BiomePlaceables(
TreeVariant::class.java
).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
// each object (lmao, whos gonna write world json by hand anyway????
// considering this is world generation data.)
data class Object(val pool: WeightedList<Pair<String, JsonElement>>) : Item() {
data class Object(val pool: WeightedList<Pair<Registry.Ref<ObjectDefinition>, JsonObject>>) : Item() {
override val type: BiomePlacementItemType
get() = BiomePlacementItemType.OBJECT
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
}
}
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.JsonObject
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
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.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
@ -73,12 +76,23 @@ data class BiomePlaceablesDefinition(
}
@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
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 {
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)")
}
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
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
data class Object(val objectSets: ImmutableList<ObjectPool>) : DistributionItemData() {
@ -284,7 +298,6 @@ data class BiomePlaceablesDefinition(
val densityOffset: Double = 2.0,
val typePeriod: Double = 10.0,
val noiseType: PerlinNoiseParameters.Type = PerlinNoiseParameters.Type.PERLIN,
val noiseScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
) : DistributionData() {
override val type: BiomePlacementDistributionType
get() = BiomePlacementDistributionType.PERIODIC
@ -303,7 +316,6 @@ data class BiomePlaceablesDefinition(
densityFunction = AbstractPerlinNoise.of(
PerlinNoiseParameters(
type = noiseType,
scale = noiseScale,
octaves = octaves,
alpha = alpha,
beta = beta,
@ -318,7 +330,6 @@ data class BiomePlaceablesDefinition(
modulusDistortion = AbstractPerlinNoise.of(
PerlinNoiseParameters(
type = noiseType,
scale = noiseScale,
octaves = octaves,
alpha = alpha,
beta = beta,
@ -336,7 +347,6 @@ data class BiomePlaceablesDefinition(
it to AbstractPerlinNoise.of(
PerlinNoiseParameters(
type = noiseType,
scale = noiseScale,
octaves = octaves,
alpha = alpha,
beta = beta,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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.StreamCodec
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.readSignedVarInt
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeDouble
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.writeStruct2f
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)
fun DelegateSyncher.vec4f(value: Vector4f, setter: DelegateSetter<Vector4f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4f> = DelegateGetter.passthrough()) = Slot(
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 ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import java.util.random.RandomGenerator
interface IContainer {
var size: Int
@ -49,6 +50,17 @@ interface IContainer {
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
fun add(item: ItemStack, simulate: Boolean = false): ItemStack {
val copy = item.copy()

View File

@ -3,6 +3,7 @@ package ru.dbotthepony.kstarbound.item
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.gson.contains
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.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import java.util.Collections
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Future
@ -44,12 +46,24 @@ object ItemRegistry {
val AIR = Entry("", ItemType.GENERIC, JsonObject(), true, "air", ImmutableSet.of(), ImmutableSet.of(), null)
private val loggedMisses = Collections.synchronizedSet(ObjectOpenHashSet<String>())
init {
entries[""] = AIR
}
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

View File

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

View File

@ -87,3 +87,7 @@ annotation class JsonImplementation(val implementingClass: KClass<*>)
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
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.Table
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.exec.DirectCallExecutor
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.provideUtilityBindings
import ru.dbotthepony.kstarbound.util.random.random
import java.util.concurrent.atomic.AtomicLong
class LuaEnvironment : StateContext {
private var nilMeta: Table? = null
@ -280,7 +283,7 @@ class LuaEnvironment : StateContext {
return true
}
fun invokeGlobal(name: String, vararg arguments: Any?): Array<out Any?> {
fun invokeGlobal(name: String, vararg arguments: Any?): Array<Any?> {
if (errorState)
return arrayOf()
@ -299,7 +302,15 @@ class LuaEnvironment : StateContext {
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 {
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)
}
}
private fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) {
}

View File

@ -1,22 +1,28 @@
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.readByteArray
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.collect.RandomListIterator
import ru.dbotthepony.kstarbound.collect.RandomSubList
import ru.dbotthepony.kstarbound.io.writeByteArray
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList
// original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol
// "extraStupid" will wrap data in extra byte array on legacy protocol
class NetworkedList<E>(
val codec: StreamCodec<E>,
val legacyCodec: StreamCodec<E> = codec,
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> {
private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
private val elements = elementsFactory(10)
@ -48,6 +54,16 @@ class NetworkedList<E>(
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() {
while (backlog.size >= maxBacklogSize) {
backlog.removeFirst()
@ -75,17 +91,18 @@ class NetworkedList<E>(
queue.clear()
elements.clear()
val count = data.readVarInt()
val stream = if (isLegacy && extraStupid) DataInputStream(FastByteArrayInputStream(data.readByteArray())) else data
val count = stream.readVarInt()
if (isLegacy) {
for (i in 0 until count) {
val read = legacyCodec.read(data)
val read = legacyCodec.read(stream)
elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read))
}
} else {
for (i in 0 until count) {
val read = codec.read(data)
val read = codec.read(stream)
elements.add(read)
backlog.add(currentVersion() to Entry(elements.size - 1, read))
}
@ -97,12 +114,23 @@ class NetworkedList<E>(
override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
val latest = latestState()
data.writeVarInt(latest.size)
if (isLegacy) {
latest.forEach { legacyCodec.write(data, it) }
if (isLegacy && extraStupid) {
val stream = FastByteArrayOutputStream()
val dstream = DataOutputStream(stream)
dstream.writeVarInt(latest.size)
latest.forEach { legacyCodec.write(dstream, it) }
data.writeByteArray(stream)
} 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
import com.google.gson.JsonObject
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import org.apache.logging.log4j.LogManager
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.kstarbound.math.AABBi
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
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.FloatingDungeonWorldParameters
import ru.dbotthepony.kstarbound.defs.world.WorldTemplate
import ru.dbotthepony.kstarbound.json.jsonArrayOf
import ru.dbotthepony.kstarbound.network.LegacyNetworkCellState
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.staticRandom64
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.world.CHUNK_SIZE_FF
import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.ChunkState
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.IChunkListener
import ru.dbotthepony.kstarbound.world.TileHealth
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.entities.AbstractEntity
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.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.function.Supplier
import java.util.random.RandomGenerator
import kotlin.concurrent.withLock
import kotlin.coroutines.cancellation.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.math.min
class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, ServerChunk>(world, pos) {
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()
// skip if we have no layout
if (world.template.worldLayout != null && world.template.worldParameters !is FloatingDungeonWorldParameters) {
if (world.template.worldLayout != null) {
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()
}
@ -717,13 +731,14 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
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) {
if (placement.item is BiomePlaceables.MicroDungeon) {
if (placement.item.microdungeons.isEmpty())
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))
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
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 {
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.future.asCompletableFuture
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.apache.logging.log4j.LogManager
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.util.AssetPathStack
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.world.ChunkPos
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 {})
*/
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)
return TileDamageResult.NONE
@ -235,7 +234,7 @@ class ServerWorld private constructor(
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)
var size: Int

View File

@ -9,7 +9,6 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
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.WorldStopPacket
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.IChunkListener
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 val damageTilesQueue = Channel<DamageTileEntry>(64) // 64 pending tile damages should be enough
private val tileModificationBudget = Pacer.actionsPerSecond(actions = 512, handicap = 2048) // TODO: make this configurable
private val damageTilesQueue = Channel<DamageTileEntry>(64) // 64 pending tile group damage requests should be more than enough
private val tileModificationBudget = ActionPacer(actions = 512, handicap = 2048) // TODO: make this configurable
private val modifyTilesQueue = Channel<Pair<Collection<Pair<Vector2i, TileModification>>, Boolean>>(64)
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() {
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()

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 val p by lazy(LazyThreadSafetyMode.NONE) { IntArray(parameters.scale * 2 + 2) }
protected val g1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
protected val p by lazy(LazyThreadSafetyMode.NONE) { IntArray(SCALE * 2 + 2) }
protected val g1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
// flat arrays for performance
protected val g2_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
protected val g2_1 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(SCALE * 2 + 2) }
protected val g3_0 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
protected val g3_1 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(parameters.scale * 2 + 2) }
protected val g3_2 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(SCALE * 2 + 2) }
protected val g3_2 by lazy(LazyThreadSafetyMode.NONE) { DoubleArray(SCALE * 2 + 2) }
private var init = false
private val initLock = Any()
@ -99,17 +99,17 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
val random = random(seed)
for (i in 0 until parameters.scale) {
for (i in 0 until SCALE) {
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_1[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble()
g2_0[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g2_1[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g3_0[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble()
g3_1[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble()
g3_2[i] = random.nextInt(-parameters.scale, parameters.scale) / parameters.scale.toDouble()
g3_0[i] = random.nextInt(-SCALE, SCALE) / SCALE.toDouble()
g3_1[i] = random.nextInt(-SCALE, SCALE) / 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 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 j = random.nextInt(0, parameters.scale - 1)
val j = random.nextInt(0, SCALE - 1)
p[i] = p[j]
p[j] = k
}
for (i in 0 until parameters.scale + 2) {
p[parameters.scale + i] = p[i]
g1[parameters.scale + i] = g1[i]
for (i in 0 until SCALE + 2) {
p[SCALE + i] = p[i]
g1[SCALE + i] = g1[i]
g2_0[parameters.scale + i] = g2_0[i]
g2_1[parameters.scale + i] = g2_1[i]
g2_0[SCALE + i] = g2_0[i]
g2_1[SCALE + i] = g2_1[i]
g3_0[parameters.scale + i] = g3_0[i]
g3_1[parameters.scale + i] = g3_1[i]
g3_2[parameters.scale + i] = g3_2[i]
g3_0[SCALE + i] = g3_0[i]
g3_1[SCALE + i] = g3_1[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 fv: Double = value - iv
val b0 = iv and (parameters.scale - 1)
val b1 = (iv + 1) and (parameters.scale - 1)
val b0 = iv and (SCALE - 1)
val b1 = (iv + 1) and (SCALE - 1)
val r1 = fv - 1.0
return Setup(b0, b1, fv, r1)
@ -260,6 +260,8 @@ abstract class AbstractPerlinNoise(val parameters: PerlinNoiseParameters) {
}
companion object : TypeAdapterFactory {
const val SCALE = 512
fun of(parameters: PerlinNoiseParameters): AbstractPerlinNoise {
return when (parameters.type) {
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())
}
fun RandomGenerator.nextRange(min: Int, max: Int): Int {
return if (min == max) return min else nextInt(min, max)
}
fun RandomGenerator.nextRange(range: IStruct2d): Double {
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.kstarbound.math.vector.Vector2d
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.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
@ -81,7 +81,7 @@ sealed class TileHealth() {
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
damagePercent = (damagePercent + actualDamage).coerceAtMost(1.0)
isHarvested = damage.harvestLevel >= config.harvestLevel
@ -97,7 +97,7 @@ sealed class TileHealth() {
val isTicking: Boolean
get() = !isHealthy && !isDead
fun tick(config: TileDamageConfig, delta: Double): Boolean {
fun tick(config: TileDamageParameters, delta: Double): Boolean {
if (isDead || isHealthy)
return false

View File

@ -1,8 +1,10 @@
package ru.dbotthepony.kstarbound.world.api
import com.github.benmanes.caffeine.cache.Interner
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kstarbound.Starbound
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.world.physics.CollisionType
import java.io.DataInputStream
@ -29,6 +31,10 @@ sealed class AbstractCell {
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 mutable(): MutableCell
@ -53,14 +59,19 @@ sealed class AbstractCell {
background.write(stream)
liquid.write(stream)
stream.write(0) // collisionMap
stream.write(foreground.material.value.collisionKind.ordinal)
stream.writeShort(dungeonId)
stream.writeByte(blockBiome)
stream.writeByte(envBiome)
stream.writeBoolean(biomeTransition)
stream.write(0) // unknown
if (rootSource == null) {
stream.writeBoolean(false)
} else {
stream.writeBoolean(true)
stream.writeStruct2i(rootSource!!)
}
}
companion object {

View File

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

View File

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

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
import com.google.gson.JsonObject
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
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.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileDamage
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
*/
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 yTilePositionNet = networkedSignedInt()
@ -228,5 +242,6 @@ abstract class TileEntity : AbstractEntity() {
companion object {
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.World
import ru.dbotthepony.kstarbound.world.entities.Animator
import ru.dbotthepony.kstarbound.world.entities.api.ScriptedEntity
import java.io.DataOutputStream
import java.util.Collections
import java.util.HashMap
import java.util.random.RandomGenerator
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity() {
open fun deserialize(data: JsonObject) {
open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntity(), ScriptedEntity {
override fun deserialize(data: JsonObject) {
super.deserialize(data)
direction = data.get("direction", directions) { Direction.LEFT }
orientationIndex = data.get("orientationIndex", -1).toLong()
isInteractive = data.get("interactive", false)
tilePosition = data.get("tilePosition", vectors)
lua.globals["storage"] = lua.from(data.get("scriptStorage") { JsonObject() })
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
loadParameters(data.get("parameters") { JsonObject() })
if ("uniqueId" in data)
uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull))
}
open fun loadParameters(parameters: JsonObject) {
@ -110,10 +112,9 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
}
}
open fun serialize(): JsonObject {
val into = JsonObject()
override fun serialize(): JsonObject {
val into = super.serialize()
into["name"] = config.key
into["tilePosition"] = vectors.toJsonTree(tilePosition)
into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex
into["interactive"] = isInteractive
@ -619,6 +620,14 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : TileEntit
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 {
private val lightColorPath = JsonPath("lightColor")
private val lightColorsPath = JsonPath("lightColors")

View File

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