Functional serverside player network receive

This commit is contained in:
DBotThePony 2024-03-27 14:30:44 +07:00
parent 551eac4072
commit 9c1772a766
Signed by: DBot
GPG Key ID: DCC23B5715498507
98 changed files with 3773 additions and 711 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<PlayerConfig>()
private set
var actorMovementParameters = ActorMovementParameters()
private set
@ -61,6 +66,9 @@ object GlobalDefaults {
var universeServer by Delegates.notNull<UniverseServerConfig>()
private set
var currencies by Delegates.notNull<ImmutableMap<String, CurrencyDefinition>>()
private set
private object EmptyTask : ForkJoinTask<Unit>() {
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
}

View File

@ -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<Entry>(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<Entry>(1).also {
input2recipesBacking[p as String] = Collections.unmodifiableList(it)
}

View File

@ -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<LiquidDefinition>("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val particles = Registry<ParticleDefinition>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val particles = Registry<ParticleConfig>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val items = Registry<IItemDefinition>("item").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) }
val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) }
@ -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)))

View File

@ -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<T : Any>(val name: String) {
private val keysInternal = Object2ObjectOpenHashMap<String, Impl>()
private val keysInternal = HashMap<String, Impl>()
private val idsInternal = Int2ObjectOpenHashMap<Impl>()
private val keyRefs = Object2ObjectOpenHashMap<String, RefImpl>()
private val keyRefs = HashMap<String, RefImpl>()
private val idRefs = Int2ObjectOpenHashMap<RefImpl>()
private val backlog = ConcurrentLinkedQueue<Runnable>()
@ -45,7 +46,7 @@ class Registry<T : Any>(val name: String) {
private val lock = ReentrantLock()
val keys: Object2ObjectMap<String, out Entry<T>> = Object2ObjectMaps.unmodifiable(keysInternal)
val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal)
val ids: Int2ObjectMap<out Entry<T>> = Int2ObjectMaps.unmodifiable(idsInternal)
sealed class Ref<T : Any> : Supplier<Entry<T>?> {
@ -56,6 +57,9 @@ class Registry<T : Any>(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<T : Any>(val name: String) {
abstract val file: IStarboundFile?
abstract val registry: Registry<T>
abstract val isBuiltin: Boolean
abstract val ref: Ref<T>
fun traverseJsonPath(path: String): JsonElement? {
return traverseJsonPath(path, json)
@ -94,6 +99,8 @@ class Registry<T : Any>(val name: String) {
override var file: IStarboundFile? = null
override var isBuiltin: Boolean = false
override val ref: Ref<T> by lazy { ref(key) }
override fun equals(other: Any?): Boolean {
return this === other
}
@ -168,7 +175,7 @@ class Registry<T : Any>(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<T : Any>(val name: String) {
}
fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
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 ?: "<code>"})")
@ -211,6 +220,8 @@ class Registry<T : Any>(val name: String) {
}
fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry<T> {
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 ?: "<code>"})")
@ -243,6 +254,8 @@ class Registry<T : Any>(val name: String) {
}
fun add(key: String, value: T, isBuiltin: Boolean = false): Entry<T> {
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 <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
@ -270,6 +283,8 @@ class Registry<T : Any>(val name: String) {
}
fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry<T> {
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 <code>; old def originate from ${keysInternal[key]?.file ?: "<code>"})")
@ -302,6 +317,8 @@ class Registry<T : Any>(val name: String) {
}
}
val emptyRef = ref("")
companion object {
private val LOGGER = LogManager.getLogger()
}

View File

@ -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 {

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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)
}

View File

@ -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<T : Any>(val min: Int = 0, val max: Int = Int.MAX_VALUE, private val map: MutableMap<Int, T> = Int2ObjectAVLTreeMap()) : MutableMap<Int, T> 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()
}
}

View File

@ -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 <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == AssetPath::class.java) {

View File

@ -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<V>(val path: String?, val fullPath: String?, val value: V?, val json: JsonElement?) {
companion object : TypeAdapterFactory {
@ -30,7 +31,7 @@ data class AssetReference<V>(val path: String?, val fullPath: String?, val value
val param = type.type as? ParameterizedType ?: return null
return object : TypeAdapter<AssetReference<T>>() {
private val cache = Collections.synchronizedMap(Object2ObjectOpenHashMap<String, Pair<T, JsonElement>>())
private val cache = Collections.synchronizedMap(HashMap<String, Pair<T, JsonElement>>())
private val adapter = gson.getAdapter(TypeToken.get(param.actualTypeArguments[0])) as TypeAdapter<T>
private val strings = gson.getAdapter(String::class.java)
private val jsons = gson.getAdapter(JsonElement::class.java)

View File

@ -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<IItemDefinition>,
val playerMax: Long = 999999,
)

View File

@ -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<IItemDefinition>,
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<String> = Interner { it }) : TypeAdapterFactory {
override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType == ItemReference::class.java) {
return object : TypeAdapter<ItemReference>() {
private val regularObject = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = false), gson, stringInterner)
private val regularList = FactoryAdapter.createFor(ItemReference::class, JsonFactory(asList = true), gson, stringInterner)
private val references = gson.getAdapter(TypeToken.getParameterized(Registry.Ref::class.java, IItemDefinition::class.java)) as TypeAdapter<Registry.Ref<IItemDefinition>>
override fun write(out: JsonWriter, value: ItemReference?) {
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<T>
}
return null
}
}
}

View File

@ -22,10 +22,10 @@ import kotlin.reflect.javaType
*/
abstract class JsonDriven(val path: String) {
private val delegates = ArrayList<Property<*>>()
private val delegatesMap = Object2ObjectOpenHashMap<String, ArrayList<Property<*>>>()
private val delegatesMap = HashMap<String, ArrayList<Property<*>>>()
private val lazies = ArrayList<LazyData<*>>()
private val namedLazies = Object2ObjectOpenHashMap<String, ArrayList<LazyData<*>>>()
private val namedLazies = HashMap<String, ArrayList<LazyData<*>>>()
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

View File

@ -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<String> = ImmutableSet.copyOf(names)
}
data class StatModifier(val stat: String, val value: Double, val type: StatModifierType) {
class Adapter(gson: Gson) : TypeAdapter<StatModifier>() {
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")
}
}
}
}

View File

@ -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<StatModifier>() {
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)
}
}
}

View File

@ -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<String> = ImmutableSet.copyOf(names)
}

View File

@ -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<AssetPath> = ImmutableList.of(),
val primaryScriptDelta: Int = 1,
val keepDamageNotificationSteps: Int = 120,
val stats: ImmutableMap<String, Stat> = ImmutableMap.of(),
val resources: ImmutableMap<String, Resource> = 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,
)
}

View File

@ -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");
}

View File

@ -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

View File

@ -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<String, Bag>,
) {
@ -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
}
}
)
}

View File

@ -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 })
}
}

View File

@ -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<String> = ImmutableList.of(),
@ -75,4 +76,4 @@ data class PlayerConfig(
val genericScriptContexts: ImmutableMap<String, String>,
val inventory: InventoryConfig,
val inventoryFilters: ImmutableMap<String, BagFilterConfig>,
)
)

View File

@ -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 })
}
}

View File

@ -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<ItemReference>,
val output: ItemReference,
val input: ImmutableList<ItemDescriptor>,
val output: ItemDescriptor,
val groups: ImmutableSet<String> = ImmutableSet.of(),
val matchInputParameters: Boolean = false,
val duration: Double = 0.5,

View File

@ -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<String> = ImmutableList.of(),
val primaryScriptDelta: Int,
val stats: ImmutableMap<String, Stat> = ImmutableMap.of(),
val resources: ImmutableMap<String, Resource> = 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,
)
}

View File

@ -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<String, StateType> = ImmutableMap.of(),
val parts: ImmutableMap<String, Part> = 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<String, State> = 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<String, ImmutableMap<String, State>> = ImmutableMap.of(),
) {
@JsonFactory
data class State(
val properties: JsonObject = JsonObject(),
val frameProperties: JsonObject = JsonObject(),
)
}
}

View File

@ -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<String, Either<ImmutableList<String>, CustomSound>> = ImmutableMap.of(),
val transformationGroups: ImmutableMap<String, TransformConfig> = ImmutableMap.of(),
val particleEmitters: ImmutableMap<String, ParticleEmitter> = ImmutableMap.of(),
val transformationGroups: ImmutableMap<String, Transform> = ImmutableMap.of(),
val rotationGroups: ImmutableMap<String, Rotation> = ImmutableMap.of(),
val particleEmitters: ImmutableMap<String, AnimationParticles> = ImmutableMap.of(),
val lights: ImmutableMap<String, Light> = ImmutableMap.of(),
val effects: ImmutableMap<String, Effect> = ImmutableMap.of(),
val globalTagDefaults: ImmutableMap<String, String> = ImmutableMap.of(),
val partTagDefaults: ImmutableMap<String, ImmutableMap<String, String>> = ImmutableMap.of(),
) {
@JsonFactory
data class CustomSound(
val pool: ImmutableList<String>,
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<String, StateType> = ImmutableMap.of(),
val parts: ImmutableMap<String, Part> = 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<String> = ImmutableList.of(),
val particles: ImmutableList<AnimationParticle>,
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<String, State> = 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<String, ImmutableMap<String, State>> = ImmutableMap.of(),
) {
@JsonFactory
data class Properties(
val fullbright: Boolean? = null,
val centered: Boolean? = null,
val transformationGroups: ImmutableList<String>? = 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<Registry.Ref<ParticleConfig>, 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<String> = 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<AssetPath> = 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
)
}

View File

@ -1,5 +0,0 @@
package ru.dbotthepony.kstarbound.defs.animation
enum class DestructionAction {
FADE
}

View File

@ -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<Particle>() {
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`))
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<ItemDescri
return Supplier { result.value }
}
fun ItemDescriptor(stream: DataInputStream): ItemDescriptor {
val name = stream.readInternedString()
val count = stream.readVarLong()
val params = stream.readJsonElement()
if (name == "")
return ItemDescriptor.EMPTY
return ItemDescriptor(name, count, if (params === JsonNull.INSTANCE || params == EMPTY_JSON) EMPTY_JSON else params as JsonObject)
}
/**
* Describes a prototype of item
*
* [parameters] is considered to be immutable and should not be modified
* directly (must be copied for mutable context)
*/
data class ItemDescriptor(
val name: String,
val count: Long,
val parameters: JsonObject = JsonObject()
val parameters: JsonObject = EMPTY_JSON
) {
val isEmpty get() = count <= 0L
constructor(ref: Registry.Ref<IItemDefinition>, 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<ItemDescriptor>() {
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) })
}
}

View File

@ -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<Piece>) {
data class Piece(
val level: Double,
val pool: ImmutableList<PoolEntry> = ImmutableList.of(),
val fill: ImmutableList<Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
val fill: ImmutableList<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>> = ImmutableList.of(),
val poolRounds: IPoolRounds = OneRound,
// TODO: что оно делает?
// оно точно не запрещает ему появляться несколько раз за одну генерацию treasure pool
@ -150,7 +149,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
data class PoolEntry(
val weight: Double,
val treasure: Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>
val treasure: Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>
) {
init {
require(weight > 0.0) { "Invalid pool entry weight: $weight" }
@ -161,7 +160,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType === TreasurePoolDefinition::class.java) {
return object : TypeAdapter<TreasurePoolDefinition>() {
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<Registry.Ref<TreasurePoolDefinition>>
private val objReader = gson.getAdapter(JsonObject::class.java)
@ -189,7 +188,7 @@ class TreasurePoolDefinition(pieces: List<Piece>) {
val things = objReader.read(`in`)
val pool = ImmutableList.Builder<PoolEntry>()
val fill = ImmutableList.Builder<Either<ItemReference, Registry.Ref<TreasurePoolDefinition>>>()
val fill = ImmutableList.Builder<Either<ItemDescriptor, Registry.Ref<TreasurePoolDefinition>>>()
var poolRounds: IPoolRounds = OneRound
val allowDuplication = things["allowDuplication"]?.asBoolean ?: false

View File

@ -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<TreasurePoolDefinition>? = null,
// null - not specified, empty list - always drop nothing
val breakDropOptions: ImmutableList<ImmutableList<ItemReference>>? = null,
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemReference>> = ImmutableList.of(),
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
//val animation: AssetReference<AnimationDefinition>? = null,
val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),
@ -96,9 +96,9 @@ data class ObjectDefinition(
val retainObjectParametersInItem: Boolean = false,
val breakDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
// null - not specified, empty list - always drop nothing
val breakDropOptions: ImmutableList<ImmutableList<ItemReference>>? = null,
val breakDropOptions: ImmutableList<ImmutableList<ItemDescriptor>>? = null,
val smashDropPool: Registry.Ref<TreasurePoolDefinition>? = null,
val smashDropOptions: ImmutableList<ImmutableList<ItemReference>> = ImmutableList.of(),
val smashDropOptions: ImmutableList<ImmutableList<ItemDescriptor>> = ImmutableList.of(),
//val animation: AssetReference<AnimationDefinition>? = null,
val animation: AssetPath? = null,
val smashSounds: ImmutableSet<AssetPath> = ImmutableSet.of(),

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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<AnimationDefinition>? = 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

View File

@ -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<Registry.Ref<ParticleDefinition>, 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

View File

@ -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
)

View File

@ -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<String> = ImmutableList.of(),
val particles: ImmutableList<ParticleCreator>,
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

View File

@ -1,5 +0,0 @@
package ru.dbotthepony.kstarbound.defs.particle
enum class ParticleLayer {
FRONT
}

View File

@ -1,8 +0,0 @@
package ru.dbotthepony.kstarbound.defs.particle
enum class ParticleType {
ANIMATED,
TEXTURED,
EMBER,
TEXT
}

View File

@ -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

View File

@ -34,7 +34,7 @@ private fun flattenJsonArray(input: JsonArray, interner: Interner<String> = Inte
}
private fun flattenJsonObject(input: JsonObject, interner: Interner<String> = Interner { it }): MutableMap<String, Any> {
val flattened = Object2ObjectOpenHashMap<String, Any>()
val flattened = HashMap<String, Any>()
for ((k, v) in input.entrySet()) {
when (v) {

View File

@ -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<String>,
val alpha: Double,
val parallaxValue: Vector2d,

View File

@ -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<AABB> {
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<AABB>) {
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)
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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<IItemDefinition>?, count: Long, val parameters: JsonObject, marker: Unit) {
constructor(item: Registry.Entry<IItemDefinition>, 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<IItemDefinition>? = 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<IItemDefinition>
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<IItemDefinition>?, 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<IItemDefinition>?, 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<IItemDefinition>?, 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<IItemDefinition>?, 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<IItemDefinition>?, 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<IItemDefinition>?, 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
}
}
}

View File

@ -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())

View File

@ -96,7 +96,7 @@ operator fun Table.iterator(): Iterator<Map.Entry<Any, Any>> {
fun Table.toJson(): JsonElement {
val arrayValues = Long2ObjectAVLTreeMap<Any>()
val hashValues = Object2ObjectOpenHashMap<Any, Any>()
val hashValues = HashMap<Any, Any>()
for ((k, v) in this) {
if (k is Number) {
@ -150,14 +150,8 @@ fun Table.toJson(): JsonElement {
}
fun Table.toJsonObject(): JsonObject {
val hashValues = Object2ObjectOpenHashMap<Any, Any>()
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) {

View File

@ -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 {

View File

@ -79,7 +79,7 @@ class JsonRPC {
private val pendingWrite = ArrayList<Entry>()
private val responses = Int2ObjectOpenHashMap<CompletableFuture<JsonElement>>()
private val lock = ReentrantLock()
private val handlers = Object2ObjectOpenHashMap<String, Callback>()
private val handlers = HashMap<String, Callback>()
fun add(name: String, callback: Callback): (JsonElement) -> CompletableFuture<JsonElement> {
val old = handlers.put(name, callback)

View File

@ -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()

View File

@ -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) {

View File

@ -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<By
}
override fun play(connection: ServerConnection) {
connection.enqueue {
for ((id, delta) in deltas) {
if (id !in connection.entityIDRange) {
LOGGER.error("Player $connection tried to update entity with ID $id, but that's outside of allowed range ${connection.entityIDRange}!")
} else {
entities[id]?.readDelta(ByteArrayList.wrap(delta), 0.0, connection.isLegacy)
}
}
}
}
override fun play(connection: ClientConnection) {
@ -49,5 +59,7 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<By
return EntityUpdateSetPacket(forConnection, deltas)
}
private val LOGGER = LogManager.getLogger()
}
}

View File

@ -8,12 +8,16 @@ import java.io.DataOutputStream
import java.util.*
import java.util.function.Consumer
open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, val codec: StreamCodec<TYPE>, val legacyCodec: StreamCodec<LEGACY>, val toLegacy: (TYPE) -> LEGACY, val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate<TYPE> {
open class BasicNetworkedElement<TYPE, LEGACY>(private var value: TYPE, protected val codec: StreamCodec<TYPE>, protected val legacyCodec: StreamCodec<LEGACY>, protected val toLegacy: (TYPE) -> LEGACY, protected val fromLegacy: (LEGACY) -> TYPE) : NetworkedElement(), ListenableDelegate<TYPE> {
protected val valueListeners = Listenable.Impl<TYPE>()
protected val queue = LinkedList<Pair<Double, TYPE>>()
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

View File

@ -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 <TYPE> BasicNetworkedElement(value: TYPE, codec: StreamCodec<TYPE>): 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 <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
fun <T> networkedData(value: T, codec: StreamCodec<T>) = BasicNetworkedElement(value, codec)
fun <T> networkedData(value: T, codec: StreamCodec<T>, legacyCodec: StreamCodec<T>) = BasicNetworkedElement(value, codec, legacyCodec, { it }, { it })
fun networkedAABB(value: AABB = AABB.ZERO) = networkedData(value, AABBCodecNative, AABBCodecLegacy)
fun networkedAABBNullable(value: KOptional<AABB> = KOptional()) = networkedData(value, AABBCodecNative.koptional(), AABBCodecLegacyOptional)
// this is ugly because of invariant generics, but we must bear with it.
fun <E> networkedList(codec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>>
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 <S> networkedSignals(codec: StreamCodec<S>) = NetworkedSignal(codec)
fun <S> networkedSignals(codec: StreamCodec<S>, 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 <reified T> networkedJson(value: T, adapter: TypeAdapter<T> = 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 <T> nativeCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec<T> {
return StreamCodec.Impl({ reader(it, false) }, { a, b -> writer(b, a, false) })
}
@ -99,7 +148,12 @@ fun <T> 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 <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
// networks enum as a signed variable length integer on legacy protocol
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
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 <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
// networks enum as string on legacy protocol
fun <E : Enum<E>> networkedEnumExtraStupid(value: E): BasicNetworkedElement<E, String> {
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) })
}

View File

@ -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<Double>()
override fun accept(t: Double) {

View File

@ -66,6 +66,10 @@ class MasterElement<E : NetworkedElement>(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)
}

View File

@ -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<T : Any>(private val factory: () -> T, private val accessor: (T) -> NetworkedElement, private val maxBacklogSize: Int = 100) : NetworkedElement() {
private val elementsInternal = IdMap<T>(min = 1)
val elements: Map<Int, T> = Collections.unmodifiableMap(elementsInternal)
private val backlog = ArrayDeque<Pair<Long, Entry>>()
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)
}
}

View File

@ -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<Long>()
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()
}
}
}

View File

@ -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 <E : NetworkedElement> 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<DataInputStream>? = null
fun putTrap(trap: Consumer<DataInputStream>) {
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 <E : NetworkedElement> 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() {

View File

@ -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<ItemStack> {
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)
}
}

View File

@ -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<K, V>(
private val keyCodec: Pair<StreamCodec<K>, StreamCodec<K>>,
private val valueCodec: Pair<StreamCodec<V>, StreamCodec<V>>,
private val isDumb: Boolean = false,
map: ListenableMap<K, V> = ListenableMap(),
private val maxBacklogSize: Int = 100,
) : NetworkedElement(), MutableMap<K, V> by map {
constructor(
keyCodec: StreamCodec<K>,
valueCodec: StreamCodec<V>,
isDumb: Boolean = false,
map: ListenableMap<K, V> = ListenableMap(),
maxBacklogSize: Int = 100,
) : this(keyCodec to keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize)
constructor(
keyCodec: StreamCodec<K>,
valueCodec: Pair<StreamCodec<V>, StreamCodec<V>>,
isDumb: Boolean = false,
map: ListenableMap<K, V> = ListenableMap(),
maxBacklogSize: Int = 100,
) : this(keyCodec to keyCodec, valueCodec, isDumb, map, maxBacklogSize)
constructor(
keyCodec: Pair<StreamCodec<K>, StreamCodec<K>>,
valueCodec: StreamCodec<V>,
isDumb: Boolean = false,
map: ListenableMap<K, V> = ListenableMap(),
maxBacklogSize: Int = 100,
) : this(keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize)
init {
map.addListener(object : ListenableMap.MapListener<K, V> {
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<K, V>(val action: Action, val key: KOptional<K>, val value: KOptional<V>) {
fun apply(map: MutableMap<K, V>) {
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<K, V>) {
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<K, V> {
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<K, V> {
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<K, V>(Action.CLEAR, KOptional(), KOptional())
private val backlog = ArrayDeque<Pair<Long, Entry<K, V>>>()
private val delayed = ArrayDeque<Pair<Double, Entry<K, V>>>()
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<K, V>(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
}
}
}

View File

@ -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<S>(private val codec: StreamCodec<S>, private val maxSize: Int = 100) : NetworkedElement(), Iterable<S>, Iterator<S> {
private data class Signal<S>(val version: Long, val signal: S)
private var currentTime = 0.0
private val visibleSignals = ArrayDeque<Signal<S>>()
private val internalSignals = ArrayDeque<Signal<S>>()
private val delayedSignals = ArrayDeque<Pair<Double, S>>()
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<S> {
if (visibleSignals.isEmpty())
return KOptional()
return KOptional(visibleSignals.removeFirst().signal)
}
override fun iterator(): Iterator<S> {
return this
}
override fun hasNext(): Boolean {
return visibleSignals.isNotEmpty()
}
override fun next(): S {
return visibleSignals.removeFirst().signal
}
}

View File

@ -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)
}
}

View File

@ -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, ItemStack>(EssentialSlot::class.java)
private val equipmentSlots = EnumMap<EquipmentSlot, ItemStack>(EquipmentSlot::class.java)
private val bags = ArrayList<AvatarBag>()
private val quests = Object2ObjectOpenHashMap<String, QuestInstance>()
private val quests = HashMap<String, QuestInstance>()
var cursorItem = ItemStack.EMPTY
private val availableTechs = ObjectOpenHashSet<Registry.Entry<TechDefinition>>()
private val enabledTechs = ObjectOpenHashSet<Registry.Entry<TechDefinition>>()
private val equippedTechs = Object2ObjectOpenHashMap<String, Registry.Entry<TechDefinition>>()
private val equippedTechs = HashMap<String, Registry.Entry<TechDefinition>>()
private val knownBlueprints = ObjectOpenHashSet<ItemStack>()
// С подписью NEW

View File

@ -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<ItemStack>) {
@ -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)
}

View File

@ -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<String, String>()
private val portraitTitles = HashMap<String, String>()
private var isInitialized = false
private var successfulInit = false

View File

@ -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<ByteKey, KOptional<ByteArray>>()
private val shipChunks = HashMap<ByteKey, KOptional<ByteArray>>()
private val modifiedShipChunks = ObjectOpenHashSet<ByteKey>()
var shipChunkSource by Delegates.notNull<WorldStorage>()
private set
@ -114,7 +115,7 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
shipChunks.putAll(chunks)
}
private val tickets = Object2ObjectOpenHashMap<ChunkPos, ServerWorld.ITicket>()
private val tickets = HashMap<ChunkPos, ServerWorld.ITicket>()
private val pendingSend = ObjectLinkedOpenHashSet<ChunkPos>()
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")

View File

@ -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")
}
}
}

View File

@ -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<CelestialPlanet>)
val systems = Object2ObjectOpenHashMap<Vector3i, System>()
val systems = HashMap<Vector3i, System>()
val constellations = ObjectOpenHashSet<Pair<Vector2i, Vector2i>>()
fun parameters(coordinate: UniversePos): CelestialParameters? {

View File

@ -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<String, String>) {
class Directives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap<String, String>) {
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)
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val lock = ReentrantLock()
val orphanedEntities = ReferenceOpenHashSet<AbstractEntity>()
val entities = ReferenceOpenHashSet<AbstractEntity>()
val entities = Int2ObjectOpenHashMap<AbstractEntity>()
val dynamicEntities = ReferenceOpenHashSet<DynamicEntity>()
val tileEntities = ReferenceOpenHashSet<TileEntity>()
@ -271,7 +272,7 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
ForkJoinPool.commonPool().submit(ParallelPerform(dynamicEntities.spliterator(), { it.movement.move() })).join()
mailbox.executeQueuedTasks()
entities.forEach { it.think() }
entities.values.forEach { it.think() }
mailbox.executeQueuedTasks()
for (chunk in chunkMap) {

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
@ -9,6 +10,7 @@ import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.World
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.UUID
import kotlin.concurrent.withLock
@ -88,9 +90,8 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
abstract fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean)
/**
* MUST be called by [World] itself
*/
abstract fun readDelta(stream: ByteArrayList, interpolationTime: Double = 0.0, isLegacy: Boolean)
fun joinWorld(world: World<*, *>) {
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

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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<String, AnimatedPartsDefinition.StateType.State>()
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<String, StateType>()
// sorted by key
private val parts = Object2ObjectAVLTreeMap<String, Part>()
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<String> {
return parts.keys
}
fun stateTypes(): Collection<String> {
return stateTypes.keys
}
}

View File

@ -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<NetworkedElement>()
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<String> = 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<NetworkedElement>()
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<NetworkedElement>()
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<NetworkedElement>()
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<String> = listOf()
var rotationGroup: String? = null
var rotationCenter: Vector2d? = null
val particleList = ArrayList<Config>()
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<NetworkedElement>()
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<String, NetworkedMap<String, String>>()
private val stateInfo = Object2ObjectAVLTreeMap<String, StateInfo>()
private val rotationGroups = Object2ObjectAVLTreeMap<String, RotationGroup>()
private val transformationGroups = Object2ObjectAVLTreeMap<String, TransformationGroup>()
private val particleEmitters = Object2ObjectAVLTreeMap<String, ParticleEmitter>()
private val lights = Object2ObjectAVLTreeMap<String, Light>()
private val sounds = Object2ObjectAVLTreeMap<String, Sound>()
private val effects = Object2ObjectAVLTreeMap<String, Effect>()
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 <tag> directives in it, which if set
// here will be replaced by the tag value when constructing Drawables. All
// Drawables can also have a <frame> 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<String>())
}
}

View File

@ -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<Set<Pair<String, String>>>
}
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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<String> = 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<String, Double>?
abstract val delta: Either<String, Double>?
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<String, Double>?,
override val delta: Either<String, Double>?
) : 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<String, LiveStatImpl>()
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<String, ResourceImpl>()
val liveStats: Map<String, LiveStat> = Collections.unmodifiableMap(liveStatsInternal)
val resources: Map<String, Resource> = 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<StatModifier>): 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)
}
}

View File

@ -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<ObjectDefinition>,
@ -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, () -> String?>()
protected val renderParamLocations = HashMap<String, () -> 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()
}
}

View File

@ -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")
}
}
}
}

View File

@ -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!")
}
}
}
}

View File

@ -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)

View File

@ -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<InventoryIndex> = KOptional(), val right: KOptional<InventoryIndex> = 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<String, Bag> = 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, NetworkedItemStack> = EquipmentSlot.entries
.stream()
.map { it to networkedItem() }
.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
val essentialSlots: ImmutableMap<EssentialSlot, NetworkedItemStack> = 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) }
}
}

View File

@ -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())
}

View File

@ -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<String> = 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()
}
}

View File

@ -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<Edge>, 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())

View File

@ -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)