diff --git a/ADDITIONS.md b/ADDITIONS.md index a9a1a926..4b5da743 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -18,3 +18,7 @@ * Rolled per each "stem-foliage" combination * Also two more properties were added: `sameStemHueShift` (defaults to `true`) and `sameFoliageHueShift` (defaults to `false`), which fixate hue shifts within same "stem-foliage" combination * Original engine always generates two tree types when processing placeable items, new engine however, allows to generate any number of trees. + +### player.config + * Inventory bags are no longer limited to 255 slots + * However, when joining original servers with mod which increase bag size past 255 slots will result in undefined behavior (joining servers with inventory size bag mods will already result in nearly instant desync though, so you may not ever live to see the side effects; and if original server installs said mod, original clients and original server will experience severe desyncs/undefined behavior too) diff --git a/gradle.properties b/gradle.properties index b56f2bd8..1f1a09ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.9.25 +kommonsVersion=2.10.2 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt index d3c80b5f..b935d143 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/GlobalDefaults.kt @@ -5,8 +5,10 @@ import com.google.gson.TypeAdapter import org.apache.logging.log4j.LogManager import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.ClientConfigParameters +import ru.dbotthepony.kstarbound.defs.CurrencyDefinition import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.UniverseServerConfig +import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig import ru.dbotthepony.kstarbound.defs.world.TerrestrialWorldsConfig import ru.dbotthepony.kstarbound.defs.world.AsteroidWorldsConfig @@ -25,6 +27,9 @@ import kotlin.reflect.KMutableProperty0 object GlobalDefaults { private val LOGGER = LogManager.getLogger() + var player by Delegates.notNull() + private set + var actorMovementParameters = ActorMovementParameters() private set @@ -61,6 +66,9 @@ object GlobalDefaults { var universeServer by Delegates.notNull() private set + var currencies by Delegates.notNull>() + private set + private object EmptyTask : ForkJoinTask() { private fun readResolve(): Any = EmptyTask override fun getRawResult() { @@ -109,12 +117,14 @@ object GlobalDefaults { tasks.add(load("/world_template.config", ::worldTemplate)) tasks.add(load("/sky.config", ::sky)) tasks.add(load("/universe_server.config", ::universeServer)) + tasks.add(load("/player.config", ::player)) tasks.add(load("/plants/grassDamage.config", ::grassDamage)) tasks.add(load("/plants/treeDamage.config", ::treeDamage)) tasks.add(load("/plants/bushDamage.config", ::bushDamage)) tasks.add(load("/dungeon_worlds.config", ::dungeonWorlds, Starbound.gson.mapAdapter())) + tasks.add(load("/currencies.config", ::currencies, Starbound.gson.mapAdapter())) return tasks } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt index 4da3aa5b..e69a920b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/RecipeRegistry.kt @@ -43,14 +43,14 @@ object RecipeRegistry { }).add(recipe) } - output2recipesInternal.computeIfAbsent(value.output.item.key.left(), Object2ObjectFunction { p -> + output2recipesInternal.computeIfAbsent(value.output.name, Object2ObjectFunction { p -> ArrayList(1).also { output2recipesBacking[p as String] = Collections.unmodifiableList(it) } }).add(recipe) for (input in value.input) { - input2recipesInternal.computeIfAbsent(input.item.key.left(), Object2ObjectFunction { p -> + input2recipesInternal.computeIfAbsent(input.name, Object2ObjectFunction { p -> ArrayList(1).also { input2recipesBacking[p as String] = Collections.unmodifiableList(it) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index b69839a2..63bfdca6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -7,6 +7,7 @@ import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory import com.google.gson.stream.JsonReader import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.defs.Json2Function import ru.dbotthepony.kstarbound.defs.JsonConfigFunction import ru.dbotthepony.kstarbound.defs.JsonFunction @@ -29,8 +30,8 @@ import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition import ru.dbotthepony.kstarbound.defs.npc.TenantDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition -import ru.dbotthepony.kstarbound.defs.particle.ParticleDefinition import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition +import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition @@ -66,7 +67,7 @@ object Registries { val liquid = Registry("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) } val species = Registry("species").also(registriesInternal::add).also { adapters.add(it.adapter()) } val statusEffects = Registry("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) } - val particles = Registry("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) } + val particles = Registry("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) } val items = Registry("item").also(registriesInternal::add).also { adapters.add(it.adapter()) } val questTemplates = Registry("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) } val techs = Registry("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) } @@ -153,7 +154,7 @@ object Registries { tasks.addAll(loadRegistry(worldObjects, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName))) tasks.addAll(loadRegistry(statusEffects, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name))) tasks.addAll(loadRegistry(species, fileTree["species"] ?: listOf(), key(Species::kind))) - tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), key(ParticleDefinition::kind))) + tasks.addAll(loadRegistry(particles, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to null })) tasks.addAll(loadRegistry(questTemplates, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id))) tasks.addAll(loadRegistry(techs, fileTree["tech"] ?: listOf(), key(TechDefinition::name))) tasks.addAll(loadRegistry(npcTypes, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type))) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt index fa82c3e9..efcbdb7c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registry.kt @@ -14,6 +14,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.util.traverseJsonPath +import java.util.Collections import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.locks.ReentrantLock import java.util.function.Supplier @@ -21,9 +22,9 @@ import kotlin.collections.set import kotlin.concurrent.withLock class Registry(val name: String) { - private val keysInternal = Object2ObjectOpenHashMap() + private val keysInternal = HashMap() private val idsInternal = Int2ObjectOpenHashMap() - private val keyRefs = Object2ObjectOpenHashMap() + private val keyRefs = HashMap() private val idRefs = Int2ObjectOpenHashMap() private val backlog = ConcurrentLinkedQueue() @@ -45,7 +46,7 @@ class Registry(val name: String) { private val lock = ReentrantLock() - val keys: Object2ObjectMap> = Object2ObjectMaps.unmodifiable(keysInternal) + val keys: Map> = Collections.unmodifiableMap(keysInternal) val ids: Int2ObjectMap> = Int2ObjectMaps.unmodifiable(idsInternal) sealed class Ref : Supplier?> { @@ -56,6 +57,9 @@ class Registry(val name: String) { val isPresent: Boolean get() = value != null + val isEmpty: Boolean + get() = value == null + val value: T? get() = entry?.value @@ -76,6 +80,7 @@ class Registry(val name: String) { abstract val file: IStarboundFile? abstract val registry: Registry abstract val isBuiltin: Boolean + abstract val ref: Ref fun traverseJsonPath(path: String): JsonElement? { return traverseJsonPath(path, json) @@ -94,6 +99,8 @@ class Registry(val name: String) { override var file: IStarboundFile? = null override var isBuiltin: Boolean = false + override val ref: Ref by lazy { ref(key) } + override fun equals(other: Any?): Boolean { return this === other } @@ -168,7 +175,7 @@ class Registry(val name: String) { var valid = true keyRefs.values.forEach { - if (!it.isPresent) { + if (!it.isPresent && it.key.left() != "") { LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") valid = false } @@ -185,6 +192,8 @@ class Registry(val name: String) { } fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry { + require(key != "") { "Adding $name with empty name (empty name is reserved)" } + lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") @@ -211,6 +220,8 @@ class Registry(val name: String) { } fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry { + require(key != "") { "Adding $name with empty name (empty name is reserved)" } + lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") @@ -243,6 +254,8 @@ class Registry(val name: String) { } fun add(key: String, value: T, isBuiltin: Boolean = false): Entry { + require(key != "") { "Adding $name with empty name (empty name is reserved)" } + lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") @@ -270,6 +283,8 @@ class Registry(val name: String) { } fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry { + require(key != "") { "Adding $name with empty name (empty name is reserved)" } + lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") @@ -302,6 +317,8 @@ class Registry(val name: String) { } } + val emptyRef = ref("") + companion object { private val LOGGER = LogManager.getLogger() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 0cf6db83..ffd0597b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -24,6 +24,7 @@ import ru.dbotthepony.kommons.gson.Vector4iTypeAdapter import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kstarbound.collect.WeightedList import ru.dbotthepony.kstarbound.defs.* +import ru.dbotthepony.kstarbound.defs.actor.StatModifier import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.item.api.IArmorItemDefinition @@ -32,6 +33,8 @@ import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList +import ru.dbotthepony.kstarbound.defs.animation.Particle +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.world.CelestialParameters import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType import ru.dbotthepony.kstarbound.defs.world.terrain.BiomePlaceables @@ -54,7 +57,7 @@ import ru.dbotthepony.kstarbound.json.factory.RGBAColorTypeAdapter import ru.dbotthepony.kstarbound.json.factory.SingletonTypeAdapterFactory import ru.dbotthepony.kstarbound.math.* import ru.dbotthepony.kstarbound.server.world.UniverseChunk -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.util.HashTableInterner import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise @@ -86,8 +89,8 @@ object Starbound : ISBFileLocator { const val ENGINE_VERSION = "0.0.1" const val NATIVE_PROTOCOL_VERSION = 748 const val LEGACY_PROTOCOL_VERSION = 747 - const val TICK_TIME_ADVANCE = 1.0 / 60.0 - const val TICK_TIME_ADVANCE_NANOS = (TICK_TIME_ADVANCE * 1_000_000_000L).toLong() + const val TIMESTEP = 1.0 / 60.0 + const val TICK_TIME_ADVANCE_NANOS = (TIMESTEP * 1_000_000_000L).toLong() // compile flags. uuuugh const val DEDUP_CELL_STATES = true @@ -245,7 +248,7 @@ object Starbound : ISBFileLocator { registerTypeAdapter(ItemStack.Adapter(this@Starbound)) - registerTypeAdapterFactory(ItemReference.Factory(STRINGS)) + registerTypeAdapter(ItemDescriptor::Adapter) registerTypeAdapterFactory(TreasurePoolDefinition.Companion) registerTypeAdapterFactory(UniverseChunk.Companion) @@ -255,6 +258,7 @@ object Starbound : ISBFileLocator { registerTypeAdapterFactory(Poly.Companion) registerTypeAdapter(CelestialParameters::Adapter) + registerTypeAdapter(Particle::Adapter) registerTypeAdapterFactory(BiomePlacementDistributionType.DATA_ADAPTER) registerTypeAdapterFactory(BiomePlacementDistributionType.DEFINITION_ADAPTER) @@ -289,21 +293,15 @@ object Starbound : ISBFileLocator { } fun item(name: String): ItemStack { - return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY) + TODO() } fun item(name: String, count: Long): ItemStack { - if (count <= 0L) - return ItemStack.EMPTY - - return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY, count = count) + TODO() } fun item(name: String, count: Long, parameters: JsonObject): ItemStack { - if (count <= 0L) - return ItemStack.EMPTY - - return ItemStack(Registries.items[name] ?: return ItemStack.EMPTY, count = count, parameters = parameters) + TODO() } fun item(descriptor: JsonObject): ItemStack { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt index ee862f97..f122a7b1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/StarboundClient.kt @@ -925,7 +925,7 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { } input.think() - camera.think(Starbound.TICK_TIME_ADVANCE) + camera.think(Starbound.TIMESTEP) executeQueuedTasks() layers.clear() @@ -990,8 +990,8 @@ class StarboundClient private constructor(val clientID: Int) : Closeable { ply.movement.controlRun = !input.KEY_LEFT_SHIFT_DOWN } else { camera.pos += Vector2d( - (if (input.KEY_A_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0), - (if (input.KEY_W_DOWN) Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TICK_TIME_ADVANCE * 32f / settings.zoom else 0.0) + (if (input.KEY_A_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_D_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0), + (if (input.KEY_W_DOWN) Starbound.TIMESTEP * 32f / settings.zoom else 0.0) + (if (input.KEY_S_DOWN) -Starbound.TIMESTEP * 32f / settings.zoom else 0.0) ) camera.pos = world?.geometry?.wrap(camera.pos) ?: camera.pos diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt index 3d3800ef..84c013f4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/network/packets/ForgetEntityPacket.kt @@ -17,9 +17,5 @@ class ForgetEntityPacket(val uuid: UUID) : IClientPacket { override fun play(connection: ClientConnection) { val world = connection.client.world ?: return - - world.mailbox.execute { - world.entities.firstOrNull { it.entityID == 0 }?.remove() - } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt index e851e97f..edd0407d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/world/ClientWorld.kt @@ -302,7 +302,7 @@ class ClientWorld( } } - for (ent in entities) { + for (ent in entities.values) { ent.render(client, layers) ent.addLights(client.viewportLighting, client.viewportCellX, client.viewportCellY) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt new file mode 100644 index 00000000..0b354215 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/collect/IdMap.kt @@ -0,0 +1,52 @@ +package ru.dbotthepony.kstarbound.collect + +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.ints.Int2ObjectMap + +class IdMap(val min: Int = 0, val max: Int = Int.MAX_VALUE, private val map: MutableMap = Int2ObjectAVLTreeMap()) : MutableMap by map { + private var nextIndex = min - 1 + private val range = max - min + + init { + require(range > 1) { "Invalid range for ID Map: $min .. $max" } + } + + private fun next(): Int { + if (++nextIndex > max) { + nextIndex = min + } + + return nextIndex + } + + fun add(value: T): Int { + var i = 0 + var index = next() + + if (map is Int2ObjectMap) { + while ((map as Int2ObjectMap).containsKey(index)) { + index = next() + + if (i++ > range) { + throw RuntimeException("No more free slots in ID Map") + } + } + } else { + while (map.containsKey(index)) { + index = next() + + if (i++ > range) { + throw RuntimeException("No more free slots in ID Map") + } + } + } + + map[index] = value + return index + } + + override fun clear() { + nextIndex = min - 1 + map.clear() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt index 6109fc57..0ff166fa 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetPath.kt @@ -10,6 +10,8 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack data class AssetPath(val path: String, val fullPath: String) { + constructor(path: String) : this(path, path) + companion object : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == AssetPath::class.java) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt index 4b857ceb..f44cc4ef 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/AssetReference.kt @@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.util.AssetPathStack import java.lang.reflect.ParameterizedType import java.util.* import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.HashMap data class AssetReference(val path: String?, val fullPath: String?, val value: V?, val json: JsonElement?) { companion object : TypeAdapterFactory { @@ -30,7 +31,7 @@ data class AssetReference(val path: String?, val fullPath: String?, val value val param = type.type as? ParameterizedType ?: return null return object : TypeAdapter>() { - private val cache = Collections.synchronizedMap(Object2ObjectOpenHashMap>()) + private val cache = Collections.synchronizedMap(HashMap>()) private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter private val strings = gson.getAdapter(String::class.java) private val jsons = gson.getAdapter(JsonElement::class.java) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt new file mode 100644 index 00000000..0d43363c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/CurrencyDefinition.kt @@ -0,0 +1,9 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition + +data class CurrencyDefinition( + val representativeItem: Registry.Ref, + val playerMax: Long = 999999, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt deleted file mode 100644 index 50bf8a70..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ItemReference.kt +++ /dev/null @@ -1,68 +0,0 @@ -package ru.dbotthepony.kstarbound.defs - -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 -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition -import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kommons.gson.consumeNull -import ru.dbotthepony.kstarbound.util.ItemStack - -/** - * Прототип [ItemStack] в JSON файлах - */ -data class ItemReference( - val item: Registry.Ref, - val count: Long = 1, - val parameters: JsonObject = JsonObject() -) { - init { - require(item.key.isLeft) { "Can't reference item by ID" } - } - - fun makeStack(): ItemStack { - return ItemStack(item.entry ?: return ItemStack.EMPTY, count, parameters) - } - - class Factory(val stringInterner: Interner = Interner { it }) : TypeAdapterFactory { - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (type.rawType == ItemReference::class.java) { - return object : TypeAdapter() { - private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = false), gson, stringInterner) - private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = true), gson, stringInterner) - private val references = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, IItemDefinition::class.java)) as TypeAdapter> - - override fun write(out: JsonWriter, value: ItemReference?) { - if (value == null) - out.nullValue() - else - regularObject.write(out, value) - } - - override fun read(`in`: JsonReader): ItemReference? { - if (`in`.consumeNull()) - return null - - if (`in`.peek() == JsonToken.STRING) { - return ItemReference(references.read(`in`)) - } else if (`in`.peek() == JsonToken.BEGIN_ARRAY) { - return regularList.read(`in`) - } else { - return regularObject.read(`in`) - } - } - } as TypeAdapter - } - - return null - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt index f82c3d8d..8f177f61 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/JsonDriven.kt @@ -22,10 +22,10 @@ import kotlin.reflect.javaType */ abstract class JsonDriven(val path: String) { private val delegates = ArrayList>() - private val delegatesMap = Object2ObjectOpenHashMap>>() + private val delegatesMap = HashMap>>() private val lazies = ArrayList>() - private val namedLazies = Object2ObjectOpenHashMap>>() + private val namedLazies = HashMap>>() protected val properties = JsonObject() @@ -91,7 +91,7 @@ abstract class JsonDriven(val path: String) { var name: String? = name private set(value) { - if (field != null) + if (field != null || value == null) throw IllegalStateException() field = value diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt deleted file mode 100644 index 072ee92c..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/StatModifier.kt +++ /dev/null @@ -1,58 +0,0 @@ -package ru.dbotthepony.kstarbound.defs - -import com.google.common.collect.ImmutableSet -import com.google.gson.Gson -import com.google.gson.JsonObject -import com.google.gson.JsonSyntaxException -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import ru.dbotthepony.kommons.gson.consumeNull -import ru.dbotthepony.kommons.gson.contains -import ru.dbotthepony.kommons.gson.get - -enum class StatModifierType(vararg names: String) { - BASE_ADDITION("value", "baseAddition"), - BASE_MULTIPLICATION("baseMultiplier"), - - OVERALL_ADDITION("effectiveValue", "effectiveAddition"), - OVERALL_MULTIPLICATION("effectiveMultiplier"); - - val names: ImmutableSet = ImmutableSet.copyOf(names) -} - -data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) { - class Adapter(gson: Gson) : TypeAdapter() { - private val objects = gson.getAdapter(JsonObject::class.java) - - override fun write(out: JsonWriter, value: StatModifier?) { - if (value == null) { - out.nullValue() - } else { - out.beginObject() - out.name("stat") - out.value(value.stat) - out.name(value.type.names.first()) - out.value(value.value) - out.endObject() - } - } - - override fun read(`in`: JsonReader): StatModifier? { - if (`in`.consumeNull()) { - return null - } else { - val read = objects.read(`in`) - - val stat = read["stat"]?.asString ?: throw JsonSyntaxException("No stat to modify specified") - - for (type in StatModifierType.entries) - for (name in type.names) - if (name in read) - return StatModifier(stat, read.get(name, 0.0), type) - - throw JsonSyntaxException("Not a stat modifier: $read") - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt new file mode 100644 index 00000000..124d4155 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifier.kt @@ -0,0 +1,110 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.io.readBinaryString +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream + +data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) { + class Adapter(gson: Gson) : TypeAdapter() { + private val objects = gson.getAdapter(JsonObject::class.java) + + override fun write(out: JsonWriter, value: StatModifier?) { + if (value == null) { + out.nullValue() + } else { + out.beginObject() + out.name("stat") + out.value(value.stat) + out.name(value.type.names.first()) + out.value(value.value) + out.endObject() + } + } + + override fun read(`in`: JsonReader): StatModifier? { + if (`in`.consumeNull()) { + return null + } else { + val read = objects.read(`in`) + + val stat = read["stat"]?.asString ?: throw JsonSyntaxException("No stat to modify specified") + + for (type in StatModifierType.entries) + for (name in type.names) + if (name in read) + return StatModifier(stat, read.get(name, 0.0), type) + + throw JsonSyntaxException("Not a stat modifier: $read") + } + } + } + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + when (type) { + // legacy network protocol doesn't support overall addition + StatModifierType.OVERALL_ADDITION, StatModifierType.BASE_ADDITION -> stream.writeByte(1) + StatModifierType.BASE_MULTIPLICATION -> stream.writeByte(2) + StatModifierType.OVERALL_MULTIPLICATION -> stream.writeByte(3) + } + } else { + stream.writeByte(type.ordinal) + } + + stream.writeBinaryString(stat) + + if (isLegacy) + stream.writeFloat(value.toFloat()) + else + stream.writeDouble(value) + } + + companion object { + val CODEC = nativeCodec(::read, StatModifier::write) + val LEGACY_CODEC = legacyCodec(::read, StatModifier::write) + + fun read(stream: DataInputStream, isLegacy: Boolean): StatModifier { + if (isLegacy) { + return when (val type = stream.readUnsignedByte()) { + 0 -> throw UnsupportedOperationException("Empty StatModifier MVariant received on network, this is not supported by this game engine") + 1 -> value(stream.readInternedString(), stream.readFloat().toDouble()) + 2 -> multBase(stream.readInternedString(), stream.readFloat().toDouble()) + 3 -> multEffective(stream.readInternedString(), stream.readFloat().toDouble()) + else -> throw IllegalArgumentException("Unknown StatModifier type: $type") + } + } else { + val type = StatModifierType.entries[stream.readUnsignedByte()] + return StatModifier(stream.readInternedString(), stream.readDouble(), type) + } + } + + fun value(name: String, value: Double): StatModifier { + return StatModifier(name, value, StatModifierType.BASE_ADDITION) + } + + fun multBase(name: String, value: Double): StatModifier { + return StatModifier(name, value, StatModifierType.BASE_MULTIPLICATION) + } + + fun valueEffective(name: String, value: Double): StatModifier { + return StatModifier(name, value, StatModifierType.OVERALL_ADDITION) + } + + fun multEffective(name: String, value: Double): StatModifier { + return StatModifier(name, value, StatModifierType.OVERALL_MULTIPLICATION) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifierType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifierType.kt new file mode 100644 index 00000000..af83ec21 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatModifierType.kt @@ -0,0 +1,29 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import com.google.common.collect.ImmutableSet + +enum class StatModifierType(vararg names: String) { + BASE_ADDITION("amount", "baseAddition"), + + // Multipliers act exactly the way you'd expect: 0.0 is a 100% reduction of the + // base stat, while 2.0 is a 100% increase. Since these are *base* multipliers + // they do not interact with each other, thus stacking a 0.0 and a 2.0 leaves + // the stat unmodified + + // in other words, they stack additively. + BASE_MULTIPLICATION("baseMultiplier"), + + // KStarbound extension, added on top of base addition and base multiplication, + // but BEFORE effectiveMultiplier + OVERALL_ADDITION("effectiveAmount", "effectiveAddition"), + + // Unlike base multipliers, these all stack multiplicatively with the final + // stat value (including all base and value modifiers) such that an effective + // multiplier of 0.0 will ALWAYS reduce the stat to 0 regardless of other + // effects + + // in other words, they stack multiplicatively + OVERALL_MULTIPLICATION("effectiveMultiplier"); + + val names: ImmutableSet = ImmutableSet.copyOf(names) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt new file mode 100644 index 00000000..ebfbc3d2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/StatusControllerConfig.kt @@ -0,0 +1,43 @@ +package ru.dbotthepony.kstarbound.defs.actor + +import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class StatusControllerConfig( + val statusProperties: JsonObject = JsonObject(), + val minimumLiquidStatusEffectPercentage: Double, + val appliesEnvironmentStatusEffects: Boolean, + val appliesWeatherStatusEffects: Boolean, + val environmentStatusEffectUpdateTimer: Double = 0.15, + val primaryAnimationConfig: AssetPath? = null, + val primaryScriptSources: ImmutableList = ImmutableList.of(), + val primaryScriptDelta: Int = 1, + val keepDamageNotificationSteps: Int = 120, + val stats: ImmutableMap = ImmutableMap.of(), + val resources: ImmutableMap = ImmutableMap.of(), +) { + init { + require(primaryScriptDelta >= 1) { "Non-positive primaryScriptDelta: $primaryScriptDelta" } + require(keepDamageNotificationSteps >= 1) { "Non-positive keepDamageNotificationSteps: $keepDamageNotificationSteps" } + } + + @JsonFactory + data class Stat( + val baseValue: Double = 0.0, + ) + + @JsonFactory + data class Resource( + val maxStat: String? = null, + val deltaStat: String? = null, + val maxValue: Double? = null, + val deltaValue: Double? = null, + val initialValue: Double? = null, + val initialPercentage: Double? = null, + ) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt index 7d1285e2..d054a9e9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/Types.kt @@ -22,3 +22,21 @@ enum class HumanoidEmote(override val jsonName: String) : IStringSerializable { EAT("Eat"), SLEEP("Sleep"); } + +enum class EquipmentSlot(override val jsonName: String) : IStringSerializable { + HEAD("Head"), + CHEST("Chest"), + LEGS("Legs"), + BACK("Back"), + HEAD_COSMETIC("HeadCosmetic"), + CHEST_COSMETIC("ChestCosmetic"), + LEGS_COSMETIC("LegsCosmetic"), + BACK_COSMETIC("BackCosmetic"); +} + +enum class EssentialSlot(override val jsonName: String) : IStringSerializable { + BEAM_AXE("BeamAxe"), + WIRE_TOOL("WireTool"), + PAINT_TOOL("PaintTool"), + INSPECTION_TOOL("InspectionTool"); +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt index 4b4bc091..47aa56b4 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/BagFilterConfig.kt @@ -2,7 +2,7 @@ package ru.dbotthepony.kstarbound.defs.actor.player import com.google.common.collect.ImmutableSet import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import java.util.function.Predicate @JsonFactory @@ -17,9 +17,9 @@ data class BagFilterConfig( return false if (typeBlacklist != null) { - return !typeBlacklist.contains(t.item!!.value.category) + return !typeBlacklist.contains(t.config.value!!.category) } else if (typeWhitelist != null) { - return typeWhitelist.contains(t.item!!.value.category) + return typeWhitelist.contains(t.config.value!!.category) } return true diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/InventoryConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/InventoryConfig.kt index 2fcc49b6..027902ae 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/InventoryConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/InventoryConfig.kt @@ -6,8 +6,8 @@ import ru.dbotthepony.kstarbound.util.WriteOnce @JsonFactory data class InventoryConfig( - val customBarGroups: Int, - val customBarIndexes: Int, + val customBarGroups: Int, // hotbar configurations / groups + val customBarIndexes: Int, // hotbar slots val itemBags: ImmutableMap, ) { @@ -15,13 +15,5 @@ data class InventoryConfig( class Bag( val priority: Int, val size: Int - ) { - var name: String by WriteOnce() - } - - init { - for ((k, v) in itemBags) { - v.name = k - } - } + ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerBusyState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerBusyState.kt new file mode 100644 index 00000000..d82a2ad1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerBusyState.kt @@ -0,0 +1,17 @@ +package ru.dbotthepony.kstarbound.defs.actor.player + +import ru.dbotthepony.kommons.io.IntValueCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class PlayerBusyState(override val jsonName: String) : IStringSerializable { + NONE("none"), + CHATTING("chatting"), + MENU("menu"); + + companion object { + val CODEC = StreamCodec.Enum(PlayerBusyState::class.java) + val LEGACY_CODEC = IntValueCodec.map({ entries[this] }, { ordinal }) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt index f90d2c67..29a99fc2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerConfig.kt @@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.ActorMovementParameters import ru.dbotthepony.kstarbound.defs.AssetReference import ru.dbotthepony.kstarbound.defs.Species +import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig import ru.dbotthepony.kstarbound.util.SBPattern import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition @@ -35,7 +36,7 @@ data class PlayerConfig( val movementParameters: ActorMovementParameters, val zeroGMovementParameters: ActorMovementParameters, - val statusControllerSettings: StatusControllerSettings, + val statusControllerSettings: StatusControllerConfig, val foodLowThreshold: Double, val foodLowStatusEffects: ImmutableList = ImmutableList.of(), @@ -75,4 +76,4 @@ data class PlayerConfig( val genericScriptContexts: ImmutableMap, val inventory: InventoryConfig, val inventoryFilters: ImmutableMap, -) +) \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerGamemode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerGamemode.kt new file mode 100644 index 00000000..fe94318a --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/PlayerGamemode.kt @@ -0,0 +1,17 @@ +package ru.dbotthepony.kstarbound.defs.actor.player + +import ru.dbotthepony.kommons.io.IntValueCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable + +enum class PlayerGamemode(override val jsonName: String) : IStringSerializable { + CASUAL("casual"), + SURVIVAL("survival"), + HARDCORE("hardcore"); + + companion object { + val CODEC = StreamCodec.Enum(PlayerGamemode::class.java) + val LEGACY_CODEC = IntValueCodec.map({ entries[this] }, { ordinal }) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/RecipeDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/RecipeDefinition.kt index 83c290ea..88e1059b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/RecipeDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/RecipeDefinition.kt @@ -3,13 +3,13 @@ package ru.dbotthepony.kstarbound.defs.actor.player import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet -import ru.dbotthepony.kstarbound.defs.ItemReference +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory data class RecipeDefinition( - val input: ImmutableList, - val output: ItemReference, + val input: ImmutableList, + val output: ItemDescriptor, val groups: ImmutableSet = ImmutableSet.of(), val matchInputParameters: Boolean = false, val duration: Double = 0.5, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/StatusControllerSettings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/StatusControllerSettings.kt deleted file mode 100644 index 93621b6c..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/actor/player/StatusControllerSettings.kt +++ /dev/null @@ -1,48 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.actor.player - -import com.google.common.collect.ImmutableList -import com.google.common.collect.ImmutableMap -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class StatusControllerSettings( - val statusProperties: Properties, - val appliesEnvironmentStatusEffects: Boolean = true, - val appliesWeatherStatusEffects: Boolean = true, - val minimumLiquidStatusEffectPercentage: Double = 0.1, - - val primaryScriptSources: ImmutableList = ImmutableList.of(), - - val primaryScriptDelta: Int, - - val stats: ImmutableMap = ImmutableMap.of(), - val resources: ImmutableMap = ImmutableMap.of(), -) { - @JsonFactory - data class Properties( - val targetMaterialKind: String, - val mouthPosition: Vector2d, - val breathHealthPenaltyPercentageRate: Double, - val hitInvulnerabilityThreshold: Double, - val hitInvulnerabilityTime: Double, - val hitInvulnerabilityFlash: Double, - val shieldHitInvulnerabilityTime: Double, - val damageFlashOnDirectives: String = "", - val damageFlashOffDirectives: String = "" - ) - - @JsonFactory - data class Stat( - val baseValue: Double - ) - - @JsonFactory - data class Resource( - val maxStat: String? = null, - val deltaStat: String? = null, - val deltaValue: Double? = null, - val initialPercentage: Double? = null, - val initialValue: Double? = null, - ) -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt new file mode 100644 index 00000000..a58ac74d --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimatedPartsDefinition.kt @@ -0,0 +1,50 @@ +package ru.dbotthepony.kstarbound.defs.animation + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory + +@JsonFactory +data class AnimatedPartsDefinition( + val stateTypes: ImmutableMap = ImmutableMap.of(), + val parts: ImmutableMap = ImmutableMap.of(), +) { + enum class AnimationMode(override val jsonName: String) : IStringSerializable { + END("end"), + TRANSITION("transition"), + LOOP("loop"); + } + + @JsonFactory + data class StateType( + val default: String = "", + val priority: Double = 0.0, + val enabled: Boolean = true, + val states: ImmutableMap = ImmutableMap.of(), + val properties: JsonObject = JsonObject(), + ) { + @JsonFactory + data class State( + val frames: Int = 1, + val cycle: Double = 1.0, + val mode: AnimationMode = AnimationMode.END, + val transition: String = "", + val properties: JsonObject = JsonObject(), + val frameProperties: JsonObject = JsonObject(), + ) + } + + @JsonFactory + data class Part( + val properties: JsonObject = JsonObject(), + val partStates: ImmutableMap> = ImmutableMap.of(), + ) { + @JsonFactory + data class State( + val properties: JsonObject = JsonObject(), + val frameProperties: JsonObject = JsonObject(), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimationDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimationDefinition.kt index 32e5abd5..0dde5b13 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimationDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/AnimationDefinition.kt @@ -2,10 +2,14 @@ package ru.dbotthepony.kstarbound.defs.animation import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABB import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.AssetPath import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.defs.particle.ParticleEmitter import ru.dbotthepony.kstarbound.json.builder.JsonFactory @JsonFactory @@ -16,79 +20,98 @@ data class AnimationDefinition( val animationCycle: Double? = null, val offset: Vector2d? = null, - val animatedParts: AnimatedParts? = null, + val animatedParts: AnimatedPartsDefinition? = null, val sounds: ImmutableMap, CustomSound>> = ImmutableMap.of(), - val transformationGroups: ImmutableMap = ImmutableMap.of(), - val particleEmitters: ImmutableMap = ImmutableMap.of(), + val transformationGroups: ImmutableMap = ImmutableMap.of(), + val rotationGroups: ImmutableMap = ImmutableMap.of(), + val particleEmitters: ImmutableMap = ImmutableMap.of(), + val lights: ImmutableMap = ImmutableMap.of(), + val effects: ImmutableMap = ImmutableMap.of(), + + val globalTagDefaults: ImmutableMap = ImmutableMap.of(), + val partTagDefaults: ImmutableMap> = ImmutableMap.of(), ) { @JsonFactory - data class CustomSound( - val pool: ImmutableList, + data class Effect( + val type: String, + val time: Double = 0.0, + val directives: String, ) @JsonFactory - data class TransformConfig( - val interpolated: Boolean? = null - ) - - @JsonFactory - data class AnimatedParts( - val stateTypes: ImmutableMap = ImmutableMap.of(), - val parts: ImmutableMap = ImmutableMap.of(), + data class AnimationParticles( + val emissionRate: Double = 1.0, + val emissionRateVariance: Double = 0.0, + val offsetRegion: AABB? = null, + val anchorPart: String? = null, + val rotationGroup: String? = null, + val rotationCenter: Vector2d? = null, + val transformationGroups: ImmutableList = ImmutableList.of(), + val particles: ImmutableList, + val burstCount: Int = 1, + val randomSelectCount: Int = particles.size, + val active: Boolean = false, ) { - @JsonFactory - data class StateType( - val default: String, - val priority: Int = 0, - val states: ImmutableMap = ImmutableMap.of(), - ) { - @JsonFactory - data class State( - val frames: Int = 0, - val cycle: Double = 0.0, - val mode: Mode? = null, - val transition: TransitionType? = null, - val properties: Properties = Properties() - ) { - enum class TransitionType { - NONE - } - - enum class Mode { - TRANSITION, - LOOP, - } - - @JsonFactory - data class Properties( - val immediateSound: String? = null - ) - } - } - - @JsonFactory - data class Part( - val properties: Properties = Properties.EMPTY, - val partStates: ImmutableMap> = ImmutableMap.of(), - ) { - @JsonFactory - data class Properties( - val fullbright: Boolean? = null, - val centered: Boolean? = null, - val transformationGroups: ImmutableList? = null, - val offset: Vector2d? = null, - val image: SpriteReference? = null - ) { - companion object { - val EMPTY = Properties() - } - } - - @JsonFactory - data class State( - val properties: Properties = Properties.EMPTY - ) + init { + require(burstCount >= 0) { "Negative burstCount: $burstCount" } + require(randomSelectCount >= 0) { "Negative randomSelectCount: $randomSelectCount" } } } + + @JsonFactory + data class AnimationParticle( + val particle: Either, ParticleConfig.JsonData>, + val count: Int = 1, + val offset: Vector2d = Vector2d.ZERO, + val flip: Boolean = false, + ) { + init { + require(count >= 0) { "Negative count: $count" } + } + } + + @JsonFactory + data class Light( + val active: Boolean = true, + val position: Vector2d = Vector2d.ZERO, + val color: RGBAColor = RGBAColor.WHITE, + val anchorPart: String? = null, + val transformationGroups: ImmutableList = ImmutableList.of(), + val rotationGroup: String? = null, + val rotationCenter: Vector2d? = null, + val pointLight: Boolean = false, + val pointBeam: Float = 0f, + val beamAmbience: Float = 0f, + val pointAngle: Double = 0.0, + + // sigh + val flickerPeriod: Double? = null, + val flickerMinIntensity: Double = 0.0, + val flickerMaxIntensity: Double = 0.0, + val flickerPeriodVariance: Double = 0.0, + val flickerIntensityVariance: Double = 0.0, + ) + + @JsonFactory + data class CustomSound( + val pool: ImmutableList = ImmutableList.of(), + val position: Vector2d = Vector2d.ZERO, + val volume: Double = 1.0, + val volumeRampTime: Double = 0.0, + val pitchMultiplier: Double = 1.0, + val pitchMultiplierRampTime: Double = 0.0, + val rangeMultiplier: Double = 1.0, + ) + + @JsonFactory + data class Rotation( + val angularVelocity: Double = 0.0, + val rotationCenter: Vector2d = Vector2d.ZERO, + ) + + @JsonFactory + data class Transform( + val interpolated: Boolean = false + ) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/DestructionAction.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/DestructionAction.kt deleted file mode 100644 index a4dcfe8b..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/DestructionAction.kt +++ /dev/null @@ -1,5 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.animation - -enum class DestructionAction { - FADE -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/Particle.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/Particle.kt new file mode 100644 index 00000000..21aa95e0 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/Particle.kt @@ -0,0 +1,186 @@ +package ru.dbotthepony.kstarbound.defs.animation + +import com.google.gson.Gson +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.fromJson +import java.util.random.RandomGenerator +import kotlin.math.PI + +// this struct is a complete mess +// mostly, because it represents all possible particle types +// and, of course, used directly when working with particles +// (instancing happens just by copying this structure) +data class Particle( + var type: ParticleConfig.Type = ParticleConfig.Type.VARIANCE, + // Defaults to 1.0, 1.0 will produce a reasonable size particle for whatever + // the type is. + var size: Double = 0.0, + var baseSize: Double = 0.0, // track the original size for shrink destruction action + var color: RGBAColor = RGBAColor.WHITE, + var light: RGBAColor = RGBAColor.TRANSPARENT_BLACK, + // Used differently depending on the type of the particle. + var string: String = "", + var fade: Double = 0.0, + var fullbright: Boolean = false, + var position: Vector2d = Vector2d.ZERO, + var velocity: Vector2d = Vector2d.ZERO, + var finalVelocity: Vector2d = Vector2d.ZERO, + var approach: Vector2d = Vector2d.ZERO, + var rotation: Double = 0.0, + var angularVelocity: Double = 0.0, + var timeToLive: Double = 0.0, + var layer: ParticleConfig.Layer = ParticleConfig.Layer.MIDDLE, + var collidesForeground: Boolean = true, + var collidesLiquid: Boolean = true, + var underwaterOnly: Boolean = false, + var ignoreWind: Boolean = true, + var length: Double = 0.0, + var destructionAction: ParticleConfig.DestructionAction = ParticleConfig.DestructionAction.NONE, + var destructionTime: Double = 0.0, + var trail: Boolean = false, + var flippable: Boolean = true, + var flip: Boolean = false, + var directives: String = "", + var destructionImage: String = "", +) { + constructor(data: ParticleConfig.JsonData) : this() { + type = data.type + + if (type == ParticleConfig.Type.VARIANCE) { + size = 0.0 + color = RGBAColor.TRANSPARENT_BLACK + } else { + size = 1.0 + color = RGBAColor.WHITE + } + + size = data.size ?: size + baseSize = size + + string = data.image?.fullPath ?: data.text ?: data.animation?.fullPath ?: data.string ?: "" + + if (data.color != null) + color = data.color + + light = data.light + fade = data.fade + fullbright = data.fullbright + position = data.position + velocity = data.velocity ?: data.initialVelocity ?: Vector2d.ZERO + + if (type == ParticleConfig.Type.VARIANCE) + finalVelocity = Vector2d.ZERO + else + finalVelocity = velocity + + finalVelocity = data.finalVelocity ?: finalVelocity + approach = data.approach + + flip = data.flip + flippable = data.flippable + rotation = data.rotation * PI / 180.0 + angularVelocity = data.angularVelocity * PI / 180.0 + length = data.length + destructionAction = data.destructionAction + + if (destructionAction == ParticleConfig.DestructionAction.IMAGE) + destructionImage = data.destructionImage?.fullPath ?: "" + else + destructionImage = data.destructionImage?.path ?: "" + + destructionTime = data.destructionTime + timeToLive = data.timeToLive + layer = data.layer + underwaterOnly = data.underwaterOnly + collidesForeground = data.collidesForeground ?: (layer != ParticleConfig.Layer.FRONT || underwaterOnly) + collidesLiquid = data.collidesLiquid ?: (type == ParticleConfig.Type.EMBER) + ignoreWind = data.ignoreWind + trail = data.trail + } + + constructor(data: JsonObject) : this(Starbound.gson.fromJson(data, ParticleConfig.JsonData::class.java)) + + fun toJson(): JsonObject { + val json = Starbound.gson.toJsonTree(toJsonData()) as JsonObject + + for (k in json.keySet().filter { json[it] == JsonNull.INSTANCE }) { + json.remove(k) + } + + return json + } + + fun toJsonData(): ParticleConfig.JsonData { + return ParticleConfig.JsonData( + type = type, + size = size, + string = string, + color = color, + light = light, + fade = fade, + fullbright = fullbright, + position = position, + velocity = velocity, + finalVelocity = finalVelocity, + approach = approach, + flip = flip, + flippable = flippable, + rotation = rotation, + angularVelocity = angularVelocity, + length = length, + destructionAction = destructionAction, + destructionImage = AssetPath(destructionImage), + destructionTime = destructionTime, + timeToLive = timeToLive, + layer = layer, + underwaterOnly = underwaterOnly, + collidesForeground = collidesForeground, + collidesLiquid = collidesLiquid, + ignoreWind = ignoreWind, + trail = trail, + ) + } + + fun applyVariance(variance: Particle, random: RandomGenerator): Particle { + size += variance.size * random.nextDouble(-1.0, 1.0) + position += Vector2d(variance.position.x * random.nextDouble(-1.0, 1.0), variance.position.y * random.nextDouble(-1.0, 1.0)) + velocity += Vector2d(variance.velocity.x * random.nextDouble(-1.0, 1.0), variance.velocity.y * random.nextDouble(-1.0, 1.0)) + finalVelocity += Vector2d(variance.finalVelocity.x * random.nextDouble(-1.0, 1.0), variance.finalVelocity.y * random.nextDouble(-1.0, 1.0)) + rotation += variance.rotation * random.nextDouble(-1.0, 1.0) + angularVelocity += variance.angularVelocity * random.nextDouble(-1.0, 1.0) + length += variance.length * random.nextDouble(-1.0, 1.0) + timeToLive += variance.timeToLive * random.nextDouble(-1.0, 1.0) + return this + } + + fun initializeAnimation() { + + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val data = gson.getAdapter(ParticleConfig.JsonData::class.java) + + override fun write(out: JsonWriter, value: Particle?) { + if (value == null) + out.nullValue() + else + data.write(out, value.toJsonData()) + } + + override fun read(`in`: JsonReader): Particle? { + if (`in`.consumeNull()) + return null + + return Particle(data.read(`in`)) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/ParticleConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/ParticleConfig.kt new file mode 100644 index 00000000..31247b39 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/animation/ParticleConfig.kt @@ -0,0 +1,98 @@ +package ru.dbotthepony.kstarbound.defs.animation + +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.defs.AssetPath +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.util.random.RandomGenerator + +interface ParticleFactory { + fun create(): Particle + fun create(random: RandomGenerator): Particle +} + +@JsonFactory +class ParticleConfig( + val kind: String? = null, + val definition: JsonData, +) : ParticleFactory { + override fun create(): Particle { + return definition.create() + } + + override fun create(random: RandomGenerator): Particle { + return definition.create(random) + } + + enum class Type(override val jsonName: String) : IStringSerializable { + VARIANCE("variance"), + EMBER("ember"), + TEXTURED("textured"), + ANIMATED("animated"), + STREAK("streak"), + TEXT("text"); + } + + enum class Layer(override val jsonName: String) : IStringSerializable { + BACK("back"), + MIDDLE("middle"), + FRONT("front"); + } + + enum class DestructionAction(override val jsonName: String) : IStringSerializable { + NONE("none"), + IMAGE("image"), + FADE("fade"), + SHRINK("shrink"); + } + + @JsonFactory + data class JsonData( + val variance: JsonData? = null, + val type: Type = Type.VARIANCE, + val size: Double? = null, + val image: AssetPath? = null, + val animation: AssetPath? = null, + val text: String? = null, + val string: String? = null, + val color: RGBAColor? = null, + val light: RGBAColor = RGBAColor.TRANSPARENT_BLACK, + val fade: Double = 0.0, + val fullbright: Boolean = false, + val position: Vector2d = Vector2d.ZERO, + val velocity: Vector2d? = null, + val initialVelocity: Vector2d? = null, + val finalVelocity: Vector2d? = null, + val approach: Vector2d = Vector2d.ZERO, + val flip: Boolean = false, + val flippable: Boolean = true, + val rotation: Double = 0.0, + val angularVelocity: Double = 0.0, + val length: Double = 10.0, + val destructionAction: DestructionAction = DestructionAction.NONE, + val destructionImage: AssetPath? = null, + val destructionTime: Double = 0.0, + val timeToLive: Double = 0.0, + val layer: Layer = Layer.MIDDLE, + val underwaterOnly: Boolean = false, + val collidesForeground: Boolean? = null, + val collidesLiquid: Boolean? = null, + val ignoreWind: Boolean = true, + val trail: Boolean = false, + ) : ParticleFactory { + private val variance2 = if (variance != null) Particle(variance) else null + private val prototype = Particle(this) + + override fun create(): Particle { + return prototype.copy() + } + + override fun create(random: RandomGenerator): Particle { + if (variance2 == null) + return prototype.copy() + + return prototype.copy().applyVariance(variance2, random) + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt index f674be08..a84d631b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/ItemDescriptor.kt @@ -1,22 +1,43 @@ package ru.dbotthepony.kstarbound.defs.item +import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.JsonSyntaxException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter import org.classdump.luna.ByteString import org.classdump.luna.LuaRuntimeException import org.classdump.luna.Table import org.classdump.luna.TableFactory +import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kstarbound.lua.StateMachine import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.toJsonObject import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.gson.value +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readVarLong +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeVarLong +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement +import java.io.DataInputStream +import java.io.DataOutputStream import java.util.function.Supplier +private val EMPTY_JSON = JsonObject() + fun ItemDescriptor(data: JsonElement): ItemDescriptor { if (data is JsonPrimitive) { return ItemDescriptor(data.asString, 1L) @@ -57,12 +78,40 @@ fun ItemDescriptor(data: Table, stateMachine: StateMachine): Supplier, count: Long, parameters: JsonObject) : this(ref.key.left(), count, parameters) + + val isEmpty get() = count <= 0L || name == "" || ref.isEmpty + val ref by lazy { Registries.items.ref(name) } + + override fun toString(): String { + return "ItemDescriptor[$name, $count, $parameters]" + } + + fun makeStack(): ItemStack { + return ItemStack.create(this) + } fun toJson(): JsonObject? { if (isEmpty) { @@ -87,4 +136,35 @@ data class ItemDescriptor( it.rawset("parameters", allocator.from(parameters)) } } + + fun write(stream: DataOutputStream) { + stream.writeBinaryString(name) + stream.writeVarLong(count.coerceAtLeast(0L)) + stream.writeJsonElement(parameters) + } + + class Adapter(gson: Gson) : TypeAdapter() { + private val elements = gson.getAdapter(JsonElement::class.java) + + override fun write(out: JsonWriter, value: ItemDescriptor?) { + if (value == null) + out.nullValue() + else if (value.isEmpty) + out.nullValue() + else + out.value(value.toJson()) + } + + override fun read(`in`: JsonReader): ItemDescriptor { + if (`in`.consumeNull()) + return EMPTY + + return ItemDescriptor(elements.read(`in`)) + } + } + + companion object { + val EMPTY = ItemDescriptor("", 0) + val CODEC = StreamCodec.Impl(::ItemDescriptor, { a, b -> b.write(a) }) + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt index 48d3b94b..c444eca3 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/item/TreasurePoolDefinition.kt @@ -14,10 +14,9 @@ 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.ItemReference import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.json.stream -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.util.WriteOnce import java.util.Random import java.util.random.RandomGenerator @@ -54,7 +53,7 @@ class TreasurePoolDefinition(pieces: List) { data class Piece( val level: Double, val pool: ImmutableList = ImmutableList.of(), - val fill: ImmutableList>> = ImmutableList.of(), + val fill: ImmutableList>> = ImmutableList.of(), val poolRounds: IPoolRounds = OneRound, // TODO: что оно делает? // оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool @@ -150,7 +149,7 @@ class TreasurePoolDefinition(pieces: List) { data class PoolEntry( val weight: Double, - val treasure: Either> + val treasure: Either> ) { init { require(weight > 0.0) { "Invalid pool entry weight: $weight" } @@ -161,7 +160,7 @@ class TreasurePoolDefinition(pieces: List) { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType === TreasurePoolDefinition::class.java) { return object : TypeAdapter() { - private val itemAdapter = gson.getAdapter(ItemReference::class.java) + private val itemAdapter = gson.getAdapter(ItemDescriptor::class.java) private val poolAdapter = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, TreasurePoolDefinition::class.java)) as TypeAdapter> private val objReader = gson.getAdapter(JsonObject::class.java) @@ -189,7 +188,7 @@ class TreasurePoolDefinition(pieces: List) { val things = objReader.read(`in`) val pool = ImmutableList.Builder() - val fill = ImmutableList.Builder>>() + val fill = ImmutableList.Builder>>() var poolRounds: IPoolRounds = OneRound val allowDuplication = things["allowDuplication"]?.asBoolean ?: false 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 37626477..917ffdd6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -15,9 +15,8 @@ import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.defs.AssetPath -import ru.dbotthepony.kstarbound.defs.ItemReference import ru.dbotthepony.kstarbound.defs.JsonReference -import ru.dbotthepony.kstarbound.defs.StatModifier +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.json.builder.JsonFactory @@ -29,6 +28,7 @@ import ru.dbotthepony.kommons.gson.contains import ru.dbotthepony.kommons.gson.get import ru.dbotthepony.kommons.gson.getArray import ru.dbotthepony.kommons.gson.set +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor data class ObjectDefinition( val objectName: String, @@ -43,9 +43,9 @@ data class ObjectDefinition( val retainObjectParametersInItem: Boolean = false, val breakDropPool: Registry.Ref? = null, // null - not specified, empty list - always drop nothing - val breakDropOptions: ImmutableList>? = null, + val breakDropOptions: ImmutableList>? = null, val smashDropPool: Registry.Ref? = null, - val smashDropOptions: ImmutableList> = ImmutableList.of(), + val smashDropOptions: ImmutableList> = ImmutableList.of(), //val animation: AssetReference? = null, val animation: AssetPath? = null, val smashSounds: ImmutableSet = ImmutableSet.of(), @@ -96,9 +96,9 @@ data class ObjectDefinition( val retainObjectParametersInItem: Boolean = false, val breakDropPool: Registry.Ref? = null, // null - not specified, empty list - always drop nothing - val breakDropOptions: ImmutableList>? = null, + val breakDropOptions: ImmutableList>? = null, val smashDropPool: Registry.Ref? = null, - val smashDropOptions: ImmutableList> = ImmutableList.of(), + val smashDropOptions: ImmutableList> = ImmutableList.of(), //val animation: AssetReference? = null, val animation: AssetPath? = null, val smashSounds: ImmutableSet = ImmutableSet.of(), diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleConfig.kt deleted file mode 100644 index df1abea8..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleConfig.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kstarbound.defs.animation.DestructionAction -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation -import ru.dbotthepony.kstarbound.util.VirtualProperty -import ru.dbotthepony.kstarbound.util.SBPattern - -@JsonImplementation(ParticleConfig::class) -interface IParticleConfig : IParticleVariance { - val finalVelocity: Vector2d? - val destructionAction: DestructionAction? - val destructionTime: Double? - val fade: Double? - val layer: ParticleLayer? - val timeToLive: Double? - val variance: IParticleVariance? - val text: SBPattern? - - companion object { - fun chain(vararg particles: IParticleConfig): IParticleConfig { - val chain = IParticleVariance.chain(*particles) - @Suppress("name_shadowing") - val particles = ImmutableList.copyOf(particles) - - return object : IParticleConfig, IParticleVariance by chain { - override val finalVelocity by VirtualProperty(IParticleConfig::finalVelocity, particles) - override val destructionAction by VirtualProperty(IParticleConfig::destructionAction, particles) - override val destructionTime by VirtualProperty(IParticleConfig::destructionTime, particles) - override val fade by VirtualProperty(IParticleConfig::fade, particles) - override val layer by VirtualProperty(IParticleConfig::layer, particles) - override val timeToLive by VirtualProperty(IParticleConfig::timeToLive, particles) - override val variance by VirtualProperty(IParticleConfig::variance, particles) - override val text by VirtualProperty(IParticleConfig::text, particles) - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleVariance.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleVariance.kt deleted file mode 100644 index e4234687..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/IParticleVariance.kt +++ /dev/null @@ -1,38 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kommons.vector.Vector4d -import ru.dbotthepony.kstarbound.json.builder.JsonImplementation -import ru.dbotthepony.kstarbound.util.VirtualProperty - -@JsonImplementation(ParticleVariance::class) -interface IParticleVariance { - val offset: Vector2d? - val position: Vector2d? - val offsetRegion: Vector4d? - val initialVelocity: Vector2d? - val approach: Vector2d? - val angularVelocity: Double? - val size: Double? - val color: RGBAColor? - - companion object { - fun chain(vararg particles: IParticleVariance): IParticleVariance { - @Suppress("name_shadowing") - val particles = ImmutableList.copyOf(particles) - - return object : IParticleVariance { - override val offset by VirtualProperty(IParticleVariance::offset, particles) - override val position by VirtualProperty(IParticleVariance::position, particles) - override val offsetRegion by VirtualProperty(IParticleVariance::offsetRegion, particles) - override val initialVelocity by VirtualProperty(IParticleVariance::initialVelocity, particles) - override val approach by VirtualProperty(IParticleVariance::approach, particles) - override val angularVelocity by VirtualProperty(IParticleVariance::angularVelocity, particles) - override val size by VirtualProperty(IParticleVariance::size, particles) - override val color by VirtualProperty(IParticleVariance::color, particles) - } - } - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleConfig.kt deleted file mode 100644 index 20656a45..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleConfig.kt +++ /dev/null @@ -1,34 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kommons.vector.Vector4d -import ru.dbotthepony.kstarbound.defs.AssetReference -import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition -import ru.dbotthepony.kstarbound.defs.animation.DestructionAction -import ru.dbotthepony.kstarbound.defs.image.SpriteReference -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.util.SBPattern - -@JsonFactory -data class ParticleConfig( - val type: ParticleType, - val animation: AssetReference? = null, - val image: SpriteReference? = null, - override val offset: Vector2d? = null, - override val position: Vector2d? = null, - override val offsetRegion: Vector4d? = null, - override val initialVelocity: Vector2d? = null, - override val finalVelocity: Vector2d? = null, - override val approach: Vector2d? = null, - override val angularVelocity: Double? = null, - override val destructionAction: DestructionAction? = null, - override val destructionTime: Double? = null, - override val fade: Double? = null, - override val size: Double? = null, - override val layer: ParticleLayer? = null, - override val timeToLive: Double? = null, - override val variance: IParticleVariance? = null, - override val color: RGBAColor? = null, - override val text: SBPattern? = null, -) : IParticleConfig diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt deleted file mode 100644 index 004a2268..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleCreator.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import ru.dbotthepony.kommons.util.Either -import ru.dbotthepony.kstarbound.Registry -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class ParticleCreator( - val count: Int = 1, - val particle: Either, IParticleConfig>, - - //override val offset: Vector2d? = null, - //override val position: Vector2d? = null, - //override val offsetRegion: Vector4d? = null, - //override val initialVelocity: Vector2d? = null, - //override val finalVelocity: Vector2d? = null, - //override val approach: Vector2d? = null, - //override val angularVelocity: Double? = null, - //override val destructionAction: DestructionAction? = null, - //override val destructionTime: Double? = null, - //override val fade: Double? = null, - //override val size: Double? = null, - //override val layer: ParticleLayer? = null, - //override val timeToLive: Double? = null, - //override val variance: IParticleVariance? = null, - //override val color: Color? = null, -) //: IParticleConfig diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleDefinition.kt deleted file mode 100644 index e55a3999..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleDefinition.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class ParticleDefinition( - val kind: String, - val definition: IParticleConfig -) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleEmitter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleEmitter.kt deleted file mode 100644 index aae6d37d..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleEmitter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import com.google.common.collect.ImmutableList -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kommons.vector.Vector4d -import ru.dbotthepony.kstarbound.defs.animation.DestructionAction -import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.util.SBPattern - -@JsonFactory -data class ParticleEmitter( - val enabled: Boolean = true, - val emissionRate: Double = 1.0, - val count: Int = 1, - val transformationGroups: ImmutableList = ImmutableList.of(), - val particles: ImmutableList, - - override val offset: Vector2d? = null, - override val position: Vector2d? = null, - override val offsetRegion: Vector4d? = null, - override val initialVelocity: Vector2d? = null, - override val finalVelocity: Vector2d? = null, - override val approach: Vector2d? = null, - override val angularVelocity: Double? = null, - override val destructionAction: DestructionAction? = null, - override val destructionTime: Double? = null, - override val fade: Double? = null, - override val size: Double? = null, - override val layer: ParticleLayer? = null, - override val timeToLive: Double? = null, - override val variance: IParticleVariance? = null, - override val color: RGBAColor? = null, - override val text: SBPattern? = null, -) : IParticleConfig diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleLayer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleLayer.kt deleted file mode 100644 index a5560579..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleLayer.kt +++ /dev/null @@ -1,5 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -enum class ParticleLayer { - FRONT -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleType.kt deleted file mode 100644 index ebe46927..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleType.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -enum class ParticleType { - ANIMATED, - TEXTURED, - EMBER, - TEXT -} \ No newline at end of file diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleVariance.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleVariance.kt deleted file mode 100644 index bbd8cbee..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/particle/ParticleVariance.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.particle - -import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.vector.Vector2d -import ru.dbotthepony.kommons.vector.Vector4d -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class ParticleVariance( - override val offset: Vector2d? = null, - override val position: Vector2d? = null, - override val offsetRegion: Vector4d? = null, - override val initialVelocity: Vector2d? = null, - override val approach: Vector2d? = null, - override val angularVelocity: Double? = null, - override val size: Double? = null, - override val color: RGBAColor? = null, -) : IParticleVariance diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/util/JsonFlattener.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/util/JsonFlattener.kt index 57360f64..8f0cef26 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/util/JsonFlattener.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/util/JsonFlattener.kt @@ -34,7 +34,7 @@ private fun flattenJsonArray(input: JsonArray, interner: Interner = Inte } private fun flattenJsonObject(input: JsonObject, interner: Interner = Interner { it }): MutableMap { - val flattened = Object2ObjectOpenHashMap() + val flattened = HashMap() for ((k, v) in input.entrySet()) { when (v) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt index 550c8a1e..6e41d1b0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/world/Parallax.kt @@ -7,7 +7,7 @@ import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.defs.image.Image import ru.dbotthepony.kstarbound.json.builder.JsonFactory -import ru.dbotthepony.kstarbound.util.RenderDirectives +import ru.dbotthepony.kstarbound.util.Directives import ru.dbotthepony.kstarbound.util.random.nextRange import java.util.random.RandomGenerator @@ -128,7 +128,7 @@ class Parallax( lightMapped = lightMapped, fadePercent = fadePercent, alpha = 1.0, - directives = RenderDirectives(directives), + directives = Directives(directives), textures = ImmutableList.copyOf(textures), ) } @@ -136,7 +136,7 @@ class Parallax( @JsonFactory data class Layer( - var directives: RenderDirectives, + var directives: Directives, val textures: ImmutableList, val alpha: Double, val parallaxValue: Vector2d, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt index 3dea7210..fa88a155 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/Streams.kt @@ -8,11 +8,17 @@ import ru.dbotthepony.kommons.io.readDouble import ru.dbotthepony.kommons.io.readFloat import ru.dbotthepony.kommons.io.readLong import ru.dbotthepony.kommons.io.readSignedVarInt +import ru.dbotthepony.kommons.io.readVector2d +import ru.dbotthepony.kommons.io.readVector2f import ru.dbotthepony.kommons.io.writeDouble import ru.dbotthepony.kommons.io.writeFloat import ru.dbotthepony.kommons.io.writeLong import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.io.writeStruct2d +import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound @@ -58,3 +64,50 @@ fun InputStream.readColor(): RGBAColor { } fun InputStream.readInternedString(): String = Starbound.STRINGS.intern(readBinaryString()) + +fun InputStream.readAABBLegacy(): AABB { + val mins = readVector2f() + val maxs = readVector2f() + return AABB(mins.toDoubleVector(), maxs.toDoubleVector()) +} + +fun InputStream.readAABBLegacyOptional(): KOptional { + val mins = readVector2f() + val maxs = readVector2f() + + // what the fuck are you doing?! + if ( + mins.x == Float.MAX_VALUE && mins.y == Float.MAX_VALUE && + maxs.x == -Float.MAX_VALUE && maxs.y == -Float.MAX_VALUE + ) { + return KOptional() + } + + return KOptional(AABB(mins.toDoubleVector(), maxs.toDoubleVector())) +} + +fun InputStream.readAABB(): AABB { + return AABB(readVector2d(), readVector2d()) +} + +fun OutputStream.writeAABBLegacy(value: AABB) { + writeStruct2f(value.mins.toFloatVector()) + writeStruct2f(value.maxs.toFloatVector()) +} + +fun OutputStream.writeAABBLegacyOptional(value: KOptional) { + value.ifPresent { + writeStruct2f(it.mins.toFloatVector()) + writeStruct2f(it.maxs.toFloatVector()) + }.ifNotPresent { + writeFloat(Float.MAX_VALUE) + writeFloat(Float.MAX_VALUE) + writeFloat(Float.MIN_VALUE) + writeFloat(Float.MIN_VALUE) + } +} + +fun OutputStream.writeAABB(value: AABB) { + writeStruct2d(value.mins) + writeStruct2d(value.maxs) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt new file mode 100644 index 00000000..4b5108d8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/Container.kt @@ -0,0 +1,13 @@ +package ru.dbotthepony.kstarbound.item + +open class Container(final override val size: Int) : IContainer { + private val slots = Array(size) { ItemStack.EMPTY } + + override fun get(index: Int): ItemStack { + return slots[index] + } + + override fun set(index: Int, value: ItemStack) { + slots[index] = value + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt new file mode 100644 index 00000000..b6e698bd --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/IContainer.kt @@ -0,0 +1,7 @@ +package ru.dbotthepony.kstarbound.item + +interface IContainer { + val size: Int + operator fun get(index: Int): ItemStack + operator fun set(index: Int, value: ItemStack) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt similarity index 50% rename from src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt index f787dbd9..4417a275 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/ItemStack.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/item/ItemStack.kt @@ -1,5 +1,6 @@ -package ru.dbotthepony.kstarbound.util +package ru.dbotthepony.kstarbound.item +import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import com.google.gson.TypeAdapter @@ -13,34 +14,59 @@ import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition import ru.dbotthepony.kommons.gson.consumeNull +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeVarLong +import ru.dbotthepony.kstarbound.Registries +import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.lua.from +import java.io.DataOutputStream +import java.util.concurrent.atomic.AtomicLong -class ItemStack private constructor(item: Registry.Entry?, count: Long, val parameters: JsonObject, marker: Unit) { - constructor(item: Registry.Entry, count: Long = 1L, parameters: JsonObject = JsonObject()) : this(item, count, parameters, Unit) +/** + * Base class for instanced items in game + */ +open class ItemStack { + constructor() { + this.config = Registries.items.emptyRef + this.parameters = JsonObject() + } - var item: Registry.Entry? = item + constructor(descriptor: ItemDescriptor) { + this.config = Registries.items.ref(descriptor.name) + this.count = descriptor.count + this.parameters = descriptor.parameters.deepCopy() + } + + /** + * unique number utilized to determine whenever stack has changed + */ + var changeset: Long = CHANGESET.incrementAndGet() private set - var count = count + /** + * it uses global atomic long to guarantee stacks having different + * changesets throughout entire lifetime of game + */ + protected fun bumpVersion() { + changeset = CHANGESET.incrementAndGet() + } + + var count: Long = 0L set(value) { - if (field == 0L || item == null) - return - field = value.coerceAtLeast(0L) - - if (field == 0L) { - item = null - } } + val config: Registry.Ref + val parameters: JsonObject + val isEmpty: Boolean - get() = count <= 0 || item == null + get() = count <= 0 || config.isEmpty val isNotEmpty: Boolean - get() = count > 0 && item != null + get() = count > 0 && config.isPresent val maxStackSize: Long - get() = item?.value?.maxStack ?: 0L + get() = config.value?.maxStack ?: 0L fun grow(amount: Long) { count += amount @@ -52,9 +78,22 @@ class ItemStack private constructor(item: Registry.Entry?, coun fun createDescriptor(): ItemDescriptor { if (isEmpty) - return ItemDescriptor("", 0L, JsonObject()) + return ItemDescriptor.EMPTY - return ItemDescriptor(item!!.key, count, parameters.deepCopy()) + return ItemDescriptor(config.key.left(), count, parameters.deepCopy()) + } + + // faster than creating an item descriptor and writing it (because it avoids copying and allocation) + fun write(stream: DataOutputStream) { + if (isEmpty) { + stream.writeBinaryString("") + stream.writeVarLong(0L) + stream.writeJsonElement(JsonNull.INSTANCE) + } else { + stream.writeBinaryString(config.key.left()) + stream.writeVarLong(count) + stream.writeJsonElement(parameters) + } } /** @@ -70,7 +109,7 @@ class ItemStack private constructor(item: Registry.Entry?, coun fun mergeFrom(other: ItemStack, simulate: Boolean) { if (isStackable(other)) { - val newCount = (count + other.count).coerceAtMost(item!!.value.maxStack) + val newCount = (count + other.count).coerceAtMost(maxStackSize) val diff = newCount - count other.count -= diff @@ -86,11 +125,14 @@ class ItemStack private constructor(item: Registry.Entry?, coun if (isEmpty) return other.isEmpty - return other.count == count && other.item == item + return other.count == count && other.config == config } fun isStackable(other: ItemStack): Boolean { - return count != 0L && other.count != 0L && item!!.value.maxStack < count && other.item == item && other.parameters == parameters + if (isEmpty || other.isEmpty) + return false + + return count != 0L && other.count != 0L && maxStackSize < count && other.config == config && other.parameters == parameters } override fun equals(other: Any?): Boolean { @@ -100,35 +142,33 @@ class ItemStack private constructor(item: Registry.Entry?, coun if (isEmpty) return other.isEmpty - return other.count == count && other.item == item && other.parameters == parameters + return other.count == count && other.config == config && other.parameters == parameters } - // мы не можем делать хеш из count и parameters так как они изменяемы override fun hashCode(): Int { - return item.hashCode() + return config.hashCode() } override fun toString(): String { if (isEmpty) - return "ItemDescriptor.EMPTY" + return "ItemStack.EMPTY" - return "ItemDescriptor[${item!!.value.itemName}, count = $count, params = $parameters]" + return "ItemDescriptor[${config.value?.itemName}, count = $count, params = $parameters]" } fun copy(): ItemStack { if (isEmpty) return this - return ItemStack(item, count, parameters.deepCopy(), Unit) + return ItemStack(ItemDescriptor(config, count, parameters.deepCopy())) } fun toJson(): JsonObject? { - if (isEmpty) { + if (isEmpty) return null - } return JsonObject().also { - it.add("name", JsonPrimitive(item!!.key)) + it.add("name", JsonPrimitive(config.key.left())) it.add("count", JsonPrimitive(count)) it.add("parameters", parameters.deepCopy()) } @@ -140,7 +180,7 @@ class ItemStack private constructor(item: Registry.Entry?, coun } return allocator.newTable(0, 3).also { - it.rawset("name", item!!.key) + it.rawset("name", config.key.left()) it.rawset("count", count) it.rawset("parameters", allocator.from(parameters)) } @@ -165,7 +205,13 @@ class ItemStack private constructor(item: Registry.Entry?, coun } companion object { + private val CHANGESET = AtomicLong() + @JvmField - val EMPTY = ItemStack(null, 0L, JsonObject(), Unit) + val EMPTY = ItemStack() + + fun create(descriptor: ItemDescriptor): ItemStack { + return EMPTY + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt index 8aca7ceb..e2527fcb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/BinaryJsonReader.kt @@ -24,8 +24,9 @@ import java.util.LinkedList * Позволяет читать двоичный JSON прямиком в [JsonElement] */ fun DataInputStream.readJsonElement(): JsonElement { - return when (val id = read()) { - BinaryJsonReader.TYPE_INVALID, BinaryJsonReader.TYPE_NULL -> JsonNull.INSTANCE + return when (val id = readUnsignedByte()) { + BinaryJsonReader.TYPE_INVALID -> throw JsonParseException("Tried to read TYPE_INVALID from stream") + BinaryJsonReader.TYPE_NULL -> JsonNull.INSTANCE BinaryJsonReader.TYPE_DOUBLE -> JsonPrimitive(readDouble()) BinaryJsonReader.TYPE_BOOLEAN -> InternedJsonElementAdapter.of(readBoolean()) BinaryJsonReader.TYPE_INT -> JsonPrimitive(readSignedVarLong()) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index 9b490dc0..f15382d6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -96,7 +96,7 @@ operator fun Table.iterator(): Iterator> { fun Table.toJson(): JsonElement { val arrayValues = Long2ObjectAVLTreeMap() - val hashValues = Object2ObjectOpenHashMap() + val hashValues = HashMap() for ((k, v) in this) { if (k is Number) { @@ -150,14 +150,8 @@ fun Table.toJson(): JsonElement { } fun Table.toJsonObject(): JsonObject { - val hashValues = Object2ObjectOpenHashMap() - - for ((k, v) in this) { - hashValues[k] = v - } - return JsonObject().also { - for ((k, v) in hashValues) { + for ((k, v) in this) { if (v is ByteString) { it.add(k.toString(), JsonPrimitive(v.decode())) } else if (v is Number) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index b7ed0205..d3fd186c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -11,14 +11,13 @@ import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement -import ru.dbotthepony.kstarbound.network.syncher.GroupElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.player.Avatar import ru.dbotthepony.kstarbound.server.ServerChannels -import ru.dbotthepony.kstarbound.world.entities.PlayerEntity +import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import java.io.Closeable -import java.util.* import kotlin.properties.Delegates abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : ChannelInboundHandlerAdapter(), Closeable { @@ -154,10 +153,9 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : // holy shit val clientPresenceEntities = BasicNetworkedElement(IntAVLTreeSet(), StreamCodec.Collection(VarIntValueCodec) { IntAVLTreeSet() }) - val clientStateGroup = MasterElement(GroupElement(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientPresenceEntities)) + val clientStateGroup = MasterElement(NetworkedGroup(windowXMin, windowYMin, windowWidth, windowHeight, playerID, clientPresenceEntities)) companion object { - private val EMPTY_UUID = UUID(0L, 0L) private val LOGGER = LogManager.getLogger() val NIO_POOL by lazy { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt index 93b32141..18d447c0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/JsonRPC.kt @@ -79,7 +79,7 @@ class JsonRPC { private val pendingWrite = ArrayList() private val responses = Int2ObjectOpenHashMap>() private val lock = ReentrantLock() - private val handlers = Object2ObjectOpenHashMap() + private val handlers = HashMap() fun add(name: String, callback: Callback): (JsonElement) -> CompletableFuture { val old = handlers.put(name, callback) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 84b16985..37642253 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -184,15 +184,13 @@ class PacketRegistry(val isLegacy: Boolean) { // separate headers for each // Due to nature of netty pipeline, we can't do the same on native protocol; // so don't do that when on native protocol - while (stream.available() > 0 || !isLegacy) { + do { try { ctx.fireChannelRead(readingType!!.factory.read(DataInputStream(stream), isLegacy, side)) } catch (err: Throwable) { LOGGER.error("Error while reading incoming packet from network (type ${readingType!!.id}; ${readingType!!.type})", err) } - - if (!isLegacy) break - } + } while (stream.available() > 0 && isLegacy) stream.close() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt index a0d4848a..fb829f89 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityCreatePacket.kt @@ -1,5 +1,6 @@ package ru.dbotthepony.kstarbound.network.packets +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readSignedVarInt @@ -10,8 +11,10 @@ import ru.dbotthepony.kstarbound.defs.EntityType import ru.dbotthepony.kstarbound.network.IClientPacket import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import java.io.DataInputStream import java.io.DataOutputStream +import java.io.File class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArray, val firstNetState: ByteArray, val entityID: Int) : IServerPacket, IClientPacket { constructor(stream: DataInputStream, isLegacy: Boolean) : this( @@ -32,13 +35,22 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArray, v if (entityID !in connection.entityIDRange) { LOGGER.error("Player $connection tried to create entity $entityType with ID $entityID, but that's outside of allowed range ${connection.entityIDRange}!") } else { - connection.enqueue { + val entity = when (entityType) { + EntityType.PLAYER -> { + val player = PlayerEntity(DataInputStream(FastByteArrayInputStream(storeData)), connection.isLegacy) + player.networkGroup.read(firstNetState, isLegacy = connection.isLegacy) + player + } + else -> null + } + + entity?.entityID = entityID + + connection.enqueue { + entity?.joinWorld(this) } } - - println(entityType) - println(entityID) } override fun play(connection: ClientConnection) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt index 25268963..29ac8455 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/EntityUpdateSetPacket.kt @@ -1,7 +1,9 @@ package ru.dbotthepony.kstarbound.network.packets +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import it.unimi.dsi.fastutil.ints.Int2ObjectMap +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readSignedVarInt import ru.dbotthepony.kommons.io.readVarInt @@ -27,7 +29,15 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap(private var value: TYPE, val codec: StreamCodec, val legacyCodec: StreamCodec, val toLegacy: (TYPE) -> LEGACY, val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate { +open class BasicNetworkedElement(private var value: TYPE, protected val codec: StreamCodec, protected val legacyCodec: StreamCodec, protected val toLegacy: (TYPE) -> LEGACY, protected val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate { protected val valueListeners = Listenable.Impl() protected val queue = LinkedList>() protected var isInterpolating = false protected var currentTime = 0.0 + override fun toString(): String { + return "BasicNetworkedElement[$value, isInterpolating=$isInterpolating, currentTime=$currentTime]" + } + override fun accept(t: TYPE) { if (t != value) { value = t diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt index 26b3654b..43642c1c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/Factories.kt @@ -1,8 +1,12 @@ package ru.dbotthepony.kstarbound.network.syncher +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject import com.google.gson.TypeAdapter import ru.dbotthepony.kommons.io.BinaryStringCodec import ru.dbotthepony.kommons.io.BooleanValueCodec +import ru.dbotthepony.kommons.io.RGBACodec import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.UnsignedVarIntCodec import ru.dbotthepony.kommons.io.UnsignedVarLongCodec @@ -10,16 +14,34 @@ import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.kommons.io.VarLongValueCodec import ru.dbotthepony.kommons.io.Vector2dCodec import ru.dbotthepony.kommons.io.Vector2fCodec +import ru.dbotthepony.kommons.io.koptional import ru.dbotthepony.kommons.io.readByteArray import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.readVarLong +import ru.dbotthepony.kommons.io.writeBinaryString import ru.dbotthepony.kommons.io.writeByteArray import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kommons.io.writeVarLong +import ru.dbotthepony.kommons.math.RGBAColor +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.defs.world.SkyType +import ru.dbotthepony.kstarbound.io.readAABB +import ru.dbotthepony.kstarbound.io.readAABBLegacy +import ru.dbotthepony.kstarbound.io.readAABBLegacyOptional +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.io.writeAABB +import ru.dbotthepony.kstarbound.io.writeAABBLegacy +import ru.dbotthepony.kstarbound.io.writeAABBLegacyOptional +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.readJsonObject +import ru.dbotthepony.kstarbound.json.writeJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonObject import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.DataInputStream import java.io.DataOutputStream @@ -31,6 +53,19 @@ fun BasicNetworkedElement(value: TYPE, codec: StreamCodec): BasicNe } val ByteArrayCodec = StreamCodec.Impl(DataInputStream::readByteArray, DataOutputStream::writeByteArray) +val InternedStringCodec = StreamCodec.Impl(DataInputStream::readInternedString, DataOutputStream::writeBinaryString) + +val AABBCodecLegacy = StreamCodec.Impl(DataInputStream::readAABBLegacy, DataOutputStream::writeAABBLegacy) +val AABBCodecLegacyOptional = StreamCodec.Impl(DataInputStream::readAABBLegacyOptional, DataOutputStream::writeAABBLegacyOptional) +val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB) + +val ValidatingBooleanCodec = StreamCodec.Impl({ + when (val read = it.readUnsignedByte()) { + 0 -> false + 1 -> true + else -> throw IllegalStateException("Expected to read proper boolean from stream, but incoming byte was: $read") + } +}, DataOutputStream::writeBoolean) // networking size_t... // god help us all @@ -69,17 +104,26 @@ fun networkedSignedInt(value: Int = 0) = BasicNetworkedElement(value, VarIntValu fun networkedUnsignedInt(value: Int = 0) = BasicNetworkedElement(value, UnsignedVarIntCodec) fun networkedSignedLong(value: Long = 0L) = BasicNetworkedElement(value, VarLongValueCodec) fun networkedUnsignedLong(value: Long = 0L) = BasicNetworkedElement(value, UnsignedVarLongCodec) -fun networkedBoolean(value: Boolean = false) = BasicNetworkedElement(value, BooleanValueCodec) +fun networkedBoolean(value: Boolean = false) = BasicNetworkedElement(value, ValidatingBooleanCodec) fun networkedPointer(value: Long = 0L) = BasicNetworkedElement(value, SizeTCodec) fun networkedVec2f(value: Vector2d = Vector2d.ZERO) = BasicNetworkedElement(value, Vector2dCodec, Vector2fCodec, { it.toFloatVector() }, { it.toDoubleVector() }) fun networkedBytes(value: ByteArray = ByteArray(0)) = BasicNetworkedElement(value, ByteArrayCodec) -fun > networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java)) fun networkedData(value: T, codec: StreamCodec) = BasicNetworkedElement(value, codec) fun networkedData(value: T, codec: StreamCodec, legacyCodec: StreamCodec) = BasicNetworkedElement(value, codec, legacyCodec, { it }, { it }) +fun networkedAABB(value: AABB = AABB.ZERO) = networkedData(value, AABBCodecNative, AABBCodecLegacy) +fun networkedAABBNullable(value: KOptional = KOptional()) = networkedData(value, AABBCodecNative.koptional(), AABBCodecLegacyOptional) + +// this is ugly because of invariant generics, but we must bear with it. +fun networkedList(codec: StreamCodec): BasicNetworkedElement, List> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement, List> +fun networkedItem(value: ItemStack = ItemStack.EMPTY) = NetworkedItemStack(value) +fun networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value) + fun networkedEventCounter() = EventCounterElement() -fun networkedString(value: String = "") = BasicNetworkedElement(value, BinaryStringCodec) +fun networkedSignals(codec: StreamCodec) = NetworkedSignal(codec) +fun networkedSignals(codec: StreamCodec, maxSize: Int) = NetworkedSignal(codec, maxSize) +fun networkedString(value: String = "") = BasicNetworkedElement(value, InternedStringCodec) fun networkedPoly(value: Poly) = networkedData(value, Poly.CODEC, Poly.LEGACY_CODEC) @@ -91,6 +135,11 @@ inline fun networkedJson(value: T, adapter: TypeAdapter = Starbou } } +val JsonElementCodec = StreamCodec.Impl(DataInputStream::readJsonElement, DataOutputStream::writeJsonElement) +val JsonObjectCodec = StreamCodec.Impl(DataInputStream::readJsonObject, DataOutputStream::writeJsonObject) +fun networkedJsonElement(value: JsonElement = JsonNull.INSTANCE) = networkedData(value, JsonElementCodec) +fun networkedJsonObject(value: JsonObject) = networkedData(value, JsonObjectCodec) + fun nativeCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec { return StreamCodec.Impl({ reader(it, false) }, { a, b -> writer(b, a, false) }) } @@ -99,7 +148,12 @@ fun legacyCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutpu return StreamCodec.Impl({ reader(it, true) }, { a, b -> writer(b, a, true) }) } -// networks a signed variable length integer on legacy protocol +fun networkedColor(value: RGBAColor = RGBAColor.BLACK) = networkedData(value, RGBACodec) + +// networks enum as unsigned variable length integer +fun > networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java)) + +// networks enum as a signed variable length integer on legacy protocol fun > networkedEnumStupid(value: E): BasicNetworkedElement { val codec = StreamCodec.Enum(value::class.java) return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal.shl(1) }, { codec.values[it.ushr(1)] }) @@ -108,5 +162,5 @@ fun > networkedEnumStupid(value: E): BasicNetworkedElement { // networks enum as string on legacy protocol fun > networkedEnumExtraStupid(value: E): BasicNetworkedElement { val codec = StreamCodec.Enum(value::class.java) - return BasicNetworkedElement(value, codec, BinaryStringCodec, { if (it is IStringSerializable) it.jsonName else it.name }, { s -> codec.values.firstOrNull { if (it is IStringSerializable) it.match(s) else it.name == s } ?: throw NoSuchElementException(s) }) + return BasicNetworkedElement(value, codec, InternedStringCodec, { if (it is IStringSerializable) it.jsonName else it.name }, { s -> codec.values.firstOrNull { if (it is IStringSerializable) it.match(s) else it.name == s } ?: throw NoSuchElementException(s) }) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt index 53f05284..bbc5aead 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/FloatingNetworkedElement.kt @@ -72,6 +72,10 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va var extrapolation = 0.0 private set + override fun toString(): String { + return "FloatingNetworkedElement[$value, isInterpolating=$isInterpolating, currentTime=$currentTime]" + } + private val valueListeners = Listenable.Impl() override fun accept(t: Double) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt index 2b92837a..158efef6 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/MasterElement.kt @@ -66,6 +66,10 @@ class MasterElement(val upstream: E) : LongSupplier { } } + override fun toString(): String { + return "MasterElement[$upstream]" + } + fun read(data: ByteArray, interpolationTime: Double = 0.0, isLegacy: Boolean = false) { return read(FastByteArrayInputStream(data), interpolationTime, isLegacy) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedDynamicGroup.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedDynamicGroup.kt new file mode 100644 index 00000000..d7feaf00 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedDynamicGroup.kt @@ -0,0 +1,288 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet +import it.unimi.dsi.fastutil.io.FastByteArrayInputStream +import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream +import ru.dbotthepony.kommons.io.readByteArray +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeByteArray +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.collect.IdMap +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.Collections +import java.util.function.LongSupplier + +/** + * A dynamic group of NetElements that manages creation and destruction of + * individual elements, that is itself a NetElement. Element changes are not + * delayed by the interpolation delay, they will always happen immediately, but + * this does not inhibit the Elements themselves from handling their own delta + * update delays normally. + */ +class NetworkedDynamicGroup(private val factory: () -> T, private val accessor: (T) -> NetworkedElement, private val maxBacklogSize: Int = 100) : NetworkedElement() { + private val elementsInternal = IdMap(min = 1) + val elements: Map = Collections.unmodifiableMap(elementsInternal) + private val backlog = ArrayDeque>() + private var isInterpolating = false + private var extrapolation = 0.0 + + private enum class Action { + CLEAR, REMOVE, ADD; + } + + override fun toString(): String { + return "NetworkedDynamicGroup[keys = ${elementsInternal.size}]" + } + + // Storing data seems to be redundant at first, but this actually makes sense + // since networked elements might have interpolation enabled + // + // If we would store element itself in Entry, and write it's initial state + // upon networking to remote, we would skip all interpolation queues, since + // IF element has interpolation enabled, writeInitial will ALWAYS write latest-possible + // data (data from end of interpolation queue), and this is not something desired + // (because writes of this data happens on both writeInitial and writeDelta inside this class) + // Major downside of this is keeping byte arrays around + private class Entry( + val action: Action, + val id: Int, + val dataNative: ByteArray?, + val dataLegacy: ByteArray?, + ) { + constructor(id: Int, element: NetworkedElement) : this( + Action.ADD, + id, + FastByteArrayOutputStream().let { + element.writeInitial(DataOutputStream(it), false) + it.array.copyOf(it.length) + }, + FastByteArrayOutputStream().let { + element.writeInitial(DataOutputStream(it), true) + it.array.copyOf(it.length) + }) + + constructor(id: Int) : this(Action.REMOVE, id, null, null) + } + + private fun setupElement(element: NetworkedElement) { + val versionCounter = versionCounter + + if (versionCounter != null) + element.specifyVersioner(versionCounter) + + if (isInterpolating) + element.enableInterpolation(extrapolation) + else + element.disableInterpolation() + } + + fun add(element: T): Int { + check(element !in elementsInternal.values) { "Already containing $element" } + setupElement(accessor(element)) + val id = elementsInternal.add(element) + backlog.add(currentVersion() to Entry(id, accessor(element))) + purgeBacklog() + return id + } + + fun remove(index: Int): Boolean { + return elementsInternal.remove(index) != null + } + + private fun purgeBacklog() { + while (backlog.size >= maxBacklogSize) { + backlog.removeFirst() + } + } + + override fun hasChangedSince(version: Long): Boolean { + return version == 0L || backlog.any { it.first >= version } || elementsInternal.values.any { accessor(it).hasChangedSince(version) } + } + + override fun specifyVersioner(versionCounter: LongSupplier) { + super.specifyVersioner(versionCounter) + backlog.clear() + backlog.add(currentVersion() to clearAction) + + for ((id, element) in elementsInternal) { + accessor(element).specifyVersioner(versionCounter) + backlog.add(currentVersion() to Entry(id, accessor(element))) + } + + purgeBacklog() + } + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + val size = data.readVarInt() + + backlog.clear() + elementsInternal.clear() + + backlog.add(currentVersion() to clearAction) + + for (i in 0 until size) { + val index = data.readVarInt() + + val stream = if (isLegacy) { + // sigh + DataInputStream(FastByteArrayInputStream(data.readByteArray())) + } else { + data + } + + val element = factory() + setupElement(accessor(element)) + elementsInternal[index] = element + accessor(element).readInitial(stream, isLegacy) + backlog.add(currentVersion() to Entry(index, accessor(element))) + } + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + data.writeVarInt(elementsInternal.size) + + for ((id, element) in elementsInternal) { + data.writeVarInt(id) + + if (isLegacy) { + // sigh + val wrap = FastByteArrayOutputStream() + accessor(element).writeInitial(DataOutputStream(wrap), true) + data.writeByteArray(wrap.array, 0, wrap.length) + } else { + accessor(element).writeInitial(data, false) + } + } + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + if (data.readBoolean()) { + readInitial(data, isLegacy) + } else { + val received = IntAVLTreeSet() + + while (true) { + when (val id = data.readVarInt()) { + 0 -> break + + 1 -> { + when (Action.entries[data.readUnsignedByte()]) { + Action.CLEAR -> { + elementsInternal.clear() + backlog.add(currentVersion() to clearAction) + } + + Action.REMOVE -> { + // inconsistent usage of VarInt and int32_t when networking is driving me insane + val id = if (isLegacy) data.readInt() else data.readVarInt() + backlog.add(currentVersion() to Entry(id)) + elementsInternal.remove(id) + } + + Action.ADD -> { + // inconsistent usage of VarInt and int32_t when networking is driving me insane + val id = if (isLegacy) data.readInt() else data.readVarInt() + val element = factory() + setupElement(accessor(element)) + check(id !in elementsInternal) { "Already has networked element $id" } + elementsInternal[id] = element + + if (isLegacy) { + accessor(element).readInitial(DataInputStream(FastByteArrayInputStream(data.readByteArray())), true) + } else { + accessor(element).readInitial(data, false) + } + + backlog.add(currentVersion() to Entry(id, accessor(element))) + } + } + } + + else -> { + val element = elementsInternal[id - 1] ?: throw NoSuchElementException("Unknown network element with ID ${id - 1}, net state is corrupt!") + accessor(element).readDelta(data, interpolationDelay, isLegacy) + + if (isInterpolating) { + received.add(id) + } + } + } + } + + if (isInterpolating) { + for ((id, element) in elementsInternal) { + if (!received.contains(id)) { + accessor(element).readBlankDelta(interpolationDelay) + } + } + } + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + // backlog is guaranteed to be not empty once first networked element is added + if (backlog.isNotEmpty() && remoteVersion < backlog.first().first) { + // we fell way behind, write full state + data.writeBoolean(true) + writeInitial(data, isLegacy) + return + } + + data.writeBoolean(false) + + for ((version, entry) in backlog) { + if (version >= remoteVersion) { + data.writeByte(1) + data.writeByte(entry.action.ordinal) + + when (entry.action) { + Action.CLEAR -> {} + Action.REMOVE -> if (isLegacy) data.writeInt(entry.id) else data.writeVarInt(entry.id) + + Action.ADD -> { + if (isLegacy) { + data.writeInt(entry.id) + data.writeByteArray(entry.dataLegacy!!) + } else { + data.writeVarInt(entry.id) + data.write(entry.dataNative!!) + } + } + } + } + } + + for ((id, element) in elementsInternal) { + if (accessor(element).hasChangedSince(remoteVersion)) { + data.writeVarInt(id + 1) + accessor(element).writeDelta(data, remoteVersion, isLegacy) + } + } + + data.writeByte(0) + } + + override fun readBlankDelta(interpolationDelay: Double) { + elementsInternal.values.forEach { accessor(it).readBlankDelta(interpolationDelay) } + } + + override fun enableInterpolation(extrapolation: Double) { + isInterpolating = true + this.extrapolation = extrapolation + elementsInternal.values.forEach { accessor(it).enableInterpolation(extrapolation) } + } + + override fun disableInterpolation() { + isInterpolating = false + elementsInternal.values.forEach { accessor(it).disableInterpolation() } + } + + override fun tickInterpolation(delta: Double) { + elementsInternal.values.forEach { accessor(it).tickInterpolation(delta) } + } + + companion object { + private val clearAction = Entry(Action.CLEAR, 0, null, null) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt index cc5d7c8c..0705bd09 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedElement.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.network.syncher -import ru.dbotthepony.kommons.util.Listenable import java.io.DataInputStream import java.io.DataOutputStream import java.util.function.LongSupplier @@ -66,21 +65,22 @@ abstract class NetworkedElement { // NetElementVersion. When elements are updated, they will mark the version // number at the time they are updated so that a delta can be constructed // that contains only changes since any past version. - var version: Long = 0L - protected set(value) { + protected var version: Long = 0L + set(value) { if (field != value) { require(value > field) { "Downgrading element version from $field to $value" } field = value - listeners.accept(value) } } + protected fun currentVersion(): Long { + return versionCounter?.asLong ?: version + } + open fun hasChangedSince(version: Long): Boolean { return this.version >= version } - val listeners = Listenable.Impl() - var versionCounter: LongSupplier? = null protected set @@ -94,4 +94,54 @@ abstract class NetworkedElement { open fun bumpVersion() { version = versionCounter?.asLong ?: version } + + abstract class Passthrough : NetworkedElement() { + protected abstract val parentElement: NetworkedElement + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + parentElement.readInitial(data, isLegacy) + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + parentElement.writeInitial(data, isLegacy) + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + parentElement.readDelta(data, interpolationDelay, isLegacy) + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + parentElement.writeDelta(data, remoteVersion, isLegacy) + } + + override fun readBlankDelta(interpolationDelay: Double) { + parentElement.readBlankDelta(interpolationDelay) + } + + override fun enableInterpolation(extrapolation: Double) { + parentElement.enableInterpolation(extrapolation) + } + + override fun disableInterpolation() { + parentElement.disableInterpolation() + } + + override fun tickInterpolation(delta: Double) { + parentElement.tickInterpolation(delta) + } + + override fun hasChangedSince(version: Long): Boolean { + return super.hasChangedSince(version) || parentElement.hasChangedSince(version) + } + + override fun specifyVersioner(versionCounter: LongSupplier) { + super.specifyVersioner(versionCounter) + parentElement.specifyVersioner(versionCounter) + } + + override fun bumpVersion() { + super.bumpVersion() + parentElement.bumpVersion() + } + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedGroup.kt similarity index 81% rename from src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedGroup.kt index 5588eaab..0c4cb9b2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/GroupElement.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedGroup.kt @@ -7,7 +7,7 @@ import java.io.DataOutputStream import java.util.function.Consumer import java.util.function.LongSupplier -class GroupElement() : NetworkedElement() { +class NetworkedGroup() : NetworkedElement() { constructor(element: NetworkedElement, vararg rest: NetworkedElement) : this() { add(element) rest.forEach { add(it) } @@ -21,6 +21,17 @@ class GroupElement() : NetworkedElement() { var extrapolation = 0.0 private set + fun clear() { + elements.clear() + } + + override fun toString(): String { + if (elements.isEmpty()) + return "NetworkedGroup[]" + + return "NetworkedGroup[\n${elements.joinToString("\n") { "\t" + it.first.toString() }}]" + } + override fun specifyVersioner(versionCounter: LongSupplier) { super.specifyVersioner(versionCounter) elements.forEach { it.first.specifyVersioner(versionCounter) } @@ -42,10 +53,13 @@ class GroupElement() : NetworkedElement() { elements.forEach { it.first.tickInterpolation(delta) } } - fun add(element: E, propagateInterpolation: Boolean = true): E { - require(elements.none { it.first == element }) { "Already has element $element in $this" } - elements.add(element to propagateInterpolation) + private var trap: Consumer? = null + fun putTrap(trap: Consumer) { + this.trap = trap + } + + private fun setupElement(element: NetworkedElement, propagateInterpolation: Boolean) { if (propagateInterpolation) { if (isInterpolating) element.enableInterpolation(extrapolation) @@ -56,16 +70,25 @@ class GroupElement() : NetworkedElement() { if (versionCounter != null) { element.specifyVersioner(versionCounter!!) } + } - element.listeners.addListener(Consumer { - this.version = this.version.coerceAtLeast(it) - }) - - this.version = this.version.coerceAtLeast(element.version) + fun add(element: E, propagateInterpolation: Boolean = true): E { + require(elements.none { it.first === element }) { "Already has element $element in $this" } + elements.add(element to propagateInterpolation) + setupElement(element, propagateInterpolation) return element } + fun replace(find: NetworkedElement, replace: NetworkedElement) { + if (find === replace) return + val index = elements.indexOfFirst { it.first === find } + check(index != -1) { "Unable to find $find in $this" } + setupElement(replace, elements[index].second) + elements[index] = replace to elements[index].second + } + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + trap?.accept(data) elements.forEach { it.first.readInitial(data, isLegacy) } } @@ -142,10 +165,7 @@ class GroupElement() : NetworkedElement() { } override fun hasChangedSince(version: Long): Boolean { - if (elements.isEmpty()) - return false - - return super.hasChangedSince(version) + return elements.any { it.first.hasChangedSince(version) } } override fun bumpVersion() { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedItemStack.kt new file mode 100644 index 00000000..0b2f4955 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedItemStack.kt @@ -0,0 +1,64 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import ru.dbotthepony.kommons.util.Delegate +import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor +import ru.dbotthepony.kstarbound.item.ItemStack +import java.io.DataInputStream +import java.io.DataOutputStream + +// Due to this class observing values... event-based +// "hasChangedSince" in GroupElement had to be removed :( tough shit +// Not that it matters anyway, since even more classes followed to mangle +// event driven updates due to their complexity. +// Creating more efficient system for this case will be major complication of entire system, +// and major complications with little improvement are bad. +open class NetworkedItemStack(private var itemStack: ItemStack = ItemStack.EMPTY) : NetworkedElement(), Delegate { + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + val read = ItemDescriptor(data) + itemStack = ItemStack.create(read) + observedVersion = itemStack.changeset + bumpVersion() + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + itemStack.write(data) + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + readInitial(data, isLegacy) + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + writeInitial(data, isLegacy) + } + + override fun readBlankDelta(interpolationDelay: Double) {} + override fun enableInterpolation(extrapolation: Double) {} + override fun disableInterpolation() {} + override fun tickInterpolation(delta: Double) {} + + private var observedVersion = -1L + + override fun toString(): String { + return "NetworkedItemStack[$itemStack]" + } + + override fun accept(t: ItemStack) { + itemStack = t + observedVersion = t.changeset + bumpVersion() + } + + override fun get(): ItemStack { + return itemStack + } + + override fun hasChangedSince(version: Long): Boolean { + if (observedVersion != itemStack.changeset) { + observedVersion = itemStack.changeset + bumpVersion() + } + + return super.hasChangedSince(version) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt new file mode 100644 index 00000000..e19d32c9 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedMap.kt @@ -0,0 +1,340 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import ru.dbotthepony.kommons.collect.ListenableMap +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.util.KOptional +import java.io.DataInputStream +import java.io.DataOutputStream + +/** + * [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time + * instead of deltas + * + * [keyCodec] and [valueCodec] specify codecs as `native -> legacy` pair + */ +class NetworkedMap( + private val keyCodec: Pair, StreamCodec>, + private val valueCodec: Pair, StreamCodec>, + private val isDumb: Boolean = false, + map: ListenableMap = ListenableMap(), + private val maxBacklogSize: Int = 100, +) : NetworkedElement(), MutableMap by map { + constructor( + keyCodec: StreamCodec, + valueCodec: StreamCodec, + isDumb: Boolean = false, + map: ListenableMap = ListenableMap(), + maxBacklogSize: Int = 100, + ) : this(keyCodec to keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize) + + constructor( + keyCodec: StreamCodec, + valueCodec: Pair, StreamCodec>, + isDumb: Boolean = false, + map: ListenableMap = ListenableMap(), + maxBacklogSize: Int = 100, + ) : this(keyCodec to keyCodec, valueCodec, isDumb, map, maxBacklogSize) + + constructor( + keyCodec: Pair, StreamCodec>, + valueCodec: StreamCodec, + isDumb: Boolean = false, + map: ListenableMap = ListenableMap(), + maxBacklogSize: Int = 100, + ) : this(keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize) + + init { + map.addListener(object : ListenableMap.MapListener { + override fun onClear() { + if (isReading) return + check(!isRemote) { "This map is not owned by this side" } + + // this is fragile (due to interpolation fuckery, we remove everything before applying delayed changes), + // but let's hope it doesn't break + delayed.clear() + backlog.add(currentVersion() to clearEntry) + + purgeBacklog() + } + + override fun onValueAdded(key: K, value: V) { + if (isReading) return + check(!isRemote) { "This map is not owned by this side" } + backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value)))) + purgeBacklog() + } + + override fun onValueRemoved(key: K, value: V) { + if (isReading) return + check(!isRemote) { "This map is not owned by this side" } + backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional())) + purgeBacklog() + } + }) + } + + private val dumbCodec by lazy { + StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap) + } + + private enum class Action { + ADD, REMOVE, CLEAR; + } + + private data class Entry(val action: Action, val key: KOptional, val value: KOptional) { + fun apply(map: MutableMap) { + when (action) { + Action.ADD -> map[key.value] = value.value + Action.REMOVE -> map.remove(key.value) + Action.CLEAR -> map.clear() + } + } + + fun write(data: DataOutputStream, isLegacy: Boolean, self: NetworkedMap) { + if (isLegacy) { + when (action) { + Action.ADD -> { + self.legacyKey.write(data, key.value) + self.legacyValue.write(data, value.value) + } + + Action.REMOVE -> { + self.legacyKey.write(data, key.value) + } + + Action.CLEAR -> {} + } + } else { + when (action) { + Action.ADD -> { + self.nativeKey.write(data, key.value) + self.nativeValue.write(data, value.value) + } + + Action.REMOVE -> { + self.nativeKey.write(data, key.value) + } + + Action.CLEAR -> {} + } + } + } + } + + private fun readLegacyEntry(data: DataInputStream): Entry { + return when (Action.entries[data.readUnsignedByte()]) { + Action.ADD -> Entry(Action.ADD, KOptional(legacyKey.read(data)), KOptional(legacyValue.read(data))) + Action.REMOVE -> Entry(Action.REMOVE, KOptional(legacyKey.read(data)), KOptional()) + Action.CLEAR -> clearEntry + } + } + + private fun readNativeEntry(data: DataInputStream): Entry { + return when (Action.entries[data.readUnsignedByte()]) { + Action.ADD -> Entry(Action.ADD, KOptional(nativeKey.read(data)), KOptional(nativeValue.read(data))) + Action.REMOVE -> Entry(Action.REMOVE, KOptional(nativeKey.read(data)), KOptional()) + Action.CLEAR -> clearEntry + } + } + + private val clearEntry = Entry(Action.CLEAR, KOptional(), KOptional()) + private val backlog = ArrayDeque>>() + private val delayed = ArrayDeque>>() + private var currentTime = 0.0 + private var isReading = false + private var isRemote = false + private var isInterpolating = false + + val legacyKey get() = keyCodec.second + val nativeKey get() = keyCodec.first + val legacyValue get() = valueCodec.second + val nativeValue get() = valueCodec.first + + private fun purgeBacklog() { + while (backlog.size >= maxBacklogSize) { + backlog.removeFirst() + } + } + + override fun toString(): String { + return "NetworkedMap[keys = ${keys.size}]" + } + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + try { + isRemote = true + isReading = true + + backlog.clear() + delayed.clear() + clear() + + backlog.add(currentVersion() to clearEntry) + + if (isDumb && isLegacy) { + val readMap = dumbCodec.read(data) + + for ((k, v) in readMap) { + val action = Entry(Action.ADD, KOptional(k), KOptional(v)) + backlog.add(currentVersion() to action) + action.apply(this) + } + } else { + val values = data.readVarInt() + + if (isLegacy) { + for (i in 0 until values) { + val action = readLegacyEntry(data) + backlog.add(currentVersion() to action) + action.apply(this) + } + } else { + for (i in 0 until values) { + val action = readNativeEntry(data) + backlog.add(currentVersion() to action) + action.apply(this) + } + } + } + + purgeBacklog() + } finally { + isReading = false + } + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + if (isDumb && isLegacy) { + val construct = HashMap(size + delayed.size) + + for ((k, v) in entries) { + construct[k] = v + } + + for ((_, v) in delayed) { + v.apply(construct) + } + + dumbCodec.write(data, construct) + } else { + data.writeVarInt(size + delayed.size) + + for ((k, v) in entries) { + Entry(Action.ADD, KOptional(k), KOptional(v)).write(data, isLegacy, this) + } + + for ((_, v) in delayed) { + v.write(data, isLegacy, this) + } + } + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + if (isDumb && isLegacy) { + readInitial(data, true) + return + } + + try { + isReading = true + + while (true) { + when (val action = data.readUnsignedByte()) { + 0 -> break + 1 -> { + readInitial(data, isLegacy) + isReading = true + } + 2 -> { + val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data) + backlog.add(currentVersion() to change) + + if (isInterpolating && interpolationDelay > 0.0) { + val actualDelay = interpolationDelay + currentTime + + if (delayed.isNotEmpty() && delayed.last().first > actualDelay) { + delayed.forEach { it.second.apply(this) } + delayed.clear() + } + + delayed.add(actualDelay to change) + } else { + change.apply(this) + } + } + + else -> throw IllegalArgumentException("Invalid delta change type $action") + } + } + + purgeBacklog() + } finally { + isReading = false + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + if (isDumb && isLegacy) { + writeInitial(data, true) + return + } + + if (remoteVersion < backlog.first().first) { + // we fell way behind, serialize entire state + data.writeByte(1) + writeInitial(data, isLegacy) + data.writeByte(0) + } else { + for ((version, entry) in backlog) { + if (version >= remoteVersion) { + data.writeByte(2) + entry.write(data, isLegacy, this) + } + } + + data.writeByte(0) + } + } + + override fun hasChangedSince(version: Long): Boolean { + return backlog.any { it.first >= version } + } + + override fun readBlankDelta(interpolationDelay: Double) {} + + override fun enableInterpolation(extrapolation: Double) { + isInterpolating = true + } + + override fun disableInterpolation() { + if (isInterpolating) { + isInterpolating = false + + try { + isReading = true + delayed.forEach { it.second.apply(this) } + delayed.clear() + } finally { + isReading = false + } + + purgeBacklog() + } + } + + override fun tickInterpolation(delta: Double) { + currentTime += delta + + try { + isReading = true + + while (delayed.isNotEmpty() && delayed.first().first <= currentTime) { + delayed.removeFirst().second.apply(this) + } + } finally { + isReading = false + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt new file mode 100644 index 00000000..80d52e80 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedSignal.kt @@ -0,0 +1,125 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kommons.util.KOptional +import java.io.DataInputStream +import java.io.DataOutputStream + +/** + * NetElement that sends signals during delta writes that can be received by + * slaves. It has no 'state', and nothing is sent during a store / load, and + * it only keeps past signals for a maximum number of versions. Thus, it is + * not appropriate to use to send updates to long term states, only for event + * like things that are not harmful if missed. + */ +class NetworkedSignal(private val codec: StreamCodec, private val maxSize: Int = 100) : NetworkedElement(), Iterable, Iterator { + private data class Signal(val version: Long, val signal: S) + + private var currentTime = 0.0 + private val visibleSignals = ArrayDeque>() + private val internalSignals = ArrayDeque>() + private val delayedSignals = ArrayDeque>() + private var isInterpolating = false + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) {} + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {} + override fun readBlankDelta(interpolationDelay: Double) {} + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + val toRead = data.readVarInt() + + for (i in 0 until toRead) { + val signal = codec.read(data) + + if (isInterpolating && interpolationDelay > 0.0) { + val actualDelay = interpolationDelay + currentTime + + if (delayedSignals.isNotEmpty() && delayedSignals.last().first > actualDelay) { + delayedSignals.forEach { push(it.second) } + delayedSignals.clear() + } + + delayedSignals.add(actualDelay to signal) + } else { + push(signal) + } + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + data.writeVarInt(internalSignals.count { it.version >= remoteVersion }) + + for ((version, signal) in internalSignals) { + if (version >= remoteVersion) { + codec.write(data, signal) + } + } + } + + override fun hasChangedSince(version: Long): Boolean { + return internalSignals.any { it.version >= version } + } + + override fun enableInterpolation(extrapolation: Double) { + isInterpolating = true + } + + override fun disableInterpolation() { + isInterpolating = false + + for ((_, v) in delayedSignals) { + push(v) + } + + delayedSignals.clear() + } + + override fun tickInterpolation(delta: Double) { + currentTime += delta + + while (delayedSignals.isNotEmpty() && delayedSignals.first().first <= currentTime) { + push(delayedSignals.removeFirst().second) + } + } + + val isEmpty: Boolean + get() = visibleSignals.isEmpty() + + val isNotEmpty: Boolean + get() = visibleSignals.isNotEmpty() + + fun push(signal: S) { + val value = Signal(currentVersion(), signal) + visibleSignals.add(value) + internalSignals.add(value) + + while (visibleSignals.size >= maxSize) { + visibleSignals.removeFirst() + } + + while (internalSignals.size >= maxSize) { + internalSignals.removeFirst() + } + } + + fun poll(): KOptional { + if (visibleSignals.isEmpty()) + return KOptional() + + return KOptional(visibleSignals.removeFirst().signal) + } + + override fun iterator(): Iterator { + return this + } + + override fun hasNext(): Boolean { + return visibleSignals.isNotEmpty() + } + + override fun next(): S { + return visibleSignals.removeFirst().signal + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt new file mode 100644 index 00000000..17aa5efb --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedStatefulItemStack.kt @@ -0,0 +1,150 @@ +package ru.dbotthepony.kstarbound.network.syncher + +import ru.dbotthepony.kstarbound.item.ItemStack +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.function.LongSupplier + +class NetworkedStatefulItemStack(value: ItemStack = ItemStack.EMPTY) : NetworkedItemStack(value) { + interface Stateful { + val networkElement: NetworkedElement + } + + private var isInterpolating = false + private var extrapolation = 0.0 + + override fun enableInterpolation(extrapolation: Double) { + isInterpolating = true + this.extrapolation = extrapolation + (get() as? Stateful)?.networkElement?.enableInterpolation(extrapolation) + } + + override fun disableInterpolation() { + isInterpolating = false + (get() as? Stateful)?.networkElement?.disableInterpolation() + } + + override fun specifyVersioner(versionCounter: LongSupplier) { + super.specifyVersioner(versionCounter) + (get() as? Stateful)?.networkElement?.disableInterpolation() + } + + override fun hasChangedSince(version: Long): Boolean { + return super.hasChangedSince(version) || (get() as? Stateful)?.networkElement?.hasChangedSince(version) == true + } + + override fun accept(t: ItemStack) { + super.accept(t) + + if (t is Stateful) { + if (versionCounter != null) { + t.networkElement.specifyVersioner(versionCounter!!) + } + + if (isInterpolating) { + t.networkElement.enableInterpolation(extrapolation) + } else { + t.networkElement.disableInterpolation() + } + } + } + + override fun readBlankDelta(interpolationDelay: Double) { + super.readBlankDelta(interpolationDelay) + + if (isInterpolating) { + (get() as? Stateful)?.networkElement?.readBlankDelta(interpolationDelay) + } + } + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + super.readInitial(data, isLegacy) + + val stack = get() + + if (stack is Stateful) { + stack.networkElement.readInitial(data, isLegacy) + } + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + super.writeInitial(data, isLegacy) + + val stack = get() + + if (stack is Stateful) { + stack.networkElement.writeInitial(data, isLegacy) + } + } + + override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) { + while (true) { + when (val type = data.readUnsignedByte()) { + 0 -> break + + 1 -> { + super.readDelta(data, interpolationDelay, isLegacy) + + val stack = get() + + if (stack is Stateful) { + if (versionCounter != null) { + stack.networkElement.specifyVersioner(versionCounter!!) + } + + stack.networkElement.readInitial(data, isLegacy) + + if (isInterpolating) { + stack.networkElement.enableInterpolation(extrapolation) + } + } + } + + 2 -> { + val stack = get() + + if (stack is Stateful) { + stack.networkElement.readInitial(data, isLegacy) + } else { + throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $stack)") + } + } + + 3 -> { + val stack = get() + + if (stack is Stateful) { + stack.networkElement.readDelta(data, interpolationDelay, isLegacy) + } else { + throw IllegalStateException("Remote and Local disagree whenever ItemStack has networked state (local item: $stack)") + } + } + + else -> throw IllegalArgumentException("Unknown change type $type") + } + } + } + + override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) { + if (super.hasChangedSince(remoteVersion)) { + data.writeByte(1) + super.writeDelta(data, remoteVersion, isLegacy) + + val stack = get() + + if (stack is Stateful) { + data.writeByte(2) + stack.networkElement.writeInitial(data, isLegacy) + } + } + + val stack = get() + + if (stack is Stateful && stack.networkElement.hasChangedSince(remoteVersion)) { + data.writeByte(3) + stack.networkElement.writeDelta(data, remoteVersion, isLegacy) + } + + data.writeByte(0) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt index 5e929c4a..044c34c5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/Avatar.kt @@ -17,7 +17,7 @@ import ru.dbotthepony.kstarbound.lua.luaFunction0String import ru.dbotthepony.kstarbound.lua.luaFunctionN import ru.dbotthepony.kstarbound.lua.luaStub import ru.dbotthepony.kstarbound.lua.set -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import java.util.* import kotlin.collections.ArrayList @@ -48,13 +48,13 @@ class Avatar(val uniqueId: UUID) { private val essentialSlots = EnumMap(EssentialSlot::class.java) private val equipmentSlots = EnumMap(EquipmentSlot::class.java) private val bags = ArrayList() - private val quests = Object2ObjectOpenHashMap() + private val quests = HashMap() var cursorItem = ItemStack.EMPTY private val availableTechs = ObjectOpenHashSet>() private val enabledTechs = ObjectOpenHashSet>() - private val equippedTechs = Object2ObjectOpenHashMap>() + private val equippedTechs = HashMap>() private val knownBlueprints = ObjectOpenHashSet() // С подписью NEW diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt index 54f7da49..5d52e46b 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/AvatarBag.kt @@ -2,7 +2,7 @@ package ru.dbotthepony.kstarbound.player import com.google.common.collect.ImmutableList import ru.dbotthepony.kstarbound.defs.actor.player.InventoryConfig -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import java.util.function.Predicate class AvatarBag(val avatar: Avatar, val config: InventoryConfig.Bag, val filter: Predicate) { @@ -20,10 +20,10 @@ class AvatarBag(val avatar: Avatar, val config: InventoryConfig.Bag, val filter: fun mergeFrom(value: ItemStack, simulate: Boolean) { if (item == null) { if (!simulate) { - item = value.copy().also { it.count = value.count.coerceAtMost(value.item!!.value.maxStack) } + item = value.copy().also { it.count = value.count.coerceAtMost(value.maxStackSize) } } - value.count -= value.item!!.value.maxStack + value.count -= value.maxStackSize } else { item!!.mergeFrom(value, simulate) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt index d3635723..09208c5a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/player/QuestInstance.kt @@ -9,8 +9,9 @@ import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate import ru.dbotthepony.kstarbound.lua.NewLuaState -import ru.dbotthepony.kstarbound.util.ItemStack +import ru.dbotthepony.kstarbound.item.ItemStack import ru.dbotthepony.kommons.gson.set +import java.util.HashMap import java.util.UUID class QuestInstance( @@ -47,7 +48,7 @@ class QuestInstance( private val portraits = JsonObject() private val params = descriptor.parameters.deepCopy() - private val portraitTitles = Object2ObjectOpenHashMap() + private val portraitTitles = HashMap() private var isInitialized = false private var successfulInit = false diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt index 39e60bcd..79a6f2b0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/ServerConnection.kt @@ -32,6 +32,7 @@ import ru.dbotthepony.kstarbound.world.IChunkListener import ru.dbotthepony.kstarbound.world.api.ImmutableCell import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.WorldObject +import java.util.HashMap import java.util.concurrent.ConcurrentLinkedQueue import kotlin.properties.Delegates @@ -94,7 +95,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn } } - private val shipChunks = Object2ObjectOpenHashMap>() + private val shipChunks = HashMap>() private val modifiedShipChunks = ObjectOpenHashSet() var shipChunkSource by Delegates.notNull() private set @@ -114,7 +115,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn shipChunks.putAll(chunks) } - private val tickets = Object2ObjectOpenHashMap() + private val tickets = HashMap() private val pendingSend = ObjectLinkedOpenHashSet() private var needsToRecomputeTrackedChunks = true @@ -290,8 +291,8 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn try { msg.play(this) } catch (err: Throwable) { - LOGGER.error("Failed to read serverbound packet $msg", err) - disconnect(err.toString()) + LOGGER.error("Failed to handle serverbound packet $msg", err) + disconnect("Incoming packet caused an exception: $err") } } else { LOGGER.error("Unknown serverbound packet type $msg") 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 d2942367..718cd989 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -175,7 +175,13 @@ class ServerWorld private constructor( internalPlayers.forEach { if (!isClosed.get() && it.worldStartAcknowledged && it.channel.isOpen) { it.send(packet) - it.tickWorld() + + try { + it.tickWorld() + } catch (err: Throwable) { + LOGGER.error("Exception while ticking player $it", err) + //it.disconnect("Exception while ticking player: $err") + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt index 47c3ceb4..86793609 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/UniverseChunk.kt @@ -23,11 +23,12 @@ import ru.dbotthepony.kstarbound.defs.world.CelestialPlanet import ru.dbotthepony.kstarbound.json.pairAdapter import ru.dbotthepony.kstarbound.json.pairListAdapter import ru.dbotthepony.kstarbound.world.UniversePos +import java.util.HashMap class UniverseChunk(var chunkPos: Vector2i = Vector2i.ZERO) { data class System(val parameters: CelestialParameters, val planets: Int2ObjectMap) - val systems = Object2ObjectOpenHashMap() + val systems = HashMap() val constellations = ObjectOpenHashSet>() fun parameters(coordinate: UniversePos): CelestialParameters? { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt similarity index 80% rename from src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt index 5da35feb..0a64eaf9 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/RenderDirectives.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt @@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap import it.unimi.dsi.fastutil.objects.Object2ObjectMap import it.unimi.dsi.fastutil.objects.Object2ObjectMaps -class RenderDirectives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap) { +class Directives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap) { constructor() : this(Object2ObjectAVLTreeMap()) constructor(directives: String) : this() { if (directives.isNotBlank()) { @@ -36,29 +36,29 @@ class RenderDirectives private constructor(private val directivesInternal: Objec override fun toString(): String { if (directivesInternal.isEmpty()) - return "RenderDirectives[empty]" + return "Directives[empty]" else - return "RenderDirectives[?${directivesInternal.entries.joinToString("?") { "${it.key}=${it.value}" }}]" + return "Directives[?${directivesInternal.entries.joinToString("?") { "${it.key}=${it.value}" }}]" } override fun equals(other: Any?): Boolean { - return this === other || other is RenderDirectives && directivesInternal == other.directivesInternal + return this === other || other is Directives && directivesInternal == other.directivesInternal } override fun hashCode(): Int { return directivesInternal.hashCode() } - fun add(directive: String, value: String): RenderDirectives { + fun add(directive: String, value: String): Directives { if (directivesInternal[directive] == value) return this val copy = directivesInternal.clone() copy[directive] = value - return RenderDirectives(copy) + return Directives(copy) } - fun add(directives: String): RenderDirectives { + fun add(directives: String): Directives { if ('?' !in directives) { if ('=' !in directives) { throw IllegalArgumentException("Missing render directive delimiter in $directives") @@ -87,7 +87,7 @@ class RenderDirectives private constructor(private val directivesInternal: Objec copy[key] = value } - return RenderDirectives(copy) + return Directives(copy) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt index b8cb64d9..d193baea 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/GameTimer.kt @@ -26,12 +26,12 @@ class GameTimer(val time: Double = 0.0) { timer = time - timer } - fun tick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean { + fun tick(delta: Double = Starbound.TIMESTEP): Boolean { timer = (timer - delta).coerceAtLeast(0.0) return timer == 0.0 } - fun wrapTick(delta: Double = Starbound.TICK_TIME_ADVANCE): Boolean { + fun wrapTick(delta: Double = Starbound.TIMESTEP): Boolean { val result = tick(delta) if (result) reset() return result diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt index bf1c49f6..5e855179 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/Sky.kt @@ -1,24 +1,18 @@ package ru.dbotthepony.kstarbound.world -import it.unimi.dsi.fastutil.bytes.ByteArrayList -import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream -import ru.dbotthepony.kommons.io.VarIntValueCodec import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.util.value import ru.dbotthepony.kstarbound.defs.world.SkyParameters import ru.dbotthepony.kstarbound.defs.world.SkyType import ru.dbotthepony.kstarbound.defs.world.WarpPhase -import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement -import ru.dbotthepony.kstarbound.network.syncher.GroupElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.MasterElement -import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedDouble import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid import ru.dbotthepony.kstarbound.network.syncher.networkedFloat import ru.dbotthepony.kstarbound.network.syncher.networkedJson -import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f @@ -54,7 +48,7 @@ class Sky() { var flyingTimer by flyingTimerNetState private set - val networkedGroup = MasterElement(GroupElement( + val networkedGroup = MasterElement(NetworkedGroup( skyParametersNetState, skyTypeNetState, timeNetState, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 3189ce66..5080acfe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -1,6 +1,7 @@ package ru.dbotthepony.kstarbound.world import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.IntArraySet import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.ObjectArraySet @@ -227,7 +228,7 @@ abstract class World, ChunkType : Chunk() - val entities = ReferenceOpenHashSet() + val entities = Int2ObjectOpenHashMap() val dynamicEntities = ReferenceOpenHashSet() val tileEntities = ReferenceOpenHashSet() @@ -271,7 +272,7 @@ abstract class World, ChunkType : Chunk) { if (innerWorld != null) throw IllegalStateException("Already spawned (in world $innerWorld)") @@ -100,11 +101,13 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) { world.ensureSameThread() + check(!world.entities.containsKey(entityID)) { "Duplicate entity ID: $entityID" } + if (mailbox.isShutdown) mailbox = MailboxExecutorService() innerWorld = world - world.entities.add(this) + world.entities[entityID] = this world.orphanedEntities.add(this) onJoinWorld(world) } @@ -115,7 +118,7 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) { mailbox.shutdownNow() chunk = null - world.entities.remove(this) + check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } world.orphanedEntities.remove(this) onRemove(world) innerWorld = null diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt index 3ae22ded..c7375b1c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorEntity.kt @@ -5,4 +5,5 @@ package ru.dbotthepony.kstarbound.world.entities */ abstract class ActorEntity(path: String) : DynamicEntity(path) { final override val movement: ActorMovementController = ActorMovementController() + abstract val statusController: StatusController } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt index b1785bec..f97c2ed7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ActorMovementController.kt @@ -1,5 +1,7 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kommons.io.koptional +import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d @@ -9,6 +11,7 @@ import ru.dbotthepony.kstarbound.defs.JumpProfile import ru.dbotthepony.kstarbound.defs.MovementParameters import ru.dbotthepony.kstarbound.defs.actor.player.ActorMovementModifiers import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEnum import ru.dbotthepony.kstarbound.util.GameTimer import ru.dbotthepony.kstarbound.world.Direction @@ -26,32 +29,35 @@ class ActorMovementController : MovementController() { var controlFly: Vector2d? = null var controlFace: Direction1D? = null - var isWalking: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isWalking: Boolean by networkGroup.add(networkedBoolean()) private set - var isRunning: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isRunning: Boolean by networkGroup.add(networkedBoolean()) private set - var movingDirection: Direction1D by networkGroup.upstream.add(networkedEnum(Direction1D.RIGHT)) + var movingDirection: Direction1D by networkGroup.add(networkedEnum(Direction1D.RIGHT)) private set - var facingDirection: Direction1D by networkGroup.upstream.add(networkedEnum(Direction1D.RIGHT)) + var facingDirection: Direction1D by networkGroup.add(networkedEnum(Direction1D.RIGHT)) private set - var isCrouching: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isCrouching: Boolean by networkGroup.add(networkedBoolean()) private set - var isFlying: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isFlying: Boolean by networkGroup.add(networkedBoolean()) private set - var isFalling: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isFalling: Boolean by networkGroup.add(networkedBoolean()) private set - var canJump: Boolean by networkGroup.upstream.add(networkedBoolean()) + var canJump: Boolean by networkGroup.add(networkedBoolean()) private set - var isJumping: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isJumping: Boolean by networkGroup.add(networkedBoolean()) private set - var isGroundMovement: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isGroundMovement: Boolean by networkGroup.add(networkedBoolean()) private set - var isLiquidMovement: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isLiquidMovement: Boolean by networkGroup.add(networkedBoolean()) + private set + + var anchorState by networkGroup.add(networkedData(KOptional(), AnchorState.CODEC.koptional(), AnchorState.LEGACY_CODEC.koptional())) private set var controlJump: Boolean = false @@ -217,7 +223,7 @@ class ActorMovementController : MovementController() { isGroundMovement = false isLiquidMovement = false - velocity = (anchorEntity.position - position) / Starbound.TICK_TIME_ADVANCE + velocity = (anchorEntity.position - position) / Starbound.TIMESTEP super.move() position = anchorEntity.position } else { @@ -262,8 +268,8 @@ class ActorMovementController : MovementController() { targetHorizontalAmbulatingVelocity = 0.0 - rotation = (rotation + controlRotationRate * Starbound.TICK_TIME_ADVANCE) % (PI * 2.0) - velocity += controlAcceleration * Starbound.TICK_TIME_ADVANCE + controlForce / mass * Starbound.TICK_TIME_ADVANCE + rotation = (rotation + controlRotationRate * Starbound.TIMESTEP) % (PI * 2.0) + velocity += controlAcceleration * Starbound.TIMESTEP + controlForce / mass * Starbound.TIMESTEP approachVelocities.forEach { approachVelocity(it.target, it.maxControlForce) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt new file mode 100644 index 00000000..612e85e8 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnchorState.kt @@ -0,0 +1,22 @@ +package ru.dbotthepony.kstarbound.world.entities + +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.network.syncher.readPointer +import ru.dbotthepony.kstarbound.network.syncher.writePointer +import java.io.DataInputStream +import java.io.DataOutputStream + +data class AnchorState(val entityID: Int, val positionIndex: Int) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt(), stream.readPointer().toInt()) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeInt(entityID) + stream.writePointer(positionIndex) + } + + companion object { + val CODEC = nativeCodec(::AnchorState, AnchorState::write) + val LEGACY_CODEC = legacyCodec(::AnchorState, AnchorState::write) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnimatedParts.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnimatedParts.kt new file mode 100644 index 00000000..0a176749 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AnimatedParts.kt @@ -0,0 +1,59 @@ +package ru.dbotthepony.kstarbound.world.entities + +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap +import ru.dbotthepony.kstarbound.defs.animation.AnimatedPartsDefinition + +class AnimatedParts { + private class StateType(config: AnimatedPartsDefinition.StateType) { + var enabled = config.enabled + var activeStateDirty = true + val priority = config.priority + val stateTypeProperties = config.properties + val default: String + + // sorted by key + val states = Object2ObjectAVLTreeMap() + + init { + config.states.forEach { (t, u) -> states[t] = u } + + if (states.isNotEmpty() && config.default.isBlank()) + default = states.firstKey() + else + default = config.default + } + } + + private class Part(config: AnimatedPartsDefinition.Part) { + val partProperties = config.properties + var activePartDirty = true + val partStates = config.partStates + } + + // sorted by priority + private val stateTypes = LinkedHashMap() + // sorted by key + private val parts = Object2ObjectAVLTreeMap() + + constructor() { + + } + + constructor(config: AnimatedPartsDefinition) { + for ((k, v) in config.stateTypes.entries.sortedWith { o1, o2 -> o2.value.priority.compareTo(o1.value.priority) }) { + stateTypes[k] = StateType(v) + } + + for ((k, v) in config.parts) { + parts[k] = Part(v) + } + } + + fun parts(): Collection { + return parts.keys + } + + fun stateTypes(): Collection { + return stateTypes.keys + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt new file mode 100644 index 00000000..a0cedfd1 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/Animator.kt @@ -0,0 +1,402 @@ +package ru.dbotthepony.kstarbound.world.entities + +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import org.apache.logging.log4j.LogManager +import ru.dbotthepony.kommons.io.BinaryStringCodec +import ru.dbotthepony.kommons.io.IntValueCodec +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.matrix.Matrix3f +import ru.dbotthepony.kommons.util.AABB +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition +import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig +import ru.dbotthepony.kstarbound.defs.animation.ParticleFactory +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.math.PeriodicFunction +import ru.dbotthepony.kstarbound.network.syncher.AABBCodecLegacy +import ru.dbotthepony.kstarbound.network.syncher.AABBCodecNative +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap +import ru.dbotthepony.kstarbound.network.syncher.NetworkedSignal +import ru.dbotthepony.kstarbound.network.syncher.networkedAABBNullable +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedColor +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter +import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint +import ru.dbotthepony.kstarbound.network.syncher.networkedFloat +import ru.dbotthepony.kstarbound.network.syncher.networkedList +import ru.dbotthepony.kstarbound.network.syncher.networkedPointer +import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.Collections +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + + +class Animator() { + class Light { + private val elements = ArrayList() + + fun addTo(group: NetworkedGroup) { + elements.forEach { group.add(it) } + } + + var active by networkedBoolean().also { elements.add(it) } + var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } + var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } + var color by networkedColor().also { elements.add(it) } + var pointAngle by networkedFixedPoint(0.01).also { elements.add(it); it.interpolator = Interpolator.Linear } + + var anchorPart: String? = null + var transformationGroups: List = listOf() + var rotationGroup: String? = null + var rotationCenter: Vector2d? = null + + var flicker: PeriodicFunction? = null + var pointLight: Boolean = false + var pointBeam: Float = 0f + var beamAmbience: Float = 0f + } + + enum class SoundSignal { + PLAY, STOP_ALL; + + companion object { + val CODEC = IntValueCodec.map({ entries[this] }, { ordinal }) + } + } + + class Sound { + private val elements = ArrayList() + + fun addTo(group: NetworkedGroup) { + elements.forEach { group.add(it) } + } + + var rangeMultiplier = 0.0 + + var soundPool by networkedList(InternedStringCodec).also { elements.add(it) } + var xPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } + var yPosition by networkedFixedPoint(0.0125).also { elements.add(it); it.interpolator = Interpolator.Linear } + var volumeTarget by networkedFloat(1.0).also { elements.add(it) } + var volumeRampTime by networkedFloat(0.0).also { elements.add(it) } + var pitchMultiplierTarget by networkedFloat(1.0).also { elements.add(it) } + var pitchMultiplierRampTime by networkedFloat(0.0).also { elements.add(it) } + var loops by networkedSignedInt().also { elements.add(it) } + val signals = NetworkedSignal(SoundSignal.CODEC).also { elements.add(it) } + } + + class Effect(val type: String, val time: Double, val directives: String) { + val enabled = networkedBoolean() + var timer: Double = 0.0 + } + + class StateInfo { + val stateIndex = networkedPointer() + val startedEvent = networkedEventCounter() + } + + class RotationGroup { + var angularVelocity = 0.0 + var rotationCenter = Vector2d.ZERO + val targetAngle = networkedFloat() + var currentAngle = 0.0 + val immediateEvent = networkedEventCounter() + } + + class TransformationGroup { + private val elements = ArrayList() + + fun addTo(group: NetworkedGroup) { + elements.forEach { group.add(it) } + } + + var interpolated = false + + var xTranslation by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + var yTranslation by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + var xScale by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + var yScale by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + var xShear by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + var yShear by networkedFloat(0.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + + fun affineTransform(): Matrix3f { + return Matrix3f.rowMajor( + (xScale * cos(xShear)).toFloat(), (xScale * sin(xShear)).toFloat(), xTranslation.toFloat(), + (yScale * sin(yShear)).toFloat(), (yScale * cos(yShear)).toFloat(), yTranslation.toFloat(), + 0f, 0f, 1f + ) + } + + fun setAffineTransform(value: Matrix3f) { + xTranslation = value[2, 0].toDouble() + yTranslation = value[2, 1].toDouble() + + xScale = sqrt(value[0, 0].toDouble() * value[0, 0].toDouble() + value[1, 0].toDouble() * value[1, 0].toDouble()) + yScale = sqrt(value[0, 1].toDouble() * value[0, 1].toDouble() + value[1, 1].toDouble() * value[1, 1].toDouble()) + + xShear = atan2(value[1, 0].toDouble(), value[0, 0].toDouble()) + yShear = atan2(value[0, 1].toDouble(), value[1, 1].toDouble()) + } + } + + class ParticleEmitter { + data class Config(val count: Int, val offset: Vector2d, val flip: Boolean, val factory: ParticleFactory) + + private val elements = ArrayList() + + fun addTo(group: NetworkedGroup) { + elements.forEach { group.add(it) } + } + + var emissionRate by networkedFloat().also { elements.add(it) } + var burstCount by networkedUnsignedInt().also { elements.add(it) } + var randomSelectCount by networkedUnsignedInt().also { elements.add(it) } + var emissionRateVariance = 0.0 + var offsetRegion by networkedAABBNullable().also { elements.add(it) } + var anchorPart: String? = null + var transformationGroups: List = listOf() + var rotationGroup: String? = null + var rotationCenter: Vector2d? = null + + val particleList = ArrayList() + + var active by networkedBoolean().also { elements.add(it) } + val burstEvent = networkedEventCounter().also { elements.add(it); it.ignoreOccurrencesOnLoad = true } + + var timer = 0.0 + } + + val networkGroup = NetworkedGroup() + + private val elements = ArrayList() + + var animatedParts = AnimatedParts() + private set + + var processingDirectives by networkedString().also { elements.add(it) } + var zoom by networkedFloat().also { elements.add(it) } + var isFlipped by networkedBoolean().also { elements.add(it) } + var flippedRelativeCenterLine by networkedFloat().also { elements.add(it) } + var animationRate by networkedFloat(1.0).also { elements.add(it); it.interpolator = Interpolator.Linear } + + private val globalTags = NetworkedMap(InternedStringCodec, InternedStringCodec) + private val partTags = HashMap>() + + private val stateInfo = Object2ObjectAVLTreeMap() + private val rotationGroups = Object2ObjectAVLTreeMap() + private val transformationGroups = Object2ObjectAVLTreeMap() + private val particleEmitters = Object2ObjectAVLTreeMap() + private val lights = Object2ObjectAVLTreeMap() + private val sounds = Object2ObjectAVLTreeMap() + private val effects = Object2ObjectAVLTreeMap() + + init { + setupNetworkElements() + } + + constructor(config: AnimationDefinition) : this() { + if (config.animatedParts != null) + animatedParts = AnimatedParts(config.animatedParts) + + for ((k, v) in config.globalTagDefaults) { + globalTags[k] = v + } + + for ((part, tags) in config.partTagDefaults) { + for ((k, v) in tags) { + setPartTag(part, k, v) + } + } + + for ((k, v) in config.transformationGroups) { + val group = TransformationGroup() + transformationGroups[k] = group + group.interpolated = v.interpolated + } + + for ((k, v) in config.rotationGroups) { + val group = RotationGroup() + group.angularVelocity = v.angularVelocity + group.rotationCenter = v.rotationCenter + rotationGroups[k] = group + } + + for ((k, v) in config.particleEmitters) { + val emitter = ParticleEmitter() + + emitter.emissionRate = v.emissionRate + emitter.emissionRateVariance = v.emissionRateVariance + emitter.offsetRegion = KOptional.ofNullable(v.offsetRegion) + emitter.anchorPart = v.anchorPart + emitter.transformationGroups = v.transformationGroups + emitter.rotationGroup = v.rotationGroup + emitter.rotationCenter = v.rotationCenter + emitter.burstCount = v.burstCount + emitter.randomSelectCount = v.randomSelectCount + emitter.active = v.active + + for (particle in v.particles) { + val factory = particle.particle.map({ it.value }, { it }) ?: continue // not a valid particle, too bad. + emitter.particleList.add(ParticleEmitter.Config(particle.count, particle.offset, particle.flip, factory)) + } + + particleEmitters[k] = emitter + } + + for ((k, v) in config.lights) { + val light = Light() + + light.active = v.active + light.xPosition = v.position.x + light.yPosition = v.position.y + light.color = v.color + light.anchorPart = v.anchorPart + light.transformationGroups = v.transformationGroups + light.rotationGroup = v.rotationGroup + light.rotationCenter = v.rotationCenter + light.pointAngle = Math.toRadians(v.pointAngle) + light.pointLight = v.pointLight + light.pointBeam = v.pointBeam + light.beamAmbience = v.beamAmbience + + if (v.flickerPeriod != null) + light.flicker = PeriodicFunction(v.flickerPeriod, v.flickerMinIntensity, v.flickerMaxIntensity, v.flickerPeriodVariance, v.flickerIntensityVariance) + + lights[k] = light + } + + for ((k, v) in config.sounds) { + val sound = Sound() + + if (v.isLeft) { + sound.soundPool = v.left() + } else { + val conf = v.right() + sound.soundPool = conf.pool.map { it.fullPath } + sound.xPosition = conf.position.x + sound.yPosition = conf.position.y + sound.volumeTarget = conf.volume + sound.volumeRampTime = conf.volumeRampTime + sound.pitchMultiplierTarget = conf.pitchMultiplier + sound.pitchMultiplierRampTime = conf.pitchMultiplierRampTime + sound.rangeMultiplier = conf.rangeMultiplier + } + + sounds[k] = sound + } + + for ((k, v) in config.effects) { + effects[k] = Effect(v.type, v.time, v.directives) + } + + for (k in animatedParts.stateTypes()) { + stateInfo[k] = StateInfo() + } + + for (k in animatedParts.parts()) { + partTags.computeIfAbsent(k) { NetworkedMap(InternedStringCodec, InternedStringCodec) } + } + + setupNetworkElements() + } + + // Every part image can have one or more directives in it, which if set + // here will be replaced by the tag value when constructing Drawables. All + // Drawables can also have a tag which will be set to whatever the + // current state frame is (1 indexed, so the first frame is 1). + fun setGlobalTag(key: String, value: String) { + globalTags[key] = value + } + + fun setPartTag(partName: String, tagKey: String, tagValue: String) { + var tags = partTags[partName] + + if (tags == null) { + tags = NetworkedMap(InternedStringCodec, InternedStringCodec) + partTags[partName] = tags + } + + tags[tagKey] = tagValue + } + + private fun setupNetworkElements() { + networkGroup.clear() + elements.forEach { networkGroup.add(it) } + + networkGroup.add(globalTags) + + // animated part set + for (v in animatedParts.parts()) { + networkGroup.add(partTags[v] ?: throw RuntimeException("Missing animated part $v!")) + } + + for (v in stateInfo.values) { + networkGroup.add(v.stateIndex) + networkGroup.add(v.startedEvent) + } + + for (v in transformationGroups.values) { + v.addTo(networkGroup) + } + + for (v in rotationGroups.values) { + networkGroup.add(v.targetAngle) + networkGroup.add(v.immediateEvent) + } + + for (v in particleEmitters.values) { + v.addTo(networkGroup) + } + + for (v in lights.values) { + v.addTo(networkGroup) + } + + for (v in sounds.values) { + v.addTo(networkGroup) + } + + for (v in effects.values) { + networkGroup.add(v.enabled) + } + } + + companion object { + // lame + fun load(path: String): Animator { + val json = Starbound.loadJsonAsset(path) + + if (json == null) { + if (missing.add(path)) { + LOGGER.error("Unable to instance Animator from $path! This very likely gonna make networking code go haywire.") + } + + return Animator() + } + + return Animator(Starbound.gson.fromJson(json, AnimationDefinition::class.java)) + } + + private val LOGGER = LogManager.getLogger() + private val missing = Collections.synchronizedSet(ObjectOpenHashSet()) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt new file mode 100644 index 00000000..a39ef86e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/EffectEmitter.kt @@ -0,0 +1,23 @@ +package ru.dbotthepony.kstarbound.world.entities + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import ru.dbotthepony.kommons.io.BinaryStringCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import java.util.function.Consumer + +class EffectEmitter(val entity: AbstractEntity) { + val networkGroup = NetworkedGroup() + + // stoopid + var currentEffects by networkGroup.add(networkedData(setOf(), pairCodec)) + private set + + companion object { + private val pairCodec = StreamCodec.Collection(StreamCodec.Pair(InternedStringCodec, InternedStringCodec), ::ObjectOpenHashSet) as StreamCodec>> + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt index 37e7dd7a..12f119a0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/HumanoidActorEntity.kt @@ -1,10 +1,53 @@ package ru.dbotthepony.kstarbound.world.entities +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint +import ru.dbotthepony.kstarbound.network.syncher.networkedItem +import ru.dbotthepony.kstarbound.network.syncher.networkedStatefulItem +import java.io.DataInputStream /** * Players and NPCs */ abstract class HumanoidActorEntity(path: String) : ActorEntity(path) { abstract val aimPosition: Vector2d + + val effects = EffectEmitter(this) + + // it makes no sense to split ToolUser' logic into separate class + protected val toolsNetworkGroup = NetworkedGroup() + + var primaryHandItem by toolsNetworkGroup.add(networkedStatefulItem()) + var secondaryHandItem by toolsNetworkGroup.add(networkedStatefulItem()) + var primaryFireTimerNetState by toolsNetworkGroup.add(networkedFixedPoint(Starbound.TIMESTEP).also { it.interpolator = ToolFiringInterpolator }) + var secondaryFireTimerNetState by toolsNetworkGroup.add(networkedFixedPoint(Starbound.TIMESTEP).also { it.interpolator = ToolFiringInterpolator }) + var primaryTimeFiringNetState by toolsNetworkGroup.add(networkedFixedPoint(Starbound.TIMESTEP).also { it.interpolator = ToolFiringInterpolator }) + var secondaryTimeFiringNetState by toolsNetworkGroup.add(networkedFixedPoint(Starbound.TIMESTEP).also { it.interpolator = ToolFiringInterpolator }) + var primaryItemActive by toolsNetworkGroup.add(networkedBoolean()) + var secondaryItemActive by toolsNetworkGroup.add(networkedBoolean()) + + private object ToolFiringInterpolator : Interpolator { + override fun interpolate(t: Double, a: Double, b: Double): Double { + return if (a >= b) b else Interpolator.Linear.interpolate(t, a, b) + } + } + + // same as above + protected val armorNetworkGroup = NetworkedGroup() + + var headItem by armorNetworkGroup.add(networkedItem()) + var chestItem by armorNetworkGroup.add(networkedItem()) + var legsItem by armorNetworkGroup.add(networkedItem()) + var backItem by armorNetworkGroup.add(networkedItem()) + var headCosmeticItem by armorNetworkGroup.add(networkedItem()) + var chestCosmeticItem by armorNetworkGroup.add(networkedItem()) + var legsCosmeticItem by armorNetworkGroup.add(networkedItem()) + var backCosmeticItem by armorNetworkGroup.add(networkedItem()) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index 86eae32c..8132d35a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -1,6 +1,5 @@ package ru.dbotthepony.kstarbound.world.entities -import it.unimi.dsi.fastutil.bytes.ByteArrayList import ru.dbotthepony.kommons.io.DoubleValueCodec import ru.dbotthepony.kommons.io.FloatValueCodec import ru.dbotthepony.kommons.io.StreamCodec @@ -16,8 +15,8 @@ import ru.dbotthepony.kommons.vector.Vector2d import ru.dbotthepony.kommons.vector.times import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.MovementParameters -import ru.dbotthepony.kstarbound.network.syncher.GroupElement -import ru.dbotthepony.kstarbound.network.syncher.MasterElement +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint @@ -51,34 +50,34 @@ open class MovementController() { private val legacyPoly = networkedPoly(Poly.EMPTY) - protected val networkGroup = MasterElement(GroupElement(legacyPoly)) + val networkGroup = NetworkedGroup(legacyPoly) - var mass by networkGroup.upstream.add(networkedFloat()) + var mass by networkGroup.add(networkedFloat()) - private var xPosition by networkGroup.upstream.add(networkedFixedPoint(0.0125)) - private var yPosition by networkGroup.upstream.add(networkedFixedPoint(0.0125)) - private var xVelocity by networkGroup.upstream.add(networkedFixedPoint(0.00625)) - private var yVelocity by networkGroup.upstream.add(networkedFixedPoint(0.00625)) + private var xPosition by networkGroup.add(networkedFixedPoint(0.0125)) + private var yPosition by networkGroup.add(networkedFixedPoint(0.0125)) + private var xVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) + private var yVelocity by networkGroup.add(networkedFixedPoint(0.00625).also { it.interpolator = Interpolator.Linear }) - var rotation by networkGroup.upstream.add(networkedFixedPoint(0.01)) + var rotation by networkGroup.add(networkedFixedPoint(0.01).also { it.interpolator = Interpolator.Linear }) - var isColliding: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isColliding: Boolean by networkGroup.add(networkedBoolean()) protected set - var isCollisionStuck: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isCollisionStuck: Boolean by networkGroup.add(networkedBoolean()) protected set - var isCollidingWithNull: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isCollidingWithNull: Boolean by networkGroup.add(networkedBoolean()) protected set - private val stickingDirectionField = networkGroup.upstream.add(networkedData(KOptional(), DoubleValueCodec.koptional(), FloatValueCodec.map({ toDouble() }, { toFloat() }).koptional())) + private val stickingDirectionField = networkGroup.add(networkedData(KOptional(), DoubleValueCodec.koptional(), FloatValueCodec.map({ toDouble() }, { toFloat() }).koptional())) - var isOnGround: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isOnGround: Boolean by networkGroup.add(networkedBoolean()) protected set - var isZeroGravity: Boolean by networkGroup.upstream.add(networkedBoolean()) + var isZeroGravity: Boolean by networkGroup.add(networkedBoolean()) protected set - private val surfaceMovingCollisionField = networkGroup.upstream.add(networkedData(KOptional(), StreamCodec.Impl(::MovingCollisionID, { a, b -> b.write(a) }).koptional())) - private val relativeXSurfaceVelocity = networkGroup.upstream.add(networkedFloat()) - private val relativeYSurfaceVelocity = networkGroup.upstream.add(networkedFloat()) + private val surfaceMovingCollisionField = networkGroup.add(networkedData(KOptional(), StreamCodec.Impl(::MovingCollisionID, { a, b -> b.write(a) }).koptional())) + private val relativeXSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear }) + private val relativeYSurfaceVelocity = networkGroup.add(networkedFixedPoint(0.0125).also { it.interpolator = Interpolator.Linear }) var position: Vector2d get() = Vector2d(xPosition, yPosition) @@ -144,7 +143,7 @@ open class MovementController() { if (mag == 0.0) return - val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE + val maximumAcceleration = maxControlForce / mass * Starbound.TIMESTEP val clampedMag = mag.coerceIn(0.0, maximumAcceleration) velocity += diff * (clampedMag / mag) @@ -164,7 +163,7 @@ open class MovementController() { if (diff == 0.0 || positiveOnly && diff < 0.0) return - val maximumAcceleration = maxControlForce / mass * Starbound.TICK_TIME_ADVANCE + val maximumAcceleration = maxControlForce / mass * Starbound.TIMESTEP val diffMag = diff.absoluteValue val clampedMag = diffMag.coerceIn(0.0, maximumAcceleration) @@ -196,7 +195,7 @@ open class MovementController() { // TODO: Here: moving platforms sticky code if (movementParameters.collisionPoly == null || !movementParameters.collisionPoly.map({ true }, { it.isNotEmpty() }) || movementParameters.collisionEnabled != true) { - position += velocity * Starbound.TICK_TIME_ADVANCE + position += velocity * Starbound.TIMESTEP surfaceSlope = Vector2d.POSITIVE_Y surfaceVelocity = Vector2d.ZERO isOnGround = false @@ -210,14 +209,14 @@ open class MovementController() { var steps = 1 movementParameters.maxMovementPerStep?.let { - steps = (velocity.length * Starbound.TICK_TIME_ADVANCE / it).toInt() + 1 + steps = (velocity.length * Starbound.TIMESTEP / it).toInt() + 1 } var relativeVelocity = velocity surfaceSlope = Vector2d.POSITIVE_Y // TODO: Here: moving platforms sticky code - val dt = Starbound.TICK_TIME_ADVANCE / steps + val dt = Starbound.TIMESTEP / steps for (step in 0 until steps) { val velocityMagnitude = relativeVelocity.length @@ -289,14 +288,14 @@ open class MovementController() { // independently). if (relativeVelocity.x < 0.0 && correction.x > 0.0) - relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) + relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TIMESTEP).coerceAtMost(0.0)) else if (relativeVelocity.x > 0.0 && correction.x < 0.0) - relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) + relativeVelocity = relativeVelocity.copy(x = (relativeVelocity.x + correction.x / Starbound.TIMESTEP).coerceAtLeast(0.0)) if (relativeVelocity.y < 0.0 && correction.y > 0.0) - relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtMost(0.0)) + relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TIMESTEP).coerceAtMost(0.0)) else if (relativeVelocity.y > 0.0 && correction.y < 0.0) - relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TICK_TIME_ADVANCE).coerceAtLeast(0.0)) + relativeVelocity = relativeVelocity.copy(y = (relativeVelocity.y + correction.y / Starbound.TIMESTEP).coerceAtLeast(0.0)) } } } @@ -315,7 +314,7 @@ open class MovementController() { if (!isZeroGravity && stickingDirection == null) { val buoyancy = (movementParameters.liquidBuoyancy ?: 0.0).coerceIn(0.0, 1.0) + liquidPercentage + (movementParameters.airBuoyancy ?: 0.0).coerceIn(0.0, 1.0) * (1.0 - liquidPercentage) val gravity = determineGravity() * (movementParameters.gravityMultiplier ?: 1.0) * (1.0 - buoyancy) - var environmentVelocity = gravity * Starbound.TICK_TIME_ADVANCE + var environmentVelocity = gravity * Starbound.TIMESTEP if (isOnGround && (movementParameters.slopeSlidingFactor ?: 0.0) != 0.0 && surfaceSlope != Vector2d.ZERO) environmentVelocity += -surfaceSlope * (surfaceSlope.x * surfaceSlope.y) * (movementParameters.slopeSlidingFactor ?: 0.0) @@ -337,7 +336,7 @@ open class MovementController() { // but it is applied here as a multiplicative factor from [0, 1] so it does // not induce oscillation at very high friction and so it cannot be // negative. - val frictionFactor = (friction / mass * Starbound.TICK_TIME_ADVANCE).coerceIn(0.0, 1.0) + val frictionFactor = (friction / mass * Starbound.TIMESTEP).coerceIn(0.0, 1.0) newVelocity = linearInterpolation(frictionFactor, newVelocity, refVel) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt new file mode 100644 index 00000000..f3a551da --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/StatusController.kt @@ -0,0 +1,379 @@ +package ru.dbotthepony.kstarbound.world.entities + +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap +import it.unimi.dsi.fastutil.objects.ObjectArraySet +import ru.dbotthepony.kommons.collect.ListenableMap +import ru.dbotthepony.kommons.io.IntValueCodec +import ru.dbotthepony.kommons.io.KOptionalIntValueCodec +import ru.dbotthepony.kommons.io.KOptionalVarIntValueCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.UnsignedVarIntCodec +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kommons.util.value +import ru.dbotthepony.kstarbound.collect.IdMap +import ru.dbotthepony.kstarbound.defs.actor.StatModifier +import ru.dbotthepony.kstarbound.defs.actor.StatModifierType +import ru.dbotthepony.kstarbound.defs.actor.StatusControllerConfig +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.network.syncher.NetworkedDynamicGroup +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedDouble +import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint +import ru.dbotthepony.kstarbound.network.syncher.networkedFloat +import ru.dbotthepony.kstarbound.network.syncher.networkedJsonObject +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.Collections +import java.util.function.LongSupplier +import kotlin.properties.Delegates + +// this is unnatural to have this class separated, but since it contains +// lots of internal state, it would be nice to have it encapsulated; +// despite this class being used exactly once - as part of ActorEntity. + +// On other hand, original code separates StatCollection into subclass, +// and we won't do this. + +// On side note, it appears game initially separated StatSet from StatCollection, +// which means early in development Starbound had true singleplayer, which eventually +// got removed in favor of listening server. +// Unfortunately for me, this means I have to untangle this piece of code +// and unify StatSet and StatCollection logic into one thing. + +// StatSet original docs: +// Manages a collection of Stats and Resources. +// +// Stats are named floating point values of any base value, with an arbitrary +// number of "stat modifiers" attached to them. Stat modifiers can be added +// and removed in groups, and they can either raise or lower stats by a +// constant value or a percentage of the stat value without any other +// percentage modifications applied. The effective stat value is always the +// value with all mods applied. If a modifier is created for a stat that does +// not exist, there will be an effective stat value for the modified stat, but +// NO base stat. If the modifier is a base percentage modifier, it will have +// no effect because it is assumed that base stats that do not exist are zero. +// +// Resources are also named floating point values, but are in a different +// namespaced and are intended to be used as values that change regularly. +// They are always >= 0.0f, and optionally have a maximum value based on a +// given value or stat. In addition to a max value, they can also have a +// "delta" value or stat, which automatically adds or removes that delta to the +// resource every second. +// +// If a resource has a maximum value, then rather than trying to keep the +// *value* of the resource constant, this class will instead attempt to keep +// the *percentage* of the resource constant across stat changes. For example, +// if "health" is a stat with a max of 100, and the current health value is 50, +// and the max health stat is changed to 200 through any means, the health +// value will automatically update to 100. + +// StatCollection original docs: +// Extension of StatSet that can easily be set up from config, and is network +// capable. +class StatusController(val entity: ActorEntity, val config: StatusControllerConfig) : NetworkedElement.Passthrough() { + // status effects + private val networkGroup = NetworkedGroup() + private val statNetworkGroup = NetworkedGroup() + + override val parentElement: NetworkedElement + get() = networkGroup + + init { + networkGroup.add(statNetworkGroup) + } + + private var statusProperties by networkedJsonObject(config.statusProperties).also { networkGroup.add(it) } + + var parentDirectives by networkedString().also { networkGroup.add(it) } + private set + + private val uniqueEffectMetadata = NetworkedDynamicGroup(::UniqueEffectMetadata, UniqueEffectMetadata::networkGroup).also { networkGroup.add(it) } + private val effectAnimators = NetworkedDynamicGroup(::EffectAnimator, { it }).also { networkGroup.add(it) } + + fun update(delta: Double) { + updateStats(delta) + } + + private class EffectAnimator(var config: KOptional = KOptional()) : Passthrough() { + var animator = Animator() + private set + + override val parentElement: NetworkedElement + get() = animator.networkGroup + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + val old = config + config = data.readKOptional { readInternedString() } + + if (old != config) + animator = config.map { Animator.load(it) }.orElse { Animator() } + + super.readInitial(data, isLegacy) + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + data.writeKOptional(config) { writeBinaryString(it) } + super.writeInitial(data, isLegacy) + } + } + + private class UniqueEffectMetadata { + val networkGroup = NetworkedGroup() + + var duration by networkedFixedPoint(0.01).also { networkGroup.add(it); it.interpolator = Interpolator.Linear } + var maxDuration by networkedFixedPoint(0.01).also { networkGroup.add(it) } + var sourceEntity by networkedData(KOptional(), KOptionalIntValueCodec) + } + + // stats + sealed class LiveStat { + abstract val baseValue: Double + + // Value with just the base percent modifiers applied and the value + // modifiers + abstract val baseModifiedValue: Double + + // Final modified value that includes the effective modifiers. + abstract val effectiveModifiedValue: Double + } + + private class LiveStatImpl : LiveStat() { + override var baseValue: Double = 0.0 + + // Value with just the base percent modifiers applied and the value + // modifiers + override var baseModifiedValue: Double = 0.0 + + // Final modified value that includes the effective modifiers. + override var effectiveModifiedValue: Double = 0.0 + } + + sealed class Resource { + // null means no limit + abstract val max: Either? + abstract val delta: Either? + + abstract val name: String + + abstract var isLocked: Boolean + abstract var value: Double + + abstract val maxValue: Double? + + fun setAsPercentage(percent: Double) { + val maxValue = maxValue ?: throw IllegalArgumentException("$name does not have max value") + this.value = maxValue * percent + } + } + + private class ResourceImpl( + override val name: String, + override val max: Either?, + override val delta: Either? + ) : Resource() { + val actualValue = networkedFloat() + + override var value: Double + get() = actualValue.value + set(value) { + val maxValue = maxValue + + if (maxValue == null) { + actualValue.value = value + } else { + actualValue.value = value.coerceAtMost(maxValue) + } + } + + val actualIsLocked = networkedBoolean() + + override var isLocked: Boolean + get() = actualIsLocked.value + set(value) { actualIsLocked.value = value } + + override var maxValue: Double? = null + + private var defaultValue: Double = 0.0 + + fun mark() { + defaultValue = value + } + + fun reset() { + value = defaultValue + } + } + + // in original code it is named effectiveStats + private val liveStatsInternal = HashMap() + + private val statModifiersMap = NetworkedMap( + IntValueCodec, + NATIVE_MODIFIERS_CODEC to LEGACY_MODIFIERS_CODEC, + map = ListenableMap(Int2ObjectAVLTreeMap())) + + private val statModifiers = IdMap(map = statModifiersMap) + private val resourcesInternal = HashMap() + + val liveStats: Map = Collections.unmodifiableMap(liveStatsInternal) + val resources: Map = Collections.unmodifiableMap(resourcesInternal) + + init { + for ((statName, stat) in config.stats) { + val live = LiveStatImpl() + liveStatsInternal[statName] = live + live.baseValue = stat.baseValue + live.baseModifiedValue = stat.baseValue + } + + for ((resourceName, res) in config.resources) { + resourcesInternal[resourceName] = ResourceImpl( + resourceName, + max = res.maxValue?.let { Either.right(it) } ?: res.maxStat?.let { Either.left(it) }, + delta = res.deltaValue?.let { Either.right(it) } ?: res.deltaStat?.let { Either.left(it) }, + ) + } + + updateStats(0.0) + + for ((resourceName, res) in config.resources) { + val resource = resourcesInternal[resourceName]!! + + if (res.initialValue != null) { + resource.value = res.initialValue + } else if (res.initialPercentage != null) { + resource.setAsPercentage(res.initialPercentage) + } else if (resource.maxValue != null) { + resource.setAsPercentage(1.0) + } + + resource.mark() + } + + statNetworkGroup.add(statModifiersMap) + + for (k in resourcesInternal.keys.sorted()) { + val resource = resourcesInternal[k]!! + + statNetworkGroup.add(resource.actualValue) + statNetworkGroup.add(resource.actualIsLocked) + } + } + + fun resetResources() { + for (resource in resourcesInternal.values) { + resource.reset() + } + } + + fun addStatModifiers(modifiers: Collection): Int { + return statModifiers.add(ArrayList(modifiers)) + } + + fun removeStatModifiers(index: Int): Boolean { + return statModifiers.remove(index) != null + } + + private fun updateStats(delta: Double) { + // We use two intermediate values for calculating the effective stat value. + // The baseModifiedValue represents the application of the base percentage + // modifiers and the value modifiers, which only depend on the baseValue. + // The effectiveModifiedValue is the application of all effective percentage + // modifiers successively on the baseModifiedValue, causing them to stack with + // each other in addition to base multipliers and value modifiers + + val neverVisited = ObjectArraySet(liveStats.keys) + + for ((statName, stat) in config.stats) { + val live = liveStatsInternal[statName]!! + live.baseValue = stat.baseValue + live.baseModifiedValue = stat.baseValue + neverVisited.remove(statName) + } + + for (group in statModifiers.values) { + for (modifier in group) { + var live = liveStatsInternal[modifier.stat] + + if (live == null) { + live = LiveStatImpl() + liveStatsInternal[modifier.stat] = live + } + + neverVisited.remove(modifier.stat) + + if (modifier.type == StatModifierType.BASE_MULTIPLICATION) { + live.baseModifiedValue += (modifier.value - 1.0) * live.baseValue + } else if (modifier.type == StatModifierType.BASE_ADDITION) { + live.baseModifiedValue += modifier.value + } + } + } + + // Then we do all the StatEffectiveMultipliers and compute the + // final effectiveModifiedValue + + for (value in liveStatsInternal.values) + value.effectiveModifiedValue = value.baseModifiedValue + + for (group in statModifiers.values) { + for (modifier in group) { + val live = liveStatsInternal[modifier.stat]!! + + if (modifier.type == StatModifierType.OVERALL_MULTIPLICATION) { + live.effectiveModifiedValue *= modifier.value + } else if (modifier.type == StatModifierType.OVERALL_ADDITION) { + live.effectiveModifiedValue += modifier.value + } + } + } + + for (value in neverVisited) { + val live = liveStatsInternal[value]!! + + live.baseValue = 0.0 + live.effectiveModifiedValue = 0.0 + live.baseModifiedValue = 0.0 + } + + // Then update all the resources due to charging and percentage tracking, + // after updating the stats. + + for (resource in resourcesInternal.values) { + val oldMax = resource.maxValue + resource.maxValue = resource.max?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it }) + + // If the resource has a maximum value, rather than keeping the absolute + // value of the resource the same between updates, the resource value + // should instead track the percentage. + if (oldMax != null && resource.maxValue != null && resource.maxValue!! > 0.0) { + resource.value *= resource.maxValue!! / oldMax + } + + if (resource.maxValue != null) { + resource.value = resource.value + } + + if (delta > 0.0) { + resource.value += delta * (resource.delta?.map({ liveStatsInternal[it]?.effectiveModifiedValue }, { it }) ?: 0.0) + } + } + } + + companion object { + private val LEGACY_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.LEGACY_CODEC, ::ArrayList) + private val NATIVE_MODIFIERS_CODEC = StreamCodec.Collection(StatModifier.CODEC, ::ArrayList) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt index 1303202c..f7af0757 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/WorldObject.kt @@ -4,9 +4,9 @@ import com.google.common.collect.ImmutableMap import com.google.gson.JsonObject import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken +import it.unimi.dsi.fastutil.bytes.ByteArrayList import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import ru.dbotthepony.kommons.math.RGBAColor -import ru.dbotthepony.kommons.util.MailboxExecutorService import ru.dbotthepony.kommons.vector.Vector2i import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry @@ -15,7 +15,6 @@ import ru.dbotthepony.kstarbound.client.StarboundClient import ru.dbotthepony.kstarbound.client.render.LayeredRenderer import ru.dbotthepony.kstarbound.client.world.ClientWorld import ru.dbotthepony.kstarbound.defs.Drawable -import ru.dbotthepony.kstarbound.defs.JsonDriven import ru.dbotthepony.kstarbound.defs.image.SpriteReference import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation @@ -25,10 +24,9 @@ import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.world.Side import ru.dbotthepony.kstarbound.world.LightCalculator import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf -import ru.dbotthepony.kstarbound.world.World import ru.dbotthepony.kstarbound.world.api.TileColor import java.io.DataOutputStream -import kotlin.properties.Delegates +import java.util.HashMap open class WorldObject( val prototype: Registry.Entry, @@ -48,6 +46,10 @@ open class WorldObject( } } + override fun readDelta(stream: ByteArrayList, interpolationTime: Double, isLegacy: Boolean) { + TODO("Not yet implemented") + } + override fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean) { TODO("Not yet implemented") } @@ -74,7 +76,7 @@ open class WorldObject( inline val clientWorld get() = world as ClientWorld inline val serverWorld get() = world as ServerWorld inline val orientations get() = prototype.value.orientations - protected val renderParamLocations = Object2ObjectOpenHashMap String?>() + protected val renderParamLocations = HashMap String?>() private var frame = 0 set(value) { if (field != value) { @@ -139,14 +141,14 @@ open class WorldObject( override fun thinkShared() { super.thinkShared() - flickerPeriod?.update(Starbound.TICK_TIME_ADVANCE, world.random) + flickerPeriod?.update(Starbound.TIMESTEP, world.random) } override fun thinkRemote() { val orientation = orientation if (orientation != null) { - frameTimer = (frameTimer + Starbound.TICK_TIME_ADVANCE) % orientation.animationCycle + frameTimer = (frameTimer + Starbound.TIMESTEP) % orientation.animationCycle frame = (frameTimer / orientation.animationCycle * orientation.frames).toInt() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/HotbarIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/HotbarIndex.kt new file mode 100644 index 00000000..8d12b1ed --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/HotbarIndex.kt @@ -0,0 +1,77 @@ +package ru.dbotthepony.kstarbound.world.entities.player + +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot +import ru.dbotthepony.kstarbound.item.ItemStack +import java.io.DataInputStream +import java.io.DataOutputStream + +// same story as InventoryIndex +sealed class HotbarIndex { + abstract fun get(inventory: PlayerInventory): ItemStack + abstract fun write(stream: DataOutputStream) + + private object NoSelection : HotbarIndex() { + override fun get(inventory: PlayerInventory): ItemStack { + return ItemStack.EMPTY + } + + override fun write(stream: DataOutputStream) { + stream.writeByte(0) + } + } + + private class Essential(val index: EssentialSlot) : HotbarIndex() { + override fun get(inventory: PlayerInventory): ItemStack { + return inventory[index] + } + + override fun write(stream: DataOutputStream) { + stream.writeByte(2) + stream.writeByte(index.ordinal) + } + } + + private class Index(val index: Int) : HotbarIndex() { + init { + require(index in 0 .. 255) { "Hotbar index out of range: $index"} + } + + override fun get(inventory: PlayerInventory): ItemStack { + TODO("Not yet implemented") + } + + override fun write(stream: DataOutputStream) { + stream.writeByte(1) + stream.writeByte(index) + } + } + + companion object { + val CODEC = StreamCodec.Impl(::read, { a, b -> b.write(a) }) + + private val indices = Array(256) { Index(it) } + private val essentials = Array(EssentialSlot.entries.size) { Essential(EssentialSlot.entries[it]) } + + fun nothing(): HotbarIndex { + return NoSelection + } + + fun essential(slot: EssentialSlot): HotbarIndex { + return essentials[slot.ordinal] + } + + fun slot(slot: Int): HotbarIndex { + return indices[slot] + } + + fun read(stream: DataInputStream): HotbarIndex { + return when (val type = stream.readUnsignedByte()) { + 0 -> NoSelection + 1 -> indices[stream.readUnsignedByte()] + 2 -> essentials[stream.readUnsignedByte()] + else -> throw IndexOutOfBoundsException("Unknown HotbarIndex type: $type") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/InventoryIndex.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/InventoryIndex.kt new file mode 100644 index 00000000..dfc9a55c --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/InventoryIndex.kt @@ -0,0 +1,142 @@ +package ru.dbotthepony.kstarbound.world.entities.player + +import ru.dbotthepony.kommons.guava.immutableList +import ru.dbotthepony.kommons.guava.immutableMap +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot +import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.io.readInternedString +import java.io.DataInputStream +import java.io.DataOutputStream + +// sigh +// must do this to remain compatible with original network protocol +sealed class InventoryIndex { + abstract fun get(inventory: PlayerInventory): ItemStack + abstract fun set(inventory: PlayerInventory, value: ItemStack) + abstract fun write(stream: DataOutputStream, isLegacy: Boolean) + + open fun isValid(inventory: PlayerInventory): Boolean { + return true + } + + private data class BagIndex(val bag: String, val slot: Int) : InventoryIndex() { + init { + require(slot >= 0) { "Negative slot index: $slot" } + } + + override fun get(inventory: PlayerInventory): ItemStack { + return (inventory.bags[bag] ?: throw NoSuchElementException("No such bag $bag"))[slot] + } + + override fun set(inventory: PlayerInventory, value: ItemStack) { + (inventory.bags[bag] ?: throw NoSuchElementException("No such bag $bag"))[slot] = value + } + + override fun isValid(inventory: PlayerInventory): Boolean { + val bag = inventory.bags[bag] ?: return false + return slot in 0 until bag.size + } + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(1) + + stream.writeBinaryString(bag) + + // god help us if new client joins original server + // with mods which make inventory size absurd + if (isLegacy) + stream.writeByte(slot) + else + stream.writeVarInt(slot) + } + } + + private data class EquipmentIndex(val slot: EquipmentSlot) : InventoryIndex() { + override fun get(inventory: PlayerInventory): ItemStack { + return inventory[slot] + } + + override fun set(inventory: PlayerInventory, value: ItemStack) { + inventory[slot] = value + } + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(0) + stream.writeByte(slot.ordinal) + } + } + + private object Hand : InventoryIndex() { + override fun get(inventory: PlayerInventory): ItemStack { + return inventory.handSlot + } + + override fun set(inventory: PlayerInventory, value: ItemStack) { + inventory.handSlot = value + } + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(2) + } + } + + private object Trash : InventoryIndex() { + override fun get(inventory: PlayerInventory): ItemStack { + return inventory.trashSlot + } + + override fun set(inventory: PlayerInventory, value: ItemStack) { + inventory.trashSlot = value + } + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeByte(3) + } + } + + companion object { + private val bagIndexCache by lazy { + immutableMap { + GlobalDefaults.player.inventory.itemBags.keys.forEach { n -> + put(n, immutableList(256) { + BagIndex(n, it) + }) + } + } + } + + private val equipment = immutableList(EquipmentSlot.entries.size) { + EquipmentIndex(EquipmentSlot.entries[it]) + } + + fun hand(): InventoryIndex { + return Hand + } + + fun trash(): InventoryIndex { + return Trash + } + + fun equipment(slot: EquipmentSlot): InventoryIndex { + return equipment[slot.ordinal]!! + } + + fun bag(name: String, index: Int): InventoryIndex { + return bagIndexCache[name]?.getOrNull(index) ?: BagIndex(name, index) + } + + fun read(stream: DataInputStream, isLegacy: Boolean): InventoryIndex { + return when (val type = stream.readUnsignedByte()) { + 0 -> equipment[stream.readUnsignedByte()]!! + 1 -> bag(stream.readInternedString(), if (isLegacy) stream.readUnsignedByte() else stream.readVarInt()) + 2 -> hand() + 3 -> trash() + else -> throw IllegalArgumentException("Unknown InventoryIndex type $type!") + } + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt similarity index 58% rename from src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index e7961389..87acc150 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -1,24 +1,31 @@ -package ru.dbotthepony.kstarbound.world.entities +package ru.dbotthepony.kstarbound.world.entities.player import com.google.gson.JsonObject +import it.unimi.dsi.fastutil.bytes.ByteArrayList import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue import ru.dbotthepony.kommons.vector.Vector2d +import ru.dbotthepony.kstarbound.GlobalDefaults import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.actor.HumanoidData import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote +import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode +import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable import ru.dbotthepony.kstarbound.math.Interpolator -import ru.dbotthepony.kstarbound.network.syncher.GroupElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEnum import ru.dbotthepony.kstarbound.network.syncher.networkedEnumExtraStupid -import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.world.entities.Animator +import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity +import ru.dbotthepony.kstarbound.world.entities.StatusController +import java.io.DataInputStream import java.io.DataOutputStream import java.util.UUID import kotlin.properties.Delegates @@ -38,11 +45,31 @@ class PlayerEntity() : HumanoidActorEntity("/") { LOUNGE("Lounge"); } + constructor(data: DataInputStream, isLegacy: Boolean) : this() { + uniqueID = data.readInternedString() + println(data.readInternedString()) + gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()] + humanoidData = HumanoidData.read(data, isLegacy) + println(humanoidData) + } + + var gamemode = PlayerGamemode.CASUAL + override fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean) { TODO("Not yet implemented") } - val networkGroup = MasterElement(GroupElement()) + val inventory = PlayerInventory() + val songbook = Songbook(this) + val effectAnimator = if (GlobalDefaults.player.effectsAnimator.value == null) Animator() else Animator(GlobalDefaults.player.effectsAnimator.value!!) + override val statusController = StatusController(this, GlobalDefaults.player.statusControllerSettings) + val techController = TechController(this) + + val networkGroup = MasterElement(NetworkedGroup()) + + override fun readDelta(stream: ByteArrayList, interpolationTime: Double, isLegacy: Boolean) { + networkGroup.read(stream, interpolationTime, isLegacy) + } var state by networkGroup.upstream.add(networkedEnum(State.IDLE)) var shifting by networkGroup.upstream.add(networkedBoolean()) @@ -55,6 +82,18 @@ class PlayerEntity() : HumanoidActorEntity("/") { val newChatMessage = networkGroup.upstream.add(networkedEventCounter()) var emote by networkGroup.upstream.add(networkedEnumExtraStupid(HumanoidEmote.IDLE)) + init { + networkGroup.upstream.add(inventory.networkGroup) + networkGroup.upstream.add(toolsNetworkGroup) + networkGroup.upstream.add(armorNetworkGroup) + networkGroup.upstream.add(songbook.networkGroup) + networkGroup.upstream.add(movement.networkGroup) + networkGroup.upstream.add(effects.networkGroup) + networkGroup.upstream.add(effectAnimator.networkGroup) + networkGroup.upstream.add(statusController) + networkGroup.upstream.add(techController.networkGroup) + } + override val aimPosition: Vector2d get() = Vector2d(xAimPosition, yAimPosition) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt new file mode 100644 index 00000000..bbb7f37e --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerInventory.kt @@ -0,0 +1,150 @@ +package ru.dbotthepony.kstarbound.world.entities.player + +import com.google.common.collect.ImmutableMap +import it.unimi.dsi.fastutil.objects.Object2LongAVLTreeMap +import ru.dbotthepony.kommons.arrays.Object2DArray +import ru.dbotthepony.kommons.guava.immutableList +import ru.dbotthepony.kommons.io.BinaryStringCodec +import ru.dbotthepony.kommons.io.LongValueCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.UnsignedVarLongCodec +import ru.dbotthepony.kommons.io.VarLongValueCodec +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.GlobalDefaults +import ru.dbotthepony.kstarbound.defs.actor.EquipmentSlot +import ru.dbotthepony.kstarbound.defs.actor.EssentialSlot +import ru.dbotthepony.kstarbound.item.ItemStack +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedItem +import ru.dbotthepony.kstarbound.network.syncher.networkedUnsignedInt +import ru.dbotthepony.kstarbound.item.IContainer +import ru.dbotthepony.kstarbound.network.syncher.InternedStringCodec +import ru.dbotthepony.kstarbound.network.syncher.NetworkedItemStack +import ru.dbotthepony.kstarbound.network.syncher.NetworkedMap +import java.io.DataInputStream +import java.io.DataOutputStream + +class PlayerInventory { + inner class Bag(override val size: Int) : IContainer { + val slots = immutableList(size) { networkedItem() } + + override fun get(index: Int): ItemStack { + return slots[index].get() + } + + override fun set(index: Int, value: ItemStack) { + slots[index].accept(value) + } + } + + data class HotbarSlot(val left: KOptional = KOptional(), val right: KOptional = KOptional()) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readKOptional { InventoryIndex.read(stream, isLegacy) }, stream.readKOptional { InventoryIndex.read(stream, isLegacy) }) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeKOptional(left) { it.write(this, isLegacy) } + stream.writeKOptional(right) { it.write(this, isLegacy) } + } + + companion object { + val CODEC = nativeCodec(::HotbarSlot, HotbarSlot::write) + val LEGACY_CODEC = legacyCodec(::HotbarSlot, HotbarSlot::write) + } + } + + // here it gets interesting, original code is using List#sorted, which itself uses Star::sort, + // which is just an alias for std::sort, and std::sort is ***not*** stable sort, meaning + // if bags have same priority, PlayerInventory behavior becomes undefined + // We, on the other hand, use stable sort provided by Java + // (and if bags have the same priority they will appear in order they are defined in json) + val bags: ImmutableMap = GlobalDefaults.player.inventory.itemBags.entries + .stream() + .sorted { o1, o2 -> o1.value.priority.compareTo(o2.value.priority) } + .map { it.key to Bag(it.value.size) } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + + val networkGroup = NetworkedGroup() + + val equipment: ImmutableMap = EquipmentSlot.entries + .stream() + .map { it to networkedItem() } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + + val essentialSlots: ImmutableMap = EssentialSlot.entries + .stream() + .map { it to networkedItem() } + .collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + + private val currencies = NetworkedMap(keyCodec = InternedStringCodec, valueCodec = UnsignedVarLongCodec to LongValueCodec, isDumb = true) + + init { + // this is required for original engine + // because otherwise + // it will throw "element not found" + // when trying to update currencies + for (key in GlobalDefaults.currencies.keys) { + currencies[key] = 0L + } + } + + operator fun get(index: EquipmentSlot): ItemStack { + return equipment[index]!!.get() + } + + operator fun set(index: EquipmentSlot, value: ItemStack) { + equipment[index]!!.accept(value) + } + + operator fun get(index: EssentialSlot): ItemStack { + return essentialSlots[index]!!.get() + } + + operator fun set(index: EssentialSlot, value: ItemStack) { + essentialSlots[index]!!.accept(value) + } + + init { + equipment.values.forEach { + networkGroup.add(it) + } + + bags.values.forEach { + it.slots.forEach { + networkGroup.add(it) + } + } + } + + // "swap slot" in original sources + var handSlot by networkGroup.add(networkedItem()) + var trashSlot by networkGroup.add(networkedItem()) + + init { + networkGroup.add(currencies) + } + + var hotbarGroup by networkGroup.add(networkedUnsignedInt()) + val hotbarSlots = Object2DArray(GlobalDefaults.player.inventory.customBarIndexes, GlobalDefaults.player.inventory.customBarGroups) { _, _ -> + networkedData(HotbarSlot(), HotbarSlot.CODEC, HotbarSlot.LEGACY_CODEC) + } + + init { + for (x in 0 until hotbarSlots.rows) { + for (y in 0 until hotbarSlots.columns) { + networkGroup.add(hotbarSlots[y, x]) + } + } + } + + var hotbarIndex by networkGroup.add(networkedData(HotbarIndex.nothing(), HotbarIndex.CODEC)) + + init { + essentialSlots.values.forEach { networkGroup.add(it) } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/Songbook.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/Songbook.kt new file mode 100644 index 00000000..ca270421 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/Songbook.kt @@ -0,0 +1,21 @@ +package ru.dbotthepony.kstarbound.world.entities.player + +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedJsonElement +import ru.dbotthepony.kstarbound.network.syncher.networkedSignedInt +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import java.io.File + +// as separate class because it contains plentiful of internal logic +// (avoids pollution in main class) +class Songbook(val player: PlayerEntity) { + val networkGroup = NetworkedGroup() + var song by networkGroup.add(networkedJsonElement()) + var songEpoch by networkGroup.add(networkedSignedInt()) + var timeSource by networkGroup.add(networkedString()) + var isActive by networkGroup.add(networkedBoolean()) + var instrument by networkGroup.add(networkedString()) +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/TechController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/TechController.kt new file mode 100644 index 00000000..d0f49776 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/TechController.kt @@ -0,0 +1,91 @@ +package ru.dbotthepony.kstarbound.world.entities.player + +import ru.dbotthepony.kommons.io.IntValueCodec +import ru.dbotthepony.kommons.io.StreamCodec +import ru.dbotthepony.kommons.io.koptional +import ru.dbotthepony.kommons.io.map +import ru.dbotthepony.kommons.io.readKOptional +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeKOptional +import ru.dbotthepony.kommons.util.KOptional +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.network.syncher.NetworkedDynamicGroup +import ru.dbotthepony.kstarbound.network.syncher.NetworkedElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedData +import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint +import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.world.entities.Animator +import java.io.DataInputStream +import java.io.DataOutputStream + +// i am very disappointed. +class TechController(val player: PlayerEntity) { + val networkGroup = NetworkedGroup() + + private val animators = NetworkedDynamicGroup(::TechAnimator, { it }).also { networkGroup.add(it) } + + var state by networkedData(KOptional(), STATE_CODEC, STATE_CODEC_LEGACY).also { networkGroup.add(it) } + private set + var directives by networkedString().also { networkGroup.add(it) } + private set + var xOffset by networkedFixedPoint(0.003125).also { networkGroup.add(it) } + private set + var yOffset by networkedFixedPoint(0.003125).also { networkGroup.add(it) } + private set + var isHidden by networkedBoolean().also { networkGroup.add(it) } + private set + var isToolUsageSuppressed by networkedBoolean().also { networkGroup.add(it) } + private set + + enum class State(override val jsonName: String) : IStringSerializable { + STAND("Stand"), + FLY("Fly"), + FALL("Fall"), + SIT("Sit"), + LAY("Lay"), + DUCK("Duck"), + WALK("Walk"), + RUN("Run"), + SWIM("Swim"); + } + + private class TechAnimator(var config: KOptional = KOptional()) : NetworkedElement.Passthrough() { + val networkGroup = NetworkedGroup() + + var animator: Animator = Animator().also { networkGroup.add(it.networkGroup) } + private set(value) { + networkGroup.replace(field.networkGroup, value.networkGroup) + field = value + } + + var isVisible by networkedBoolean().also { networkGroup.add(it) } + + override val parentElement: NetworkedElement + get() = networkGroup + + override fun readInitial(data: DataInputStream, isLegacy: Boolean) { + val old = config + config = data.readKOptional { readInternedString() } + + if (old != config) + animator = config.map { Animator.load(it) }.orElse { Animator() } + + super.readInitial(data, isLegacy) + } + + override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) { + data.writeKOptional(config) { writeBinaryString(it) } + super.writeInitial(data, isLegacy) + } + } + + companion object { + private val STATE_CODEC = StreamCodec.Enum(State::class.java).koptional() + private val STATE_CODEC_LEGACY = IntValueCodec.map({ State.entries[this] }, { ordinal }).koptional() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 6c282f3d..362f3748 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -26,6 +26,8 @@ import ru.dbotthepony.kommons.io.writeCollection import ru.dbotthepony.kommons.io.writeStruct2d import ru.dbotthepony.kommons.io.writeStruct2f import ru.dbotthepony.kstarbound.json.listAdapter +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import java.io.DataInputStream import java.io.DataOutputStream import kotlin.math.absoluteValue @@ -356,8 +358,8 @@ class Poly private constructor(val edges: ImmutableList, val vertices: Imm } companion object : TypeAdapterFactory { - val CODEC = StreamCodec.Impl({ read(it, false) }, { a, b -> b.write(a, false) }) - val LEGACY_CODEC = StreamCodec.Impl({ read(it, true) }, { a, b -> b.write(a, true) }) + val CODEC = nativeCodec(::read, Poly::write) + val LEGACY_CODEC = legacyCodec(::read, Poly::write) val EMPTY = Poly(ImmutableList.of(), ImmutableList.of()) diff --git a/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt b/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt index b5f25a2b..2a67d8e9 100644 --- a/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt +++ b/src/test/kotlin/ru/dbotthepony/kstarbound/test/NetworkedElementTests.kt @@ -9,8 +9,7 @@ import ru.dbotthepony.kommons.io.Vector2fCodec import ru.dbotthepony.kommons.vector.Vector2f import ru.dbotthepony.kstarbound.network.syncher.BasicNetworkedElement import ru.dbotthepony.kstarbound.network.syncher.EventCounterElement -import ru.dbotthepony.kstarbound.network.syncher.FloatingNetworkedElement -import ru.dbotthepony.kstarbound.network.syncher.GroupElement +import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup import ru.dbotthepony.kstarbound.network.syncher.MasterElement import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedDouble @@ -45,7 +44,7 @@ object NetworkedElementTests { val masterField10 = EventCounterElement() val masterField11 = BasicNetworkedElement(Vector2f.ZERO, Vector2fCodec) - val master = MasterElement(GroupElement()) + val master = MasterElement(NetworkedGroup()) master.upstream.add(masterField1) master.upstream.add(masterField2) master.upstream.add(masterField3) @@ -94,7 +93,7 @@ object NetworkedElementTests { val slaveField10 = EventCounterElement() val slaveField11 = BasicNetworkedElement(Vector2f.ZERO, Vector2fCodec) - val slave = MasterElement(GroupElement()) + val slave = MasterElement(NetworkedGroup()) slave.upstream.add(slaveField1) slave.upstream.add(slaveField2) slave.upstream.add(slaveField3)