diff --git a/ADDITIONS.md b/ADDITIONS.md index 9a734721..8a4e668c 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt index 26095ad9..1035b3ed 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Globals.kt @@ -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>() private set - var grassDamage by Delegates.notNull() + var grassDamage by Delegates.notNull() private set - var treeDamage by Delegates.notNull() + var treeDamage by Delegates.notNull() private set - var bushDamage by Delegates.notNull() + var bushDamage by Delegates.notNull() private set - var tileDamage by Delegates.notNull() + var tileDamage by Delegates.notNull() private set var sky by Delegates.notNull() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index c597952e..74bdda35 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -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("projectile").also(registriesInternal::add).also { adapters.add(it.adapter()) } val tenants = Registry("tenant").also(registriesInternal::add).also { adapters.add(it.adapter()) } val treasurePools = Registry("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val treasureChests = Registry("treasure chest").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterSkills = Registry("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) } val monsterTypes = Registry("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) } val worldObjects = Registry("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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt index a6884732..cb459f3f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RegistryTypeAdapterFactory.kt @@ -18,13 +18,16 @@ inline fun Registry.adapter(): TypeAdapterFactory { class RegistryTypeAdapterFactory(private val registry: Registry, private val clazz: KClass) : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - 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 - } else if (type.rawType == Registry.Ref::class.java) { - return RefImpl(gson) as TypeAdapter + if (type.rawType == Registry.Entry::class.java) { + return EntryImpl(gson) as TypeAdapter + } else if (type.rawType == Registry.Ref::class.java) { + return RefImpl(gson) as TypeAdapter + } } return null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt index 61d73089..08249e76 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/IThingWithDescription.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt index b5343b63..2036938e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PerlinNoiseParameters.kt @@ -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 - } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 81b12605..4f720d1a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt index 7b06c1a7..be0bc5ec 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/image/Image.kt @@ -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 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 { + 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 { + return spaceScanCache.get(SpaceScanKey(this, pixelOffset, spaceScan, flip)) { + ImmutableSet.copyOf(worldSpaces0(pixelOffset, spaceScan, flip)) + } + } + + private fun worldSpaces0(pixelOffset: Vector2i, spaceScan: Double, flip: Boolean): Set { 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 = Caffeine.newBuilder() + private val dataCache: LoadingCache = Caffeine.newBuilder() .expireAfterAccess(Duration.ofMinutes(1)) .weigher { 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>() @JvmStatic fun get(path: String): Image? { - return imageCache.computeIfAbsent(path) { + return imageCache.computeIfAbsent(path.substringBefore(':').substringBefore('?')) { try { val file = Starbound.locate(it) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasureChestDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasureChestDefinition.kt new file mode 100644 index 00000000..ddb78073 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasureChestDefinition.kt @@ -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, +) { + var name: String by Delegates.notNull() + + @JsonFactory + data class Variant( + val containers: ImmutableSet>, + val treasurePool: Either>, Registry.Ref>, + val minimumLevel: Double = 0.0 + ) { + val validContainers: ImmutableSet> by lazy { + containers.stream().filter { it.isPresent }.map { it.entry!! }.collect(ImmutableSet.toImmutableSet()) + } + + val validTreasurePools: ImmutableSet> by lazy { + treasurePool.map({ it.stream() }, { Stream.of(it) }).filter { it.isPresent }.map { it.entry!! }.collect(ImmutableSet.toImmutableSet()) + } + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val variants = gson.getAdapter>() + + override fun write(out: JsonWriter, value: TreasureChestDefinition) { + variants.write(out, value.variants) + } + + override fun read(`in`: JsonReader): TreasureChestDefinition { + return TreasureChestDefinition(variants.read(`in`)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index d8a03c74..4839e736 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -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, ) { @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt index 17374cbd..8c17e353 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/BuiltinMetaMaterials.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageParameters.kt similarity index 61% rename from src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageParameters.kt index d22fdd48..4545b811 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageParameters.kt @@ -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 = 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 = 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() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt index 15433ecc..bc30ba2c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDamageType.kt @@ -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), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt index e9db9e1a..1c4ecd79 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileDefinition.kt @@ -27,7 +27,7 @@ data class TileDefinition( val category: String, @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) - val damageTable: AssetReference = AssetReference(Globals::tileDamage), + val damageTable: AssetReference = 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt index cd15fc5e..766a2d37 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/TileModifierDefinition.kt @@ -25,7 +25,7 @@ data class TileModifierDefinition( val miningSounds: ImmutableList = ImmutableList.of(), @Deprecated("", replaceWith = ReplaceWith("this.actualDamageTable")) - val damageTable: AssetReference = AssetReference(Globals::tileDamage), + val damageTable: AssetReference = 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt index c92e68cf..1eaa42d1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceables.kt @@ -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 create(gson: Gson, type: TypeToken): TypeAdapter? { 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() - 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) : 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) : Item() { @@ -182,19 +256,48 @@ data class BiomePlaceables( TreeVariant::class.java ).type) } - } - private object PoolTypeToken : TypeToken>>() + 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>) : Item() { + data class Object(val pool: WeightedList, 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, JsonObject>>> + + private val objectPoolAdapter by lazy { + Starbound.gson.getAdapter(typeToken) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt index 1b3f9586..9ae82a0a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BiomePlaceablesDefinition.kt @@ -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 = ImmutableSet.of()) : DistributionItemData() { + data class TreasureBox(val treasureBoxSets: ImmutableSet> = ImmutableSet.of()) : DistributionItemData() { override val type: BiomePlacementItemType get() = BiomePlacementItemType.MICRO_DUNGEON + val validTreasureBoxSets: ImmutableSet> 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> = ImmutableList.of(), val parameters: JsonElement = JsonObject()) + data class ObjectPool(val pool: ImmutableList>> = ImmutableList.of(), val parameters: JsonObject = JsonObject()) @JsonFactory data class Object(val objectSets: ImmutableList) : 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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt index 82bbf45a..6889bf2b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/BushVariant.kt @@ -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) @@ -46,7 +46,7 @@ class BushVariant( val mods: ImmutableSet = ImmutableSet.of(), val ceiling: Boolean = false, val ephemeral: Boolean = true, - val damageTable: AssetReference? = null, + val damageTable: AssetReference? = null, val health: Double = 1.0, ) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt index 1f219340..764a9b9a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/GrassVariant.kt @@ -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 = 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? = null, + val damageTable: AssetReference? = null, val health: Double = 1.0 ) { init { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt index 2b97f256..175397ab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/TreeVariant.kt @@ -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 = 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? = null, + val damageTable: AssetReference? = null, val health: Double = 1.0, ) @@ -75,11 +74,11 @@ data class TreeVariant( fun create(data: Registry.Entry, 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, stemHueShift: Double, fdata: Registry.Entry, 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() ?: "/", diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt index 0c6e6c33..1fb132e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldLayout.kt @@ -89,8 +89,6 @@ class WorldLayout { WorldGeometry(worldSize, loopX, loopY) } - private object StartingRegionsToken : TypeToken>() - @JsonFactory data class SerializedLayer( val yStart: Int, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt index 68aca882..504a68fa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/WorldTemplate.kt @@ -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, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index 049da7f2..dab4bf35 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -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 = De ListenableDelegate.maskSmart(value, getter, setter), Vector4dCodec) fun DelegateSyncher.vec4f(value: Vector4f, setter: DelegateSetter = DelegateSetter.passthrough(), getter: DelegateGetter = 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) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt index 37875886..32d63a88 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt index fcd239fd..6f175d4a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemRegistry.kt @@ -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()) + 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt index e2527fcb..938f71a2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt @@ -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") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt index b0b284a8..700fcb94 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/Annotations.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt index cbeb03fe..bf1e8f9a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/LuaEnvironment.kt @@ -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 { + fun invokeGlobal(name: String, vararg arguments: Any?): Array { 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 { + return executor.call(this, loader.compileTextChunk(chunk, name).newInstance(Variable(globals))) + } + companion object { private val LOGGER = LogManager.getLogger() + private val COUNTER = AtomicLong() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt new file mode 100644 index 00000000..759ecac5 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/ServerWorldBindings.kt @@ -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) { + +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt index 0f3a7ebc..99d68bd2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldBindings.kt @@ -586,7 +586,3 @@ fun provideWorldBindings(self: World<*, *>, lua: LuaEnvironment) { provideServerWorldBindings(self, callbacks, lua) } } - -private fun provideServerWorldBindings(self: ServerWorld, callbacks: Table, lua: LuaEnvironment) { - -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt index ffed82db..225f43de 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt @@ -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( val codec: StreamCodec, val legacyCodec: StreamCodec = codec, private val maxBacklogSize: Int = 100, - private val elementsFactory: (Int) -> MutableList = ::ArrayList + private val elementsFactory: (Int) -> MutableList = ::ArrayList, + private val extraStupid: Boolean = false, ) : NetworkedElement(), MutableList { private val backlog = ArrayDeque>>() private val elements = elementsFactory(10) @@ -48,6 +54,16 @@ class NetworkedList( 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( 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( 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) } + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 886866b4..54104c54 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -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(world, pos) { override var state: ChunkState = ChunkState.FRESH @@ -185,10 +194,15 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk() + + 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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index a440729d..3e9f5cd4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -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, isBackground: Boolean, sourcePosition: Vector2d, damage: TileDamage, source: AbstractEntity? = null, pacer: Pacer? = null): TileDamageResult { + suspend fun damageTiles(positions: Collection, 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>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: Pacer?): List> { + suspend fun applyTileModifications(modifications: Collection>, allowEntityOverlap: Boolean, ignoreTileProtection: Boolean = false, pacer: ActionPacer?): List> { val unapplied = ArrayList(modifications) var size: Int diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt index 13433008..f631a4dd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorldTracker.kt @@ -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, val isBackground: Boolean, val sourcePosition: Vector2d, val damage: TileDamage, val source: AbstractEntity? = null) - private val damageTilesQueue = Channel(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(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>, Boolean>>(64) private suspend fun damageTilesLoop() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ActionPacer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ActionPacer.kt new file mode 100644 index 00000000..d4444508 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ActionPacer.kt @@ -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) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt index 7a0f967b..177a476f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/BlockableEventLoop.kt @@ -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() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionTimePacer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionTimePacer.kt new file mode 100644 index 00000000..aaf28d54 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/ExecutionTimePacer.kt @@ -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() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Pacer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Pacer.kt deleted file mode 100644 index 58a6622f..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Pacer.kt +++ /dev/null @@ -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) - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt index df4c3296..0e6054d0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/AbstractPerlinNoise.kt @@ -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) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt index eb52eaa9..dd3b6f44 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/random/RandomUtils.kt @@ -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()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt index 4f5c066d..8aee8cf8 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/TileHealth.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt index c9782b29..84df3d78 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/AbstractCell.kt @@ -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 { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt index 67e3d6bd..cc888de5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/ImmutableCell.kt @@ -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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt index 42bc1cf0..e977b476 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/api/MutableCell.kt @@ -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 } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index f03091f1..15cf72af 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -72,8 +72,8 @@ abstract class AbstractEntity : Comparable { 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 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index bea994eb..681c7866 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -143,6 +143,8 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } } } + + items.shuffle(random) } override fun randomize(random: RandomGenerator, threatLevel: Double) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt new file mode 100644 index 00000000..2c39be31 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -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 = 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 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 = 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 = setOf() + private var calculatedRoots: Set = setOf() + + override var metaBoundingBox: AABB = calculatedBoundingBox + private set + + override var occupySpaces: ImmutableSet = ImmutableSet.of() + private set + + override val materialSpaces: Collection>> + get() = setOf() + + override var roots: ImmutableSet = 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() + 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() + + 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, 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) } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt index 9e849236..085a663c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/TileEntity.kt @@ -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) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 244869ab..a489a225 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -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) : TileEntity() { - open fun deserialize(data: JsonObject) { +open class WorldObject(val config: Registry.Entry) : 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) : 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) : TileEntit return tileHealth.isDead } + override fun callScript(fnName: String, vararg arguments: Any?): Array { + return lua.invokeGlobal(fnName, *arguments) + } + + override fun evalScript(code: String): Array { + return lua.eval(code) + } + companion object { private val lightColorPath = JsonPath("lightColor") private val lightColorsPath = JsonPath("lightColors") diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt index fd25aa4b..0b9c3688 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/terrain/DisplacementTerrainSelector.kt @@ -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,