More work on NPCs, async network messages handling support
This commit is contained in:
parent
40d306544f
commit
918b6ff95f
@ -154,6 +154,11 @@ In addition to `add`, `multiply`, `merge` and `override` new merge methods are a
|
||||
* `monster.setPhysicsForces(forces: Table?)` now accepts nil as equivalent of empty table (consistency fix)
|
||||
* `mosnter.setName(name: String?)` now accepts nil to reset custom name
|
||||
|
||||
## npc
|
||||
* `npc.setDropPools(dropPools: Table?)` now accepts `nil`
|
||||
* Added `npc.beginSecondaryFire()` which is alias for `npc.beginAltFire()`
|
||||
* Added `npc.endSecondaryFire()` which is alias for `npc.endAltFire()`
|
||||
|
||||
## status
|
||||
|
||||
* Implemented `status.appliesEnvironmentStatusEffects(): Boolean`, which exists in original engine's code but was never hooked up to Lua bindings
|
||||
|
@ -1,6 +1,4 @@
|
||||
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.10"
|
||||
id("me.champeau.jmh") version "0.7.1"
|
||||
|
@ -37,11 +37,11 @@ inline fun <reified T> GsonBuilder.registerTypeAdapter(noinline factory: (Gson)
|
||||
|
||||
fun <T> Array<T>.stream(): Stream<T> = Arrays.stream(this)
|
||||
|
||||
operator fun <T> ThreadLocal<T>.getValue(thisRef: Any, property: KProperty<*>): T {
|
||||
operator fun <T> ThreadLocal<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
|
||||
return get()
|
||||
}
|
||||
|
||||
operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, value: T) {
|
||||
operator fun <T> ThreadLocal<T>.setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
set(value)
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import ru.dbotthepony.kstarbound.defs.ElementalDamageType
|
||||
import ru.dbotthepony.kstarbound.defs.MovementParameters
|
||||
import ru.dbotthepony.kstarbound.defs.world.SpawnerConfig
|
||||
import ru.dbotthepony.kstarbound.defs.UniverseServerConfig
|
||||
import ru.dbotthepony.kstarbound.defs.actor.GlobalNPCConfig
|
||||
import ru.dbotthepony.kstarbound.defs.world.WorldServerConfig
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.PlayerConfig
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.ShipUpgrades
|
||||
@ -129,6 +130,9 @@ object Globals {
|
||||
var elementalTypes by Delegates.notNull<ImmutableMap<String, ElementalDamageType>>()
|
||||
private set
|
||||
|
||||
var npcs by Delegates.notNull<GlobalNPCConfig>()
|
||||
private set
|
||||
|
||||
private var profanityFilterInternal by Delegates.notNull<ImmutableList<String>>()
|
||||
|
||||
val profanityFilter: ImmutableSet<String> by lazy {
|
||||
@ -237,6 +241,7 @@ object Globals {
|
||||
tasks.add(load("/ships/shipupgrades.config", ::shipUpgrades))
|
||||
tasks.add(load("/quests/quests.config", ::quests))
|
||||
tasks.add(load("/spawning.config", ::spawner))
|
||||
tasks.add(load("/npcs/npc.config", ::npcs))
|
||||
|
||||
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/dungeon_worlds.config", ::dungeonWorlds, mapAdapter("/dungeon_worlds.config")) }.asCompletableFuture())
|
||||
tasks.add(Starbound.GLOBAL_SCOPE.launch { load("/currencies.config", ::currencies, mapAdapter("/currencies.config")) }.asCompletableFuture())
|
||||
|
@ -38,6 +38,7 @@ import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
|
||||
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.item.TreasureChestDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.ProjectileDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.actor.DanceDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.actor.behavior.BehaviorNodeDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.monster.MonsterPaletteSwap
|
||||
@ -112,6 +113,7 @@ object Registries {
|
||||
val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
||||
val markovGenerators = Registry<MarkovTextGenerator>("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
||||
val damageKinds = Registry<DamageKind>("damage kind").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
||||
val dance = Registry<DanceDefinition>("dance").also(registriesInternal::add).also { adapters.add(it.adapter()) }
|
||||
|
||||
private val monsterParts = HashMap<Pair<String, String>, HashMap<String, Pair<MonsterPartDefinition, IStarboundFile>>>()
|
||||
private val loggedMonsterPartMisses = Collections.synchronizedSet(ObjectOpenHashSet<Pair<String, String>>())
|
||||
@ -292,6 +294,7 @@ object Registries {
|
||||
tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName)))
|
||||
tasks.addAll(loadRegistry(behavior, patchTree, fileTree["behavior"] ?: listOf(), key(BehaviorDefinition::name)))
|
||||
tasks.addAll(loadRegistry(damageKinds, patchTree, fileTree["damage"] ?: listOf(), key(DamageKind::kind)))
|
||||
tasks.addAll(loadRegistry(dance, patchTree, fileTree["dance"] ?: listOf(), key(DanceDefinition::name)))
|
||||
|
||||
tasks.addAll(loadCombined(behaviorNodes, fileTree["nodes"] ?: listOf(), patchTree))
|
||||
|
||||
|
@ -10,10 +10,15 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kommons.util.XXHash64
|
||||
import ru.dbotthepony.kstarbound.io.StreamCodec
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.util.limit
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.function.Supplier
|
||||
@ -33,7 +38,7 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
|
||||
// idiot-proof miss lookup. Surely, it will cause some entries to be never logged
|
||||
// if they are missing, but at least if malicious actor spams with long-ass invalid data
|
||||
// it won't explode memory usage of server
|
||||
// it won't easily explode memory usage of server
|
||||
private val loggedMisses = LongOpenHashSet()
|
||||
|
||||
val keys: Map<String, Entry<T>> = Collections.unmodifiableMap(keysInternal)
|
||||
@ -44,6 +49,9 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
abstract val entry: Entry<T>?
|
||||
abstract val registry: Registry<T>
|
||||
|
||||
val entryOrThrow: Entry<T>
|
||||
get() = entry ?: throw NoSuchElementException("No such ${registry.name}: ${key.map({ it }, { it.toString() }).limit()}")
|
||||
|
||||
val isPresent: Boolean
|
||||
get() = value != null
|
||||
|
||||
@ -53,6 +61,18 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
val value: T?
|
||||
get() = entry?.value
|
||||
|
||||
inline fun ifPresent(block: (Entry<T>) -> Unit) {
|
||||
entry?.let(block)
|
||||
}
|
||||
|
||||
inline fun <R> map(block: (Entry<T>) -> R): KOptional<R> {
|
||||
return entry?.let { KOptional(block(it)) } ?: KOptional()
|
||||
}
|
||||
|
||||
inline fun <R> mapOrThrow(block: (Entry<T>) -> R): R {
|
||||
return block(entryOrThrow)
|
||||
}
|
||||
|
||||
final override fun get(): Entry<T>? {
|
||||
return entry
|
||||
}
|
||||
@ -106,7 +126,7 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Entry of $name at $key/${id ?: "-"}"
|
||||
return "E/$name/$key/${id ?: "-"}"
|
||||
}
|
||||
|
||||
override val registry: Registry<T>
|
||||
@ -139,7 +159,11 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Ref of $name at $key/${if (entry != null) "bound" else "missing"}"
|
||||
if (entry == null) {
|
||||
return "R/$name/${key.map({ "'$it'" }, { it.toString() })}/!"
|
||||
} else {
|
||||
return "R/$name/${entry!!.key}/${entry?.id ?: "-"}"
|
||||
}
|
||||
}
|
||||
|
||||
override val registry: Registry<T>
|
||||
@ -329,6 +353,34 @@ class Registry<T : Any>(val name: String, val storeJson: Boolean = true) {
|
||||
|
||||
val emptyRef = ref("")
|
||||
|
||||
val nameRefCodec = object : StreamCodec<Ref<T>> {
|
||||
override fun read(stream: DataInputStream): Ref<T> {
|
||||
return ref(stream.readInternedString())
|
||||
}
|
||||
|
||||
override fun write(stream: DataOutputStream, value: Ref<T>) {
|
||||
stream.writeBinaryString(value.key.left())
|
||||
}
|
||||
|
||||
override fun copy(value: Ref<T>): Ref<T> {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
val nameEntryCodec = object : StreamCodec<Entry<T>> {
|
||||
override fun read(stream: DataInputStream): Entry<T> {
|
||||
return getOrThrow(stream.readInternedString())
|
||||
}
|
||||
|
||||
override fun write(stream: DataOutputStream, value: Entry<T>) {
|
||||
stream.writeBinaryString(value.key)
|
||||
}
|
||||
|
||||
override fun copy(value: Entry<T>): Entry<T> {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOGGER = LogManager.getLogger()
|
||||
}
|
||||
|
@ -4,19 +4,18 @@ import com.github.benmanes.caffeine.cache.AsyncCacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.Interner
|
||||
import com.github.benmanes.caffeine.cache.Scheduler
|
||||
import com.google.common.base.Predicate
|
||||
import com.google.gson.*
|
||||
import com.google.gson.stream.JsonReader
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import org.classdump.luna.compiler.CompilerChunkLoader
|
||||
import org.classdump.luna.compiler.CompilerSettings
|
||||
@ -34,6 +33,7 @@ import ru.dbotthepony.kstarbound.defs.image.Image
|
||||
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
|
||||
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.quest.QuestParameter
|
||||
import ru.dbotthepony.kstarbound.defs.world.CelestialParameters
|
||||
import ru.dbotthepony.kstarbound.defs.world.VisitableWorldParametersType
|
||||
@ -97,17 +97,11 @@ import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory
|
||||
import java.util.concurrent.ForkJoinWorkerThread
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import java.util.random.RandomGenerator
|
||||
import kotlin.NoSuchElementException
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLocator {
|
||||
const val ENGINE_VERSION = "0.0.1"
|
||||
@ -122,6 +116,11 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
|
||||
const val DEDUP_CELL_STATES = true
|
||||
const val USE_CAFFEINE_INTERNER = false
|
||||
const val USE_INTERNER = true
|
||||
// enables a fuckton of runtime checks for data which doesn't make much sense
|
||||
// especially for data which will explode legacy client
|
||||
// also changes some constants
|
||||
const val DEBUG_BUILD = true
|
||||
// ----
|
||||
|
||||
fun <E : Any> interner(): Interner<E> {
|
||||
if (!USE_INTERNER)
|
||||
@ -417,6 +416,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
|
||||
|
||||
registerTypeAdapter(LongRangeAdapter)
|
||||
|
||||
registerTypeAdapter(ItemDescriptor.Adapter)
|
||||
|
||||
create()
|
||||
}
|
||||
|
||||
@ -782,10 +783,22 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun generateName(asset: String, random: RandomGenerator): String {
|
||||
val load = loadJsonAsset(asset).get() as? JsonArray ?: return "missingasset"
|
||||
@Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync"))
|
||||
fun generateName(asset: String, random: RandomGenerator, maxTries: Int = 500): String {
|
||||
return runBlocking {
|
||||
generateNameAsync(asset, random, maxTries)
|
||||
}
|
||||
}
|
||||
|
||||
var tries = 500
|
||||
@Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync"))
|
||||
fun generateName(asset: String, seed: Long, maxTries: Int = 500) = generateName(asset, random(seed), maxTries)
|
||||
@Deprecated("Blocks thread it is running in, consider generateNameAsync instead", replaceWith = ReplaceWith("this.generateNameAsync"))
|
||||
fun generateName(asset: String, maxTries: Int = 500) = generateName(asset, System.nanoTime(), maxTries)
|
||||
|
||||
suspend fun generateNameAsync(asset: String, random: RandomGenerator, maxTries: Int = 500): String {
|
||||
val load = loadJsonAsset(asset).await() as? JsonArray ?: return "missingasset:$asset"
|
||||
|
||||
var tries = maxTries
|
||||
var result = ""
|
||||
|
||||
while (tries-- > 0 && (result.isEmpty() || result.lowercase() in Globals.profanityFilter))
|
||||
@ -794,7 +807,7 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca
|
||||
return result
|
||||
}
|
||||
|
||||
fun generateName(asset: String, seed: Long) = generateName(asset, random(seed))
|
||||
fun generateName(asset: String) = generateName(asset, System.nanoTime())
|
||||
suspend fun generateNameAsync(asset: String, seed: Long, maxTries: Int = 500) = generateNameAsync(asset, random(seed), maxTries)
|
||||
suspend fun generateNameAsync(asset: String, maxTries: Int = 500) = generateNameAsync(asset, System.nanoTime(), maxTries)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import io.netty.channel.local.LocalAddress
|
||||
import io.netty.channel.local.LocalChannel
|
||||
import io.netty.channel.socket.nio.NioSocketChannel
|
||||
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
@ -78,11 +79,13 @@ class ClientConnection(val client: StarboundClient, type: ConnectionType) : Conn
|
||||
|
||||
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||
if (msg is IClientPacket) {
|
||||
try {
|
||||
msg.play(this)
|
||||
} catch (err: Throwable) {
|
||||
LOGGER.error("Failed to read incoming packet $msg", err)
|
||||
disconnect(err.toString())
|
||||
runBlocking {
|
||||
try {
|
||||
msg.play(this@ClientConnection)
|
||||
} catch (err: Throwable) {
|
||||
LOGGER.error("Failed to read incoming packet $msg", err)
|
||||
disconnect(err.toString())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.error("Unknown incoming packet type $msg")
|
||||
|
@ -29,7 +29,7 @@ class ChunkCellsPacket(val pos: ChunkPos, val data: List<ImmutableCell>) : IClie
|
||||
stream.writeCollection(data) { it.writeLegacy(stream) }
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.client.mailbox.execute {
|
||||
val chunk = connection.client.world?.chunkMap?.compute(pos.x, pos.y) ?: return@execute
|
||||
val itr = data.iterator()
|
||||
|
@ -15,7 +15,7 @@ class ForgetChunkPacket(val pos: ChunkPos) : IClientPacket {
|
||||
stream.writeStruct2i(pos)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.client.mailbox.execute {
|
||||
val world = connection.client.world ?: return@execute
|
||||
world.chunkMap.remove(pos)
|
||||
|
@ -15,7 +15,7 @@ class ForgetEntityPacket(val uuid: UUID) : IClientPacket {
|
||||
stream.writeUUID(uuid)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
val world = connection.client.world ?: return
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ data class JoinWorldPacket(val uuid: UUID, val geometry: WorldGeometry) : IClien
|
||||
geometry.write(stream)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.client.mailbox.execute {
|
||||
connection.client.world = ClientWorld(connection.client, WorldTemplate(geometry))
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ object LeaveWorldPacket : IClientPacket {
|
||||
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.client.mailbox.execute {
|
||||
connection.client.world = null
|
||||
}
|
||||
|
@ -19,8 +19,12 @@ class ColorReplacements private constructor(private val mapping: Int2IntOpenHash
|
||||
return mapping.getOrDefault(color, color)
|
||||
}
|
||||
|
||||
private val asImageOperator by lazy {
|
||||
"?replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}"
|
||||
}
|
||||
|
||||
fun toImageOperator(): String {
|
||||
return "replace;${mapping.int2IntEntrySet().joinToString(";") { "${RGBAColor.rgb(it.intKey).toHexStringRGB().substring(1)}=${RGBAColor.rgb(it.intValue).toHexStringRGB().substring(1)}" }}"
|
||||
return asImageOperator
|
||||
}
|
||||
|
||||
class Adapter(gson: Gson) : TypeAdapter<ColorReplacements>() {
|
||||
|
@ -15,6 +15,7 @@ import ru.dbotthepony.kommons.io.readSignedVarInt
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeCollection
|
||||
import ru.dbotthepony.kommons.io.writeSignedVarInt
|
||||
import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.io.readDouble
|
||||
@ -153,7 +154,35 @@ data class TouchDamage(
|
||||
val damageSourceKind: String = "",
|
||||
val knockback: Double = 0.0,
|
||||
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* new protocol only
|
||||
*/
|
||||
constructor(stream: DataInputStream) : this(
|
||||
ImmutableList.copyOf(stream.readCollection { readVector2d() }),
|
||||
TeamType.entries[stream.readUnsignedByte()],
|
||||
stream.readDouble(),
|
||||
stream.readInternedString(),
|
||||
stream.readDouble(),
|
||||
ImmutableSet.copyOf(stream.readCollection { readInternedString() })
|
||||
)
|
||||
|
||||
/**
|
||||
* new protocol only
|
||||
*/
|
||||
fun write(stream: DataOutputStream) {
|
||||
stream.writeCollection(poly) { writeStruct2d(it) }
|
||||
stream.writeByte(teamType.ordinal)
|
||||
stream.writeDouble(damage)
|
||||
stream.writeBinaryString(damageSourceKind)
|
||||
stream.writeDouble(knockback)
|
||||
stream.writeCollection(statusEffects) { writeBinaryString(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY = TouchDamage()
|
||||
}
|
||||
}
|
||||
|
||||
@JsonFactory
|
||||
data class DamageNotification(
|
||||
|
@ -4,6 +4,7 @@ import com.google.gson.JsonObject
|
||||
import ru.dbotthepony.kommons.io.readBinaryString
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.actor.NPCVariant
|
||||
import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant
|
||||
import ru.dbotthepony.kstarbound.defs.`object`.ObjectType
|
||||
import ru.dbotthepony.kstarbound.fromJsonFast
|
||||
@ -13,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.ItemDropEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.NPCEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.ProjectileEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.ContainerObject
|
||||
@ -24,7 +26,7 @@ import java.io.DataInputStream
|
||||
|
||||
enum class EntityType(override val jsonName: String, val storeName: String, val canBeCreatedByClient: Boolean, val canBeSpawnedByClient: Boolean, val ephemeralIfSpawnedByClient: Boolean = true) : IStringSerializable {
|
||||
PLANT("plant", "PlantEntity", false, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return PlantEntity(stream, isLegacy)
|
||||
}
|
||||
|
||||
@ -34,7 +36,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
OBJECT("object", "ObjectEntity", false, true) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
val name = stream.readInternedString()
|
||||
val parameters = stream.readJsonElement()
|
||||
val config = Registries.worldObjects.getOrThrow(name)
|
||||
@ -59,7 +61,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
VEHICLE("vehicle", "VehicleEntity", false, true, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
TODO("VEHICLE")
|
||||
}
|
||||
|
||||
@ -69,7 +71,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
ITEM_DROP("itemDrop", "ItemDropEntity", false, true, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return ItemDropEntity(stream, isLegacy)
|
||||
}
|
||||
|
||||
@ -79,7 +81,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
PLANT_DROP("plantDrop", "PlantDropEntity", false, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return PlantPieceEntity(stream, isLegacy)
|
||||
}
|
||||
|
||||
@ -89,7 +91,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
PROJECTILE("projectile", "ProjectileEntity", true, true) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return ProjectileEntity(Registries.projectiles.getOrThrow(stream.readBinaryString()), stream, isLegacy)
|
||||
}
|
||||
|
||||
@ -99,7 +101,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
STAGEHAND("stagehand", "StagehandEntity", true, true) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
TODO("STAGEHAND")
|
||||
}
|
||||
|
||||
@ -109,7 +111,7 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
MONSTER("monster", "MonsterEntity", false, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return MonsterEntity(MonsterVariant.read(stream, isLegacy))
|
||||
}
|
||||
|
||||
@ -132,17 +134,19 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
},
|
||||
|
||||
NPC("npc", "NpcEntity", false, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
TODO("NPC")
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return NPCEntity(NPCVariant.read(stream, isLegacy))
|
||||
}
|
||||
|
||||
override fun fromStorage(data: JsonObject): AbstractEntity {
|
||||
TODO("NPC")
|
||||
val entity = NPCEntity(Starbound.gson.fromJsonFast(data["npcVariant"], NPCVariant::class.java))
|
||||
entity.deserialize(data)
|
||||
return entity
|
||||
}
|
||||
},
|
||||
|
||||
PLAYER("player", "PlayerEntity", true, false) {
|
||||
override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
override suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity {
|
||||
return PlayerEntity(stream, isLegacy)
|
||||
}
|
||||
|
||||
@ -151,6 +155,6 @@ enum class EntityType(override val jsonName: String, val storeName: String, val
|
||||
}
|
||||
};
|
||||
|
||||
abstract fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity
|
||||
abstract suspend fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity
|
||||
abstract fun fromStorage(data: JsonObject): AbstractEntity
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
|
||||
@JsonFactory
|
||||
data class DanceDefinition(
|
||||
val name: String,
|
||||
val states: ImmutableSet<String>,
|
||||
val cycle: Double,
|
||||
val cyclic: Boolean,
|
||||
val duration: Double,
|
||||
val steps: ImmutableList<Step>
|
||||
) {
|
||||
@JsonFactory(asList = 0)
|
||||
data class Step(
|
||||
val bodyFrame: String? = null,
|
||||
val frontArmFrame: String? = null,
|
||||
val backArmFrame: String? = null,
|
||||
val headOffset: Vector2d = Vector2d.ZERO,
|
||||
val frontArmOffset: Vector2d = Vector2d.ZERO,
|
||||
val backArmOffset: Vector2d = Vector2d.ZERO,
|
||||
val frontArmRotation: Double = 0.0,
|
||||
val backArmRotation: Double = 0.0,
|
||||
)
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
|
||||
data class GlobalNPCConfig(
|
||||
val hitDamageNotificationLimit: Int,
|
||||
val emoteCooldown: Double,
|
||||
val danceCooldown: Double,
|
||||
val shieldHitSoundLimit: Double,
|
||||
val blinkInterval: Vector2d,
|
||||
) {
|
||||
init {
|
||||
require(hitDamageNotificationLimit >= 1) { "Pointless hitDamageNotificationLimit: $hitDamageNotificationLimit" }
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.gson.JsonObject
|
||||
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
|
||||
@JsonFactory
|
||||
data class HumanoidConfig(
|
||||
val globalOffset: Vector2d, // in pixels
|
||||
val headRunOffset: Vector2d, // in pixels
|
||||
val headSwimOffset: Vector2d, // in pixels
|
||||
val runFallOffset: Double, // in pixels
|
||||
val duckOffset: Double, // in pixels
|
||||
val headDuckOffset: Vector2d, // in pixels
|
||||
val sitOffset: Double, // in pixels
|
||||
val layOffset: Double, // in pixels
|
||||
val headSitOffset: Vector2d, // in pixels
|
||||
val headLayOffset: Vector2d, // in pixels
|
||||
val recoilOffset: Vector2d, // in pixels
|
||||
val mouthOffset: Vector2d, // in pixels
|
||||
val feetOffset: Vector2d, // in pixels
|
||||
|
||||
val bodyFullbright: Boolean = false,
|
||||
|
||||
val headArmorOffset: Vector2d, // in pixels
|
||||
val chestArmorOffset: Vector2d, // in pixels
|
||||
val legsArmorOffset: Vector2d, // in pixels
|
||||
val backArmorOffset: Vector2d, // in pixels
|
||||
|
||||
val bodyHidden: Boolean = false,
|
||||
|
||||
val armWalkSeq: ImmutableList<Int>,
|
||||
val armRunSeq: ImmutableList<Int>,
|
||||
|
||||
val walkBob: ImmutableList<Double>, // in pixels
|
||||
val runBob: ImmutableList<Double>, // in pixels
|
||||
val swimBob: ImmutableList<Double>, // in pixels
|
||||
|
||||
val jumpBob: Double,
|
||||
val frontArmRotationCenter: Vector2d, // in pixels
|
||||
val backArmRotationCenter: Vector2d, // in pixels
|
||||
val frontHandPosition: Vector2d, // in pixels
|
||||
val backArmOffset: Vector2d, // in pixels
|
||||
val vaporTrailFrames: Int,
|
||||
val vaporTrailCycle: Double,
|
||||
|
||||
val deathParticles: String,
|
||||
val particleEmitters: JsonObject,
|
||||
|
||||
val movementParameters: ActorMovementParameters,
|
||||
|
||||
val personalities: ImmutableList<Personality>,
|
||||
) {
|
||||
data class Timing(
|
||||
val stateCycle: ImmutableList<Double>? = null,
|
||||
val emoteCycle: ImmutableList<Double>? = null,
|
||||
val stateFrames: ImmutableList<Int>? = null,
|
||||
val emoteFrames: ImmutableList<Int>? = null,
|
||||
)
|
||||
}
|
@ -6,6 +6,9 @@ import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kommons.io.writeStruct2f
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.io.readColor
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
@ -17,9 +20,9 @@ import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
@JsonFactory
|
||||
data class HumanoidData(
|
||||
data class HumanoidIdentity(
|
||||
val name: String = "Humanoid",
|
||||
val species: String = "human",
|
||||
val species: Registry.Ref<Species> = Registries.species.ref("human"),
|
||||
val gender: Gender = Gender.MALE,
|
||||
|
||||
val hairGroup: String = "hair",
|
||||
@ -37,8 +40,8 @@ data class HumanoidData(
|
||||
|
||||
val color: RGBAColor = RGBAColor.BLACK,
|
||||
|
||||
val personalityIdle: String = "",
|
||||
val personalityArmIdle: String = "",
|
||||
val personalityIdle: String = "idle.1",
|
||||
val personalityArmIdle: String = "idle.1",
|
||||
val personalityHeadOffset: Vector2d = Vector2d.ZERO,
|
||||
val personalityArmOffset: Vector2d = Vector2d.ZERO,
|
||||
|
||||
@ -48,14 +51,28 @@ data class HumanoidData(
|
||||
get() = personalityIdle
|
||||
override val armIdle: String
|
||||
get() = personalityArmIdle
|
||||
override val handOffset: Vector2d
|
||||
override val headOffset: Vector2d
|
||||
get() = personalityHeadOffset
|
||||
override val armOffset: Vector2d
|
||||
get() = personalityArmOffset
|
||||
|
||||
fun check() {
|
||||
if (Starbound.DEBUG_BUILD) {
|
||||
check(hairType.isNotEmpty()) { "'hairType' is an empty string" }
|
||||
check(hairGroup.isNotEmpty()) { "'hairGroup' is an empty string" }
|
||||
// check(facialHairType.isNotEmpty()) { "'facialHairType' is an empty string" }
|
||||
// check(facialMaskGroup.isNotEmpty()) { "'facialMaskGroup' is an empty string" }
|
||||
// check(facialMaskType.isNotEmpty()) { "'facialMaskType' is an empty string" }
|
||||
check(personalityIdle.isNotEmpty()) { "'personalityIdle' is an empty string" }
|
||||
check(personalityArmIdle.isNotEmpty()) { "'personalityArmIdle' is an empty string" }
|
||||
}
|
||||
}
|
||||
|
||||
fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
check()
|
||||
|
||||
stream.writeBinaryString(name)
|
||||
stream.writeBinaryString(species)
|
||||
stream.writeBinaryString(species.key.left())
|
||||
stream.writeByte(gender.ordinal)
|
||||
stream.writeBinaryString(hairGroup)
|
||||
stream.writeBinaryString(hairType)
|
||||
@ -91,12 +108,12 @@ data class HumanoidData(
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC = nativeCodec(::read, HumanoidData::write)
|
||||
val LEGACY_CODEC = legacyCodec(::read, HumanoidData::write)
|
||||
val CODEC = nativeCodec(::read, HumanoidIdentity::write)
|
||||
val LEGACY_CODEC = legacyCodec(::read, HumanoidIdentity::write)
|
||||
|
||||
fun read(stream: DataInputStream, isLegacy: Boolean): HumanoidData {
|
||||
fun read(stream: DataInputStream, isLegacy: Boolean): HumanoidIdentity {
|
||||
val name: String = stream.readInternedString()
|
||||
val species: String = stream.readInternedString()
|
||||
val species = Registries.species.ref(stream.readInternedString())
|
||||
val gender: Gender = Gender.entries[stream.readUnsignedByte()]
|
||||
|
||||
val hairGroup = stream.readInternedString()
|
||||
@ -119,7 +136,7 @@ data class HumanoidData(
|
||||
val color: RGBAColor = if (isLegacy) RGBAColor(stream.readUnsignedByte(), stream.readUnsignedByte(), stream.readUnsignedByte(), stream.readUnsignedByte()) else stream.readColor()
|
||||
val imagePath: String? = if (stream.readBoolean()) stream.readInternedString() else null
|
||||
|
||||
return HumanoidData(
|
||||
return HumanoidIdentity(
|
||||
name, species, gender, hairGroup, hairType, hairDirectives, bodyDirectives,
|
||||
emoteDirectives, facialHairGroup, facialHairType, facialHairDirectives,
|
||||
facialMaskGroup, facialMaskType, facialMaskDirectives, color, personalityIdle,
|
@ -0,0 +1,11 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
|
||||
|
||||
enum class MoveControlType(override val jsonName: String) : IStringSerializable {
|
||||
LEFT("left"),
|
||||
RIGHT("right"),
|
||||
DOWN("down"),
|
||||
UP("up"),
|
||||
JUMP("jump");
|
||||
}
|
@ -1,44 +1,415 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableMap
|
||||
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.TypeAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import kotlinx.coroutines.future.await
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kommons.gson.stream
|
||||
import ru.dbotthepony.kommons.io.readBinaryString
|
||||
import ru.dbotthepony.kommons.io.readMap
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeMap
|
||||
import ru.dbotthepony.kommons.math.RGBAColor
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
|
||||
import ru.dbotthepony.kstarbound.defs.AssetPath
|
||||
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
|
||||
import ru.dbotthepony.kstarbound.defs.TeamType
|
||||
import ru.dbotthepony.kstarbound.defs.TouchDamage
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
|
||||
import ru.dbotthepony.kstarbound.io.readColor
|
||||
import ru.dbotthepony.kstarbound.io.readDouble
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.io.writeColor
|
||||
import ru.dbotthepony.kstarbound.io.writeDouble
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.util.coalesceNull
|
||||
import ru.dbotthepony.kstarbound.util.random.nextRange
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import java.util.random.RandomGenerator
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.math.max
|
||||
|
||||
@JsonAdapter(NPCVariant.Adapter::class)
|
||||
data class NPCVariant(
|
||||
val species: Registry.Entry<Species>,
|
||||
val seed: Long,
|
||||
val typeName: String,
|
||||
val level: Double,
|
||||
val overrides: JsonElement,
|
||||
val humanoidConfig: HumanoidConfig,
|
||||
val humanoidIdentity: HumanoidIdentity,
|
||||
val scripts: ImmutableList<AssetPath>,
|
||||
val initialScriptDelta: Int = 5,
|
||||
val initialScriptDelta: Double = 5.0,
|
||||
val scriptConfig: JsonElement = JsonNull.INSTANCE,
|
||||
val movementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY,
|
||||
val statusControllerSettings: StatusControllerConfig = StatusControllerConfig.EMPTY,
|
||||
val innateStatusEffects: ImmutableList<PersistentStatusEffect> = ImmutableList.of(),
|
||||
val touchDamage: TouchDamage = TouchDamage.EMPTY,
|
||||
val disableWornArmor: Boolean = true,
|
||||
val dropPools: ImmutableList<Registry.Ref<TreasurePoolDefinition>> = ImmutableList.of(),
|
||||
val persistent: Boolean = false,
|
||||
val keepAlive: Boolean = false,
|
||||
val damageTeam: Int = 0,
|
||||
val damageTeamType: TeamType = TeamType.ENEMY,
|
||||
val nametagColor: RGBAColor = RGBAColor.WHITE,
|
||||
val items: ImmutableMap<String, ItemDescriptor> = ImmutableMap.of(),
|
||||
) {
|
||||
@JsonFactory
|
||||
data class SerializedData(
|
||||
val levelVariance: Vector2d = Vector2d.ZERO,
|
||||
val scripts: ImmutableList<AssetPath>,
|
||||
val initialScriptDelta: Int = 5,
|
||||
val initialScriptDelta: Double = 5.0,
|
||||
val scriptConfig: JsonElement = JsonNull.INSTANCE,
|
||||
val humanoidConfig: String? = null,
|
||||
val npcname: String? = null,
|
||||
val nameGen: ImmutableList<String>? = null,
|
||||
val movementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY,
|
||||
val statusControllerSettings: StatusControllerConfig = StatusControllerConfig.EMPTY,
|
||||
val innateStatusEffects: ImmutableList<PersistentStatusEffect> = ImmutableList.of(),
|
||||
val touchDamage: TouchDamage = TouchDamage.EMPTY,
|
||||
val disableWornArmor: Boolean = true,
|
||||
val dropPools: ImmutableList<Registry.Ref<TreasurePoolDefinition>> = ImmutableList.of(),
|
||||
val persistent: Boolean = false,
|
||||
val keepAlive: Boolean = false,
|
||||
val damageTeam: Int = 0,
|
||||
val damageTeamType: TeamType = TeamType.ENEMY,
|
||||
val nametagColor: RGBAColor = RGBAColor.WHITE,
|
||||
val matchColorIndices: Boolean = false,
|
||||
)
|
||||
|
||||
val team: EntityDamageTeam
|
||||
get() = EntityDamageTeam(damageTeamType, damageTeam)
|
||||
|
||||
fun check() {
|
||||
if (Starbound.DEBUG_BUILD) {
|
||||
humanoidIdentity.check()
|
||||
}
|
||||
}
|
||||
|
||||
fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeBinaryString(species.key)
|
||||
stream.writeBinaryString(typeName)
|
||||
stream.writeDouble(level, isLegacy)
|
||||
stream.writeLong(seed)
|
||||
stream.writeJsonElement(overrides)
|
||||
|
||||
if (isLegacy)
|
||||
stream.writeInt(initialScriptDelta.toInt())
|
||||
else
|
||||
stream.writeDouble(initialScriptDelta)
|
||||
|
||||
humanoidIdentity.write(stream, isLegacy)
|
||||
stream.writeMap(items, { writeBinaryString(it) }, { it.write(this) })
|
||||
stream.writeBoolean(persistent)
|
||||
stream.writeBoolean(keepAlive)
|
||||
stream.writeByte(damageTeam)
|
||||
stream.writeByte(damageTeamType.ordinal)
|
||||
|
||||
if (!isLegacy) {
|
||||
stream.writeBoolean(disableWornArmor)
|
||||
touchDamage.write(stream)
|
||||
stream.writeColor(nametagColor)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonFactory
|
||||
data class DiskSerializedData(
|
||||
val species: Registry.Entry<Species>,
|
||||
val typeName: String,
|
||||
val level: Double,
|
||||
val seed: Long,
|
||||
val overrides: JsonElement,
|
||||
val initialScriptDelta: Double = 5.0,
|
||||
val humanoidIdentity: HumanoidIdentity,
|
||||
val items: ImmutableMap<String, ItemDescriptor>,
|
||||
val persistent: Boolean,
|
||||
val keepAlive: Boolean,
|
||||
val damageTeam: Int,
|
||||
val damageTeamType: TeamType,
|
||||
val touchDamage: TouchDamage? = null,
|
||||
val disableWornArmor: Boolean? = null,
|
||||
val nametagColor: RGBAColor? = null,
|
||||
)
|
||||
|
||||
class Adapter(gson: Gson) : TypeAdapter<NPCVariant>() {
|
||||
private val parent = gson.getAdapter(DiskSerializedData::class.java)
|
||||
|
||||
override fun write(out: JsonWriter, value: NPCVariant) {
|
||||
parent.write(out, DiskSerializedData(
|
||||
species = value.species,
|
||||
typeName = value.typeName,
|
||||
level = value.level,
|
||||
seed = value.seed,
|
||||
overrides = value.overrides,
|
||||
initialScriptDelta = value.initialScriptDelta,
|
||||
humanoidIdentity = value.humanoidIdentity,
|
||||
items = value.items,
|
||||
persistent = value.persistent,
|
||||
keepAlive = value.keepAlive,
|
||||
damageTeam = value.damageTeam,
|
||||
damageTeamType = value.damageTeamType,
|
||||
touchDamage = value.touchDamage,
|
||||
disableWornArmor = value.disableWornArmor,
|
||||
nametagColor = value.nametagColor,
|
||||
))
|
||||
}
|
||||
|
||||
override fun read(`in`: JsonReader): NPCVariant {
|
||||
val (
|
||||
species,
|
||||
typeName,
|
||||
level,
|
||||
seed,
|
||||
overrides,
|
||||
initialScriptDelta,
|
||||
humanoidIdentity,
|
||||
items,
|
||||
persistent,
|
||||
keepAlive,
|
||||
damageTeam,
|
||||
damageTeamType,
|
||||
touchDamage,
|
||||
disableWornArmor,
|
||||
nametagColor,
|
||||
) = parent.read(`in`)
|
||||
|
||||
val config = Registries.buildNPCConfig(typeName, overrides)
|
||||
val serialized = Starbound.gson.fromJson(config, SerializedData::class.java)
|
||||
|
||||
val humanoidConfig = runBlocking { humanoidConfig(serialized, species) }
|
||||
|
||||
val movementParameters = humanoidConfig
|
||||
.movementParameters
|
||||
.merge(serialized.movementParameters)
|
||||
|
||||
return NPCVariant(
|
||||
species = species,
|
||||
seed = seed,
|
||||
typeName = typeName,
|
||||
level = level,
|
||||
humanoidConfig = humanoidConfig,
|
||||
humanoidIdentity = humanoidIdentity,
|
||||
overrides = overrides.deepCopy(),
|
||||
scripts = serialized.scripts,
|
||||
initialScriptDelta = initialScriptDelta,
|
||||
scriptConfig = serialized.scriptConfig,
|
||||
movementParameters = movementParameters,
|
||||
statusControllerSettings = serialized.statusControllerSettings,
|
||||
innateStatusEffects = ImmutableList.copyOf(innates(serialized, level)),
|
||||
touchDamage = touchDamage ?: serialized.touchDamage,
|
||||
disableWornArmor = disableWornArmor ?: serialized.disableWornArmor,
|
||||
dropPools = serialized.dropPools,
|
||||
persistent = persistent,
|
||||
keepAlive = keepAlive,
|
||||
damageTeam = damageTeam,
|
||||
damageTeamType = damageTeamType,
|
||||
nametagColor = nametagColor ?: serialized.nametagColor,
|
||||
items = ImmutableMap.copyOf(items)
|
||||
).also { it.check() }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun create(species: Registry.Entry<Species>, type: String, level: Double, random: RandomGenerator, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant {
|
||||
suspend fun humanoidConfig(serialized: SerializedData, species: Registry.Entry<Species>): HumanoidConfig {
|
||||
if (serialized.humanoidConfig != null)
|
||||
return Starbound.gson.fromJson(Starbound.loadJsonAsset(serialized.humanoidConfig).await() as JsonObject, HumanoidConfig::class.java)
|
||||
else
|
||||
return species.value.humanoidConfig()
|
||||
}
|
||||
|
||||
fun innates(serialized: SerializedData, level: Double): ArrayList<PersistentStatusEffect> {
|
||||
val innateStatusEffects = ArrayList(serialized.innateStatusEffects)
|
||||
|
||||
innateStatusEffects.add(
|
||||
Either.left(StatModifier.value("powerMultiplier", Registries.jsonFunctions.getOrThrow("npcLevelPowerMultiplierModifier").value.evaluate(level)))
|
||||
)
|
||||
|
||||
innateStatusEffects.add(
|
||||
Either.left(StatModifier.multBase("protection", Registries.jsonFunctions.getOrThrow("npcLevelProtectionMultiplier").value.evaluate(level)))
|
||||
)
|
||||
|
||||
innateStatusEffects.add(
|
||||
Either.left(StatModifier.multBase("maxHealth", Registries.jsonFunctions.getOrThrow("npcLevelHealthMultiplier").value.evaluate(level)))
|
||||
)
|
||||
|
||||
innateStatusEffects.add(
|
||||
Either.left(StatModifier.multBase("maxEnergy", Registries.jsonFunctions.getOrThrow("npcLevelEnergyMultiplier").value.evaluate(level)))
|
||||
)
|
||||
|
||||
return innateStatusEffects
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun read(stream: DataInputStream, isLegacy: Boolean): NPCVariant {
|
||||
val species = Registries.species.getOrThrow(stream.readBinaryString())
|
||||
val type = stream.readInternedString()
|
||||
val level = stream.readDouble(isLegacy)
|
||||
val seed = stream.readLong()
|
||||
val overrides = stream.readJsonElement()
|
||||
|
||||
val initialScriptDelta = if (isLegacy) stream.readInt().toDouble() else stream.readDouble()
|
||||
val identity = HumanoidIdentity.read(stream, isLegacy)
|
||||
val items = stream.readMap({ readInternedString() }, { ItemDescriptor(this) })
|
||||
val persistent = stream.readBoolean()
|
||||
val keepAlive = stream.readBoolean()
|
||||
val damageTeam = stream.readUnsignedByte()
|
||||
val damageTeamType = TeamType.entries[stream.readUnsignedByte()]
|
||||
|
||||
val config = Registries.buildNPCConfig(type, overrides)
|
||||
val serialized = Starbound.gson.fromJson(config, SerializedData::class.java)
|
||||
|
||||
val humanoidConfig = humanoidConfig(serialized, species)
|
||||
|
||||
val movementParameters = humanoidConfig
|
||||
.movementParameters
|
||||
.merge(serialized.movementParameters)
|
||||
|
||||
val disableWornArmor = if (isLegacy) serialized.disableWornArmor else stream.readBoolean()
|
||||
val touchDamage = if (isLegacy) serialized.touchDamage else TouchDamage(stream)
|
||||
val nametagColor = if (isLegacy) serialized.nametagColor else stream.readColor()
|
||||
|
||||
return NPCVariant(
|
||||
species = species,
|
||||
seed = seed,
|
||||
typeName = type,
|
||||
level = level,
|
||||
humanoidConfig = humanoidConfig,
|
||||
humanoidIdentity = identity,
|
||||
overrides = overrides.deepCopy(),
|
||||
scripts = serialized.scripts,
|
||||
initialScriptDelta = initialScriptDelta,
|
||||
scriptConfig = serialized.scriptConfig,
|
||||
movementParameters = movementParameters,
|
||||
statusControllerSettings = serialized.statusControllerSettings,
|
||||
innateStatusEffects = ImmutableList.copyOf(innates(serialized, level)),
|
||||
touchDamage = touchDamage,
|
||||
disableWornArmor = disableWornArmor,
|
||||
dropPools = serialized.dropPools,
|
||||
persistent = persistent,
|
||||
keepAlive = keepAlive,
|
||||
damageTeam = damageTeam,
|
||||
damageTeamType = damageTeamType,
|
||||
nametagColor = nametagColor,
|
||||
items = ImmutableMap.copyOf(items)
|
||||
).also { it.check() }
|
||||
}
|
||||
|
||||
suspend fun create(species: Registry.Entry<Species>, type: String, level: Double, seed: Long, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant {
|
||||
val random = random(seed)
|
||||
val config = Registries.buildNPCConfig(type, overrides)
|
||||
|
||||
val serialized = Starbound.gson.fromJson(config, SerializedData::class.java)
|
||||
val finalLevel = max(0.0, random.nextRange(serialized.levelVariance) + level)
|
||||
TODO()
|
||||
}
|
||||
|
||||
suspend fun create(species: Registry.Entry<Species>, type: String, level: Double, seed: Long, overrides: JsonElement = JsonNull.INSTANCE): NPCVariant {
|
||||
return create(species, type, level, random(seed), overrides)
|
||||
val humanoidConfig = humanoidConfig(serialized, species)
|
||||
|
||||
var identity = species.value.generateHumanoid(random)
|
||||
|
||||
val name = serialized.npcname ?: serialized.nameGen?.let { Starbound.generateNameAsync(it[identity.gender.ordinal], random) } ?: ""
|
||||
val personality = humanoidConfig.personalities.random(random)
|
||||
|
||||
identity = identity.copy(
|
||||
name = name,
|
||||
personalityIdle = personality.idle,
|
||||
personalityArmIdle = personality.armIdle,
|
||||
personalityHeadOffset = personality.headOffset,
|
||||
personalityArmOffset = personality.armOffset
|
||||
)
|
||||
|
||||
if ("identity" in config)
|
||||
identity = Starbound.gson.fromJson(mergeJson(Starbound.gson.toJsonTree(identity), config["identity"]!!), HumanoidIdentity::class.java)
|
||||
|
||||
val movementParameters = humanoidConfig
|
||||
.movementParameters
|
||||
.merge(serialized.movementParameters)
|
||||
|
||||
val items = ImmutableMap.Builder<String, ItemDescriptor>()
|
||||
|
||||
if ("items" in config) {
|
||||
val itemsConfig = config.getAsJsonObject("items")
|
||||
|
||||
val speciesItemsConfig = itemsConfig["override"]?.coalesceNull?.asJsonArray
|
||||
?: itemsConfig[species.key]?.coalesceNull?.asJsonArray
|
||||
?: itemsConfig["default"]?.coalesceNull?.asJsonArray
|
||||
|
||||
if (speciesItemsConfig != null) {
|
||||
val highestLevelItemsConfig = speciesItemsConfig.stream()
|
||||
.map { it.asJsonArray.let { it[0].asDouble to it[1] } }
|
||||
.filter { it.first <= finalLevel }
|
||||
.sorted { o1, o2 -> o2.first.compareTo(o1.first) }
|
||||
.findFirst()
|
||||
.getOrNull()?.second as JsonArray?
|
||||
|
||||
if (highestLevelItemsConfig != null) {
|
||||
var randomColorIndex = -1
|
||||
|
||||
for (itemSlotConfig in highestLevelItemsConfig.random(random).asJsonObject.entrySet()) {
|
||||
var item = ItemDescriptor(itemSlotConfig.value.asJsonArray.random(random))
|
||||
val colorIndex = item.parameters["colorIndex"]
|
||||
|
||||
// Randomize color index if colorIndex is an array
|
||||
if (colorIndex is JsonArray) {
|
||||
if (!serialized.matchColorIndices || randomColorIndex == -1) {
|
||||
randomColorIndex = colorIndex.random(random).asInt
|
||||
}
|
||||
|
||||
item = item.applyParameters(JsonObject().also {
|
||||
it["colorIndex"] = JsonPrimitive(randomColorIndex)
|
||||
})
|
||||
}
|
||||
|
||||
items.put(itemSlotConfig.key, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NPCVariant(
|
||||
species = species,
|
||||
seed = seed,
|
||||
typeName = type,
|
||||
level = finalLevel,
|
||||
humanoidConfig = humanoidConfig,
|
||||
humanoidIdentity = identity,
|
||||
overrides = overrides.deepCopy(),
|
||||
scripts = serialized.scripts,
|
||||
initialScriptDelta = serialized.initialScriptDelta,
|
||||
scriptConfig = serialized.scriptConfig,
|
||||
movementParameters = movementParameters,
|
||||
statusControllerSettings = serialized.statusControllerSettings,
|
||||
innateStatusEffects = ImmutableList.copyOf(innates(serialized, finalLevel)),
|
||||
touchDamage = serialized.touchDamage,
|
||||
disableWornArmor = serialized.disableWornArmor,
|
||||
dropPools = serialized.dropPools,
|
||||
persistent = serialized.persistent,
|
||||
keepAlive = serialized.keepAlive,
|
||||
damageTeam = serialized.damageTeam,
|
||||
damageTeamType = serialized.damageTeamType,
|
||||
nametagColor = serialized.nametagColor,
|
||||
items = items.buildOrThrow()
|
||||
).also { it.check() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,50 @@
|
||||
package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
import ru.dbotthepony.kommons.io.writeStruct2d
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.io.readVector2d
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
// this is because personality data structure is used inconsistently across original source code
|
||||
interface IPersonality {
|
||||
val idle: String
|
||||
val armIdle: String
|
||||
val handOffset: Vector2d
|
||||
val headOffset: Vector2d
|
||||
val armOffset: Vector2d
|
||||
}
|
||||
|
||||
@JsonFactory(asList = true)
|
||||
@JsonFactory(asList = 1)
|
||||
data class Personality(
|
||||
override val idle: String,
|
||||
override val armIdle: String,
|
||||
override val handOffset: Vector2d,
|
||||
override val headOffset: Vector2d,
|
||||
override val armOffset: Vector2d
|
||||
) : IPersonality
|
||||
) : IPersonality {
|
||||
/**
|
||||
* new protocol only
|
||||
*/
|
||||
constructor(stream: DataInputStream) : this(
|
||||
stream.readInternedString(),
|
||||
stream.readInternedString(),
|
||||
stream.readVector2d(),
|
||||
stream.readVector2d(),
|
||||
)
|
||||
|
||||
/**
|
||||
* new protocol only
|
||||
*/
|
||||
fun write(stream: DataOutputStream) {
|
||||
stream.writeBinaryString(idle)
|
||||
stream.writeBinaryString(armIdle)
|
||||
stream.writeStruct2d(headOffset)
|
||||
stream.writeStruct2d(armOffset)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ORIGINAL_ENGINE_VERY_COMPATIBLE_COMPATIBILITY = Personality("", "", Vector2d.ZERO, Vector2d.ZERO)
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,33 @@ package ru.dbotthepony.kstarbound.defs.actor
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.coroutines.future.await
|
||||
import ru.dbotthepony.kommons.collect.filterNotNull
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.AssetPath
|
||||
import ru.dbotthepony.kstarbound.defs.ColorReplacements
|
||||
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
|
||||
import ru.dbotthepony.kstarbound.defs.actor.player.BlueprintLearnList
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.fromJson
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.util.getWrapAround
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import java.util.random.RandomGenerator
|
||||
|
||||
@JsonFactory
|
||||
data class Species(
|
||||
val kind: String,
|
||||
@Deprecated("Use humanoidConfig() function", replaceWith = ReplaceWith("this.humanoidConfig()"))
|
||||
val humanoidConfig: AssetPath = AssetPath("/humanoid.config"),
|
||||
@Deprecated("Use humanoidConfig() function", replaceWith = ReplaceWith("this.humanoidConfig()"))
|
||||
val humanoidOverrides: JsonObject = JsonObject(),
|
||||
val charCreationTooltip: Tooltip = Tooltip(),
|
||||
val nameGen: ImmutableList<String>,
|
||||
val ouchNoises: ImmutableList<AssetPath>,
|
||||
@ -21,6 +36,8 @@ data class Species(
|
||||
val skull: AssetPath = AssetPath("/humanoid/any/dead.png"), // ваш свет угасает
|
||||
val defaultBlueprints: BlueprintLearnList,
|
||||
|
||||
val effectDirectives: String = "",
|
||||
|
||||
val headOptionAsHairColor: Boolean = false,
|
||||
val headOptionAsFacialhair: Boolean = false,
|
||||
val altOptionAsUndyColor: Boolean = false,
|
||||
@ -44,13 +61,92 @@ data class Species(
|
||||
val name: String = "",
|
||||
val image: SpriteReference = SpriteReference.NEVER,
|
||||
val characterImage: SpriteReference = SpriteReference.NEVER,
|
||||
val hairGroup: String? = null,
|
||||
val hairGroup: String = "hair",
|
||||
val hair: ImmutableSet<String>,
|
||||
val shirt: ImmutableSet<ItemDescriptor>,
|
||||
val pants: ImmutableSet<ItemDescriptor>,
|
||||
val facialHairGroup: String? = null,
|
||||
val facialHairGroup: String = "",
|
||||
val facialHair: ImmutableSet<String> = ImmutableSet.of(),
|
||||
val facialMaskGroup: String? = null,
|
||||
val facialMaskGroup: String = "",
|
||||
val facialMask: ImmutableList<String> = ImmutableList.of(),
|
||||
)
|
||||
|
||||
suspend fun humanoidConfig(): HumanoidConfig {
|
||||
return Starbound.gson.fromJson(mergeJson(Starbound.loadJsonAsset(humanoidConfig.fullPath).thenApply { it ?: JsonObject() }.await().deepCopy(), humanoidOverrides), HumanoidConfig::class.java)
|
||||
}
|
||||
|
||||
fun buildStatusEffects(): List<PersistentStatusEffect> {
|
||||
return statusEffects.stream().filter { it.isPresent }.map { Either.right<StatModifier, Registry.Ref<StatusEffectDefinition>>(it) }.toList()
|
||||
}
|
||||
|
||||
suspend fun generateHumanoid(random: RandomGenerator): HumanoidIdentity {
|
||||
val gender = Gender.valueOf(random.nextBoolean())
|
||||
val name = Starbound.generateNameAsync(nameGen[gender.ordinal], random)
|
||||
|
||||
val genderSettings = genders[gender.ordinal]
|
||||
var bodyColor = bodyColor.random(random).toImageOperator()
|
||||
|
||||
var altColor = ""
|
||||
|
||||
val altOpt = random.nextInt()
|
||||
val headOpt = random.nextInt()
|
||||
val hairOpt = random.nextInt()
|
||||
|
||||
if (altOptionAsUndyColor) {
|
||||
altColor = undyColor.getWrapAround(altOpt).toImageOperator()
|
||||
}
|
||||
|
||||
val hair = genderSettings.hair.getWrapAround(hairOpt)
|
||||
var hairColor = bodyColor
|
||||
|
||||
if (headOptionAsHairColor) {
|
||||
hairColor = this.hairColor.getWrapAround(headOpt).toImageOperator()
|
||||
if (altOptionAsHairColor) hairColor += this.undyColor.getWrapAround(altOpt).toImageOperator()
|
||||
}
|
||||
|
||||
if (hairColorAsBodySubColor)
|
||||
bodyColor += hairColor
|
||||
|
||||
var facialHair = ""
|
||||
var facialHairGroup = ""
|
||||
var facialHairDirective = ""
|
||||
|
||||
if (headOptionAsFacialhair) {
|
||||
facialHair = genderSettings.facialHair.getWrapAround(headOpt)
|
||||
facialHairGroup = genderSettings.facialHairGroup
|
||||
facialHairDirective = hairColor
|
||||
}
|
||||
|
||||
var facialMask = ""
|
||||
var facialMaskGroup = ""
|
||||
var facialMaskDirectives = ""
|
||||
|
||||
if (altOptionAsFacialMask) {
|
||||
facialMask = genderSettings.facialMask.getWrapAround(altOpt)
|
||||
facialMaskGroup = genderSettings.facialMaskGroup
|
||||
}
|
||||
|
||||
if (bodyColorAsFacialMaskSubColor)
|
||||
facialMaskDirectives += bodyColor
|
||||
|
||||
if (altColorAsFacialMaskSubColor)
|
||||
facialMaskDirectives += altColor
|
||||
|
||||
return HumanoidIdentity(
|
||||
name = name,
|
||||
species = Registries.species.getOrThrow(kind).ref,
|
||||
gender = gender,
|
||||
hairGroup = genderSettings.hairGroup,
|
||||
hairType = hair,
|
||||
hairDirectives = hairColor,
|
||||
bodyDirectives = bodyColor + altColor,
|
||||
emoteDirectives = bodyColor + altColor,
|
||||
facialHairGroup = facialHairGroup,
|
||||
facialHairType = facialHair,
|
||||
facialHairDirectives = facialHairDirective,
|
||||
facialMaskGroup = facialMaskGroup,
|
||||
facialMaskType = facialMask,
|
||||
facialMaskDirectives = facialMaskDirectives
|
||||
).also { it.check() }
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,12 @@ typealias PersistentStatusEffect = Either<StatModifier, Registry.Ref<StatusEffec
|
||||
// uint8_t
|
||||
enum class Gender(override val jsonName: String) : IStringSerializable {
|
||||
MALE("Male"), FEMALE("Female");
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Boolean): Gender {
|
||||
return if (value) MALE else FEMALE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// int32_t
|
||||
|
@ -31,6 +31,8 @@ data class PlayerConfig(
|
||||
|
||||
val defaultBlueprints: BlueprintLearnList,
|
||||
|
||||
val blinkInterval: Vector2d,
|
||||
|
||||
val defaultCodexes: ImmutableMap<String, ImmutableList<ItemDescriptor>>,
|
||||
val metaBoundBox: AABB,
|
||||
val movementParameters: ActorMovementParameters,
|
||||
|
@ -1,18 +1,29 @@
|
||||
package ru.dbotthepony.kstarbound.defs.dungeon
|
||||
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.collect.ImmutableSet
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kommons.collect.collect
|
||||
import ru.dbotthepony.kommons.collect.filterNotNull
|
||||
import ru.dbotthepony.kommons.collect.map
|
||||
import ru.dbotthepony.kommons.gson.contains
|
||||
import ru.dbotthepony.kommons.gson.set
|
||||
import ru.dbotthepony.kommons.util.Either
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.actor.Species
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
||||
@ -23,8 +34,10 @@ import ru.dbotthepony.kstarbound.defs.tile.isRealModifier
|
||||
import ru.dbotthepony.kstarbound.defs.tile.orEmptyLiquid
|
||||
import ru.dbotthepony.kstarbound.defs.tile.orEmptyModifier
|
||||
import ru.dbotthepony.kstarbound.defs.tile.orEmptyTile
|
||||
import ru.dbotthepony.kstarbound.fromJsonFast
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonIgnore
|
||||
import ru.dbotthepony.kstarbound.util.AssetPathStack
|
||||
import ru.dbotthepony.kstarbound.util.random.random
|
||||
import ru.dbotthepony.kstarbound.world.Direction
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
|
||||
@ -218,10 +231,83 @@ abstract class DungeonBrush {
|
||||
}
|
||||
}
|
||||
|
||||
data class NPC(val data: JsonObject) : DungeonBrush() {
|
||||
class NPC(data: JsonObject) : DungeonBrush() {
|
||||
@JsonFactory
|
||||
data class NPCConfig(
|
||||
val seed: Either<Long, String>? = null,
|
||||
val species: String,
|
||||
val typeName: String = "default",
|
||||
val parameters: JsonObject = JsonObject(),
|
||||
) {
|
||||
private val assetCommentary = AssetPathStack.assetCommentary()
|
||||
// interpret species as a comma separated list of unquoted strings
|
||||
val speciesList: ImmutableSet<Registry.Ref<Species>> = ImmutableSet.copyOf(species.trim().replace(" ", "").split(',').map { it.trim() }.filter { it.isNotEmpty() }.map { Registries.species.ref(it) })
|
||||
private var triggeredWarning = false
|
||||
|
||||
val speciesListResolved: ImmutableList<Registry.Entry<Species>> get() {
|
||||
val result = speciesList.iterator().map { it.entry }.filterNotNull().collect(ImmutableList.toImmutableList())
|
||||
|
||||
if (result.isEmpty() && !triggeredWarning) {
|
||||
triggeredWarning = true
|
||||
LOGGER.error("No valid species specified or species string is empty: $species$assetCommentary")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
init {
|
||||
if ("persistent" !in parameters) {
|
||||
parameters["persistent"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonFactory
|
||||
data class MonsterConfig(
|
||||
val seed: Either<Long, String>? = null,
|
||||
val typeName: Registry.Ref<MonsterTypeDefinition>,
|
||||
val parameters: JsonObject = JsonObject(),
|
||||
) {
|
||||
init {
|
||||
if (seed != null && seed.isRight && seed.right() != "stable")
|
||||
|
||||
if ("persistent" !in parameters) {
|
||||
parameters["persistent"] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val data: Either<NPCConfig, MonsterConfig>
|
||||
|
||||
init {
|
||||
val kind = data["kind"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing NPC 'kind' in NPC brush data")
|
||||
|
||||
if (kind == "npc") {
|
||||
this.data = Either.left(Starbound.gson.fromJsonFast(data, NPCConfig::class.java))
|
||||
} else if (kind == "monster") {
|
||||
this.data = Either.right(Starbound.gson.fromJsonFast(data, MonsterConfig::class.java))
|
||||
} else {
|
||||
throw JsonSyntaxException("Unknown NPC type $kind!")
|
||||
}
|
||||
}
|
||||
|
||||
override fun execute(x: Int, y: Int, phase: Phase, world: DungeonWorld) {
|
||||
if (phase === Phase.PLACE_NPCS) {
|
||||
LOGGER.warn("NYI: NPC at $x, $y")
|
||||
data.map({
|
||||
val speciesListResolved = it.speciesListResolved
|
||||
|
||||
if (speciesListResolved.isNotEmpty()) {
|
||||
world.placeNPC(x, y, speciesListResolved.random(world.random), it.typeName, it.seed?.map({ it }, { world.random.nextLong() }) ?: world.random.nextLong(), it.parameters)
|
||||
} else {
|
||||
LOGGER.error("Unable to place NPC ${it.typeName}, because species list is empty or contain no valid species")
|
||||
}
|
||||
}, {
|
||||
if (it.typeName.isPresent) {
|
||||
world.placeMonster(x, y, it.typeName.entryOrThrow, it.seed?.map({ it }, { world.random.nextLong() }) ?: world.random.nextLong(), it.parameters)
|
||||
} else {
|
||||
LOGGER.error("Unable to place Monster ${it.typeName} at $x $y, because no such monster exists")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
package ru.dbotthepony.kstarbound.defs.dungeon
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.future.await
|
||||
import org.apache.logging.log4j.LogManager
|
||||
import ru.dbotthepony.kstarbound.math.AABBi
|
||||
@ -10,7 +15,11 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2i
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.actor.NPCVariant
|
||||
import ru.dbotthepony.kstarbound.defs.actor.Species
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.monster.MonsterVariant
|
||||
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
|
||||
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
|
||||
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
|
||||
@ -27,16 +36,16 @@ import ru.dbotthepony.kstarbound.util.random.random
|
||||
import ru.dbotthepony.kstarbound.world.ChunkPos
|
||||
import ru.dbotthepony.kstarbound.world.ChunkState
|
||||
import ru.dbotthepony.kstarbound.world.Direction
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractCell
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState
|
||||
import ru.dbotthepony.kstarbound.world.api.AbstractTileState
|
||||
import ru.dbotthepony.kstarbound.world.api.MutableTileState
|
||||
import ru.dbotthepony.kstarbound.world.api.TileColor
|
||||
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.NPCEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WireConnection
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import java.util.Collections
|
||||
import java.util.LinkedHashSet
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.Consumer
|
||||
@ -84,6 +93,19 @@ class DungeonWorld(
|
||||
val parameters: JsonObject = JsonObject()
|
||||
)
|
||||
|
||||
private data class NPCData(
|
||||
val species: Registry.Entry<Species>,
|
||||
val type: String,
|
||||
val seed: Long,
|
||||
val overrides: JsonElement = JsonNull.INSTANCE
|
||||
)
|
||||
|
||||
private data class MonsterData(
|
||||
val type: Registry.Entry<MonsterTypeDefinition>,
|
||||
val seed: Long,
|
||||
val overrides: JsonObject = JsonObject()
|
||||
)
|
||||
|
||||
var hasGenerated = false
|
||||
private set
|
||||
|
||||
@ -168,6 +190,9 @@ class DungeonWorld(
|
||||
|
||||
private val placedObjects = LinkedHashMap<Vector2i, PlacedObject>()
|
||||
|
||||
private val placedNPCs = LinkedHashMap<Vector2i, ArrayList<NPCData>>()
|
||||
private val placedMonsters = LinkedHashMap<Vector2i, ArrayList<MonsterData>>()
|
||||
|
||||
var playerStart: Vector2d? = null
|
||||
|
||||
fun finishPart() {
|
||||
@ -189,6 +214,14 @@ class DungeonWorld(
|
||||
table.computeIfAbsent(group) { LinkedHashSet() }.add(geometry.wrap(Vector2i(x, y)))
|
||||
}
|
||||
|
||||
fun placeNPC(x: Int, y: Int, species: Registry.Entry<Species>, type: String, seed: Long, overrides: JsonElement = JsonNull.INSTANCE) {
|
||||
placedNPCs.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(NPCData(species, type, seed, overrides))
|
||||
}
|
||||
|
||||
fun placeMonster(x: Int, y: Int, type: Registry.Entry<MonsterTypeDefinition>, seed: Long, overrides: JsonObject = JsonObject()) {
|
||||
placedMonsters.computeIfAbsent(geometry.wrap(Vector2i(x, y))) { ArrayList() }.add(MonsterData(type, seed, overrides))
|
||||
}
|
||||
|
||||
fun requestLiquid(x: Int, y: Int, liquid: AbstractLiquidState) {
|
||||
pendingLiquids[geometry.wrap(Vector2i(x, y))] = liquid
|
||||
}
|
||||
@ -495,6 +528,28 @@ class DungeonWorld(
|
||||
val tickets = ArrayList<ServerChunk.ITicket>()
|
||||
|
||||
try {
|
||||
// construct npc variants as early as possible because they potentially involve disk I/O
|
||||
val npcVariantsJob = Starbound.GLOBAL_SCOPE.async {
|
||||
val variants = ArrayList<Pair<Vector2i, Deferred<NPCVariant?>>>(placedNPCs.values.sumOf { it.size })
|
||||
|
||||
for ((pos, npcs) in placedNPCs) {
|
||||
for (npc in npcs) {
|
||||
val variant = async {
|
||||
try {
|
||||
NPCVariant.create(npc.species, npc.type, parent.template.threatLevel, npc.seed, npc.overrides)
|
||||
} catch (err: Throwable) {
|
||||
LOGGER.error("Unable to place NPC '${npc.type}' with species '${npc.species}' at $pos", err)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
variants.add(pos to variant)
|
||||
}
|
||||
}
|
||||
|
||||
variants.map { it.first to it.second.await() }
|
||||
}
|
||||
|
||||
val terrainBlendingVertexes = ArrayList<Vector2d>()
|
||||
val spaceBlendingVertexes = ArrayList<Vector2d>()
|
||||
|
||||
@ -600,6 +655,15 @@ class DungeonWorld(
|
||||
}, Starbound.EXECUTOR)
|
||||
}
|
||||
|
||||
val monsterVariants = ArrayList<Pair<Vector2i, MonsterVariant>>(placedMonsters.values.sumOf { it.size })
|
||||
|
||||
for ((pos, monsters) in placedMonsters) {
|
||||
for (monster in monsters) {
|
||||
val variant = monster.type.value.create(monster.seed, monster.overrides)
|
||||
monsterVariants.add(pos to variant)
|
||||
}
|
||||
}
|
||||
|
||||
val biomeItems = ArrayList<() -> Unit>()
|
||||
|
||||
for (biomeTree in biomeTreesFutures) {
|
||||
@ -627,6 +691,8 @@ class DungeonWorld(
|
||||
}
|
||||
}
|
||||
|
||||
val npcVariants = npcVariantsJob.await()
|
||||
|
||||
// wait for all chunks to be loaded (and cell changes to be applied)
|
||||
// if any of cell change operation fails, entire generation fails... leaving world in inconsistent state,
|
||||
// but also limiting damage by exiting early.
|
||||
@ -680,6 +746,20 @@ class DungeonWorld(
|
||||
}
|
||||
}
|
||||
|
||||
for ((pos, variant) in npcVariants) {
|
||||
val ent = NPCEntity(variant ?: continue)
|
||||
ent.movement.xPosition = pos.x.toDouble()
|
||||
ent.movement.yPosition = pos.y.toDouble()
|
||||
ent.joinWorld(parent)
|
||||
}
|
||||
|
||||
for ((pos, variant) in monsterVariants) {
|
||||
val ent = MonsterEntity(variant)
|
||||
ent.movement.xPosition = pos.x.toDouble()
|
||||
ent.movement.yPosition = pos.y.toDouble()
|
||||
ent.joinWorld(parent)
|
||||
}
|
||||
|
||||
parent.enableDungeonTileProtection = true
|
||||
}.await()
|
||||
} finally {
|
||||
|
@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.VersionRegistry
|
||||
import ru.dbotthepony.kstarbound.io.readInternedString
|
||||
import ru.dbotthepony.kstarbound.item.ItemRegistry
|
||||
import ru.dbotthepony.kstarbound.item.ItemStack
|
||||
import ru.dbotthepony.kstarbound.json.mergeJson
|
||||
import ru.dbotthepony.kstarbound.json.readJsonElement
|
||||
import ru.dbotthepony.kstarbound.json.writeJsonElement
|
||||
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
|
||||
@ -162,7 +163,6 @@ fun ItemDescriptor(stream: DataInputStream): ItemDescriptor {
|
||||
* [parameters] is considered to be immutable and should not be modified
|
||||
* directly (must be copied for mutable context)
|
||||
*/
|
||||
@JsonAdapter(ItemDescriptor.Adapter::class)
|
||||
data class ItemDescriptor(
|
||||
val name: String,
|
||||
val count: Long,
|
||||
@ -172,6 +172,10 @@ data class ItemDescriptor(
|
||||
val isNotEmpty get() = !isEmpty
|
||||
val ref by lazy { if (name == "") ItemRegistry.AIR else ItemRegistry[name] }
|
||||
|
||||
fun applyParameters(parameters: JsonObject): ItemDescriptor {
|
||||
return copy(parameters = mergeJson(this.parameters.deepCopy(), parameters))
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ItemDescriptor[$name, $count, $parameters]"
|
||||
}
|
||||
@ -240,7 +244,7 @@ data class ItemDescriptor(
|
||||
stream.writeJsonElement(parameters)
|
||||
}
|
||||
|
||||
class Adapter(gson: Gson) : TypeAdapter<ItemDescriptor>() {
|
||||
object Adapter : TypeAdapter<ItemDescriptor>() {
|
||||
override fun write(out: JsonWriter, value: ItemDescriptor) {
|
||||
if (value.isEmpty)
|
||||
out.nullValue()
|
||||
|
@ -5,6 +5,7 @@ import com.google.gson.Gson
|
||||
import com.google.gson.TypeAdapter
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.stream.JsonReader
|
||||
import com.google.gson.stream.JsonToken
|
||||
import com.google.gson.stream.JsonWriter
|
||||
import ru.dbotthepony.kommons.io.readCollection
|
||||
import ru.dbotthepony.kommons.io.writeBinaryString
|
||||
@ -38,7 +39,21 @@ data class QuestArcDescriptor(
|
||||
if (stagehandUniqueId != null) stream.writeBinaryString(stagehandUniqueId)
|
||||
}
|
||||
|
||||
class Adapter(gson: Gson) : VersionedAdapter<QuestArcDescriptor>("QuestArcDescriptor", FactoryAdapter.createFor(QuestArcDescriptor::class, gson))
|
||||
class Adapter(gson: Gson) : TypeAdapter<QuestArcDescriptor>() {
|
||||
private val parent = VersionedAdapter("QuestArcDescriptor", FactoryAdapter.createFor(QuestArcDescriptor::class, gson))
|
||||
|
||||
override fun write(out: JsonWriter, value: QuestArcDescriptor) {
|
||||
parent.write(out, value)
|
||||
}
|
||||
|
||||
override fun read(`in`: JsonReader): QuestArcDescriptor {
|
||||
if (`in`.peek() == JsonToken.STRING) {
|
||||
return QuestArcDescriptor(ImmutableList.of(QuestDescriptor(`in`.nextString())))
|
||||
} else {
|
||||
return parent.read(`in`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CODEC = nativeCodec(::QuestArcDescriptor, QuestArcDescriptor::write)
|
||||
|
@ -14,6 +14,7 @@ import ru.dbotthepony.kstarbound.json.builder.FactoryAdapter
|
||||
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
|
||||
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
|
||||
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
|
||||
import ru.dbotthepony.kstarbound.util.random.threadLocalRandom
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
@ -22,7 +23,8 @@ data class QuestDescriptor(
|
||||
val questId: String,
|
||||
val templateId: String = questId,
|
||||
val parameters: ImmutableMap<String, QuestParameter> = ImmutableMap.of(),
|
||||
val seed: Long = makeSeed(),
|
||||
// TODO: this is original engine behavior, and this is stupid
|
||||
val seed: Long = threadLocalRandom.nextLong(),
|
||||
) {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(
|
||||
stream.readInternedString(),
|
||||
@ -43,9 +45,5 @@ data class QuestDescriptor(
|
||||
companion object {
|
||||
val CODEC = nativeCodec(::QuestDescriptor, QuestDescriptor::write)
|
||||
val LEGACY_CODEC = legacyCodec(::QuestDescriptor, QuestDescriptor::write)
|
||||
|
||||
fun makeSeed(): Long {
|
||||
return System.nanoTime().rotateLeft(27).xor(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ data class RenderMatch(
|
||||
val haltOnMatch: Boolean = false,
|
||||
val haltOnSubMatch: Boolean = false,
|
||||
) {
|
||||
@JsonFactory(asList = true)
|
||||
@JsonFactory(asList = 1)
|
||||
data class Piece(
|
||||
val name: String,
|
||||
val offset: Vector2i
|
||||
@ -140,7 +140,7 @@ data class RenderMatch(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonFactory(asList = true)
|
||||
@JsonFactory(asList = 1)
|
||||
data class Matcher(
|
||||
val offset: Vector2i,
|
||||
val ruleName: String
|
||||
@ -204,7 +204,7 @@ data class RenderMatch(
|
||||
}
|
||||
}
|
||||
|
||||
@JsonFactory(asList = true)
|
||||
@JsonFactory(asList = 1)
|
||||
data class RenderMatchList(
|
||||
val name: String,
|
||||
val list: ImmutableList<RenderMatch>
|
||||
|
@ -31,7 +31,7 @@ class BushVariant(
|
||||
val ephemeral: Boolean,
|
||||
val tileDamageParameters: TileDamageParameters,
|
||||
) {
|
||||
@JsonFactory(asList = true)
|
||||
@JsonFactory(asList = 1)
|
||||
data class Shape(val image: String, val mods: ImmutableList<String>)
|
||||
|
||||
@JsonFactory
|
||||
|
@ -287,7 +287,7 @@ val BigDecimalValueCodec = StreamCodec.Impl(DataInputStream::readBigDecimal, Dat
|
||||
val UUIDValueCodec = StreamCodec.Impl({ s -> UUID(s.readLong(), s.readLong()) }, { s, v -> s.writeLong(v.mostSignificantBits); s.writeLong(v.leastSignificantBits) })
|
||||
val VarIntValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarInt, DataOutputStream::writeSignedVarInt)
|
||||
val VarLongValueCodec = StreamCodec.Impl(DataInputStream::readSignedVarLong, DataOutputStream::writeSignedVarLong)
|
||||
val BinaryStringCodec = StreamCodec.Impl(DataInputStream::readBinaryString, DataOutputStream::writeBinaryString)
|
||||
val BinaryStringCodec = StreamCodec.Impl(DataInputStream::readInternedString, DataOutputStream::writeBinaryString)
|
||||
|
||||
val UnsignedVarLongCodec = StreamCodec.Impl(DataInputStream::readVarLong, DataOutputStream::writeVarLong)
|
||||
val UnsignedVarIntCodec = StreamCodec.Impl(DataInputStream::readVarInt, DataOutputStream::writeVarInt)
|
||||
@ -334,5 +334,21 @@ val KOptionalBinaryStringCodec = StreamCodec.KOptional(BinaryStringCodec)
|
||||
val KOptionalRGBCodec = StreamCodec.KOptional(RGBCodec)
|
||||
val KOptionalRGBACodec = StreamCodec.KOptional(RGBACodec)
|
||||
|
||||
val NullableBooleanValueCodec = StreamCodec.Nullable(BooleanValueCodec)
|
||||
val NullableByteValueCodec = StreamCodec.Nullable(ByteValueCodec)
|
||||
val NullableShortValueCodec = StreamCodec.Nullable(ShortValueCodec)
|
||||
val NullableCharValueCodec = StreamCodec.Nullable(CharValueCodec)
|
||||
val NullableIntValueCodec = StreamCodec.Nullable(IntValueCodec)
|
||||
val NullableLongValueCodec = StreamCodec.Nullable(LongValueCodec)
|
||||
val NullableFloatValueCodec = StreamCodec.Nullable(FloatValueCodec)
|
||||
val NullableDoubleValueCodec = StreamCodec.Nullable(DoubleValueCodec)
|
||||
val NullableBigDecimalValueCodec = StreamCodec.Nullable(BigDecimalValueCodec)
|
||||
val NullableUUIDValueCodec = StreamCodec.Nullable(UUIDValueCodec)
|
||||
val NullableVarIntValueCodec = StreamCodec.Nullable(VarIntValueCodec)
|
||||
val NullableVarLongValueCodec = StreamCodec.Nullable(VarLongValueCodec)
|
||||
val NullableBinaryStringCodec = StreamCodec.Nullable(BinaryStringCodec)
|
||||
val NullableRGBCodec = StreamCodec.Nullable(RGBCodec)
|
||||
val NullableRGBACodec = StreamCodec.Nullable(RGBACodec)
|
||||
|
||||
fun <E : Enum<E>> Class<E>.codec() = StreamCodec.Enum(this)
|
||||
fun <E : Enum<E>> KClass<E>.codec() = StreamCodec.Enum(this.java)
|
||||
|
@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.Globals
|
||||
import ru.dbotthepony.kstarbound.Registry
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.Drawable
|
||||
import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
|
||||
import ru.dbotthepony.kstarbound.defs.image.SpriteReference
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemRarity
|
||||
@ -262,6 +263,12 @@ open class ItemStack(val entry: ItemRegistry.Entry, val config: JsonObject, para
|
||||
return AgingResult(null, false)
|
||||
}
|
||||
|
||||
open val effects: Collection<String>
|
||||
get() = listOf()
|
||||
|
||||
open val statusEffects: Collection<PersistentStatusEffect>
|
||||
get() = listOf()
|
||||
|
||||
fun createDescriptor(): ItemDescriptor {
|
||||
if (isEmpty)
|
||||
return ItemDescriptor.EMPTY
|
||||
|
@ -153,15 +153,16 @@ enum class JsonPatch(val key: String) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
suspend fun applyAsync(base: JsonElement, source: Collection<IStarboundFile>?): JsonElement {
|
||||
source ?: return base
|
||||
val loaded = source.map { it.asyncJsonReader() to it }
|
||||
var base = base
|
||||
|
||||
for (patch in source) {
|
||||
val read = Starbound.ELEMENTS_ADAPTER.read(patch.asyncJsonReader().await())
|
||||
for ((patch, f) in loaded) {
|
||||
val read = Starbound.ELEMENTS_ADAPTER.read(patch.await())
|
||||
|
||||
if (read !is JsonArray) {
|
||||
LOGGER.error("$patch root element is not an array")
|
||||
} else {
|
||||
base = apply(base, read, patch)
|
||||
base = apply(base, read, f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ import com.google.gson.stream.JsonWriter
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.VersionRegistry
|
||||
|
||||
abstract class VersionedAdapter<T>(val name: String, val parent: TypeAdapter<T>) : TypeAdapter<T>() {
|
||||
open class VersionedAdapter<T>(val name: String, val parent: TypeAdapter<T>) : TypeAdapter<T>() {
|
||||
override fun write(out: JsonWriter, value: T) {
|
||||
if (Starbound.IS_STORE_JSON) {
|
||||
VersionRegistry.make(name, parent.toJsonTree(value)).toJson(out)
|
||||
|
@ -61,7 +61,12 @@ annotation class JsonBuilder
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class JsonFactory(
|
||||
val asList: Boolean = false,
|
||||
/**
|
||||
* * -1 - input can be only as object
|
||||
* * 0 - input may be either an object or a list, output is constructed as object
|
||||
* * 1 - input can be only as list
|
||||
*/
|
||||
val asList: Int = -1,
|
||||
val logMisses: Boolean = false,
|
||||
)
|
||||
|
||||
|
@ -47,15 +47,19 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
val clazz: KClass<T>,
|
||||
val types: ImmutableList<ReferencedProperty<T, *>>,
|
||||
aliases: Map<String, List<String>>,
|
||||
val asJsonArray: Boolean,
|
||||
val asJsonArray: AsJsonArray,
|
||||
val stringInterner: Interner<String>,
|
||||
val logMisses: Boolean,
|
||||
) : TypeAdapter<T>() {
|
||||
enum class AsJsonArray {
|
||||
NO, EITHER, YES
|
||||
}
|
||||
|
||||
private val name2index = Object2ObjectArrayMap<String, IntArrayList>()
|
||||
private val loggedMisses = Collections.synchronizedSet(ObjectArraySet<String>())
|
||||
|
||||
init {
|
||||
if (asJsonArray && types.any { it.isFlat }) {
|
||||
if (asJsonArray != AsJsonArray.NO && types.any { it.isFlat }) {
|
||||
throw IllegalArgumentException("Can't have both flat properties and input be as json array")
|
||||
}
|
||||
|
||||
@ -159,7 +163,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
return
|
||||
}
|
||||
|
||||
if (asJsonArray)
|
||||
if (asJsonArray == AsJsonArray.YES)
|
||||
out.beginArray()
|
||||
else
|
||||
out.beginObject()
|
||||
@ -169,7 +173,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
continue
|
||||
|
||||
if (type.isFlat) {
|
||||
check(!asJsonArray)
|
||||
check(asJsonArray != AsJsonArray.YES)
|
||||
|
||||
val (field, adapter) = type
|
||||
val result = (adapter as TypeAdapter<Any>).toJsonTree((field as KProperty1<T, Any>).get(value))
|
||||
@ -189,10 +193,10 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
val getValue = field.get(value)
|
||||
|
||||
// god fucking damn it
|
||||
if (!asJsonArray && getValue === null)
|
||||
if (asJsonArray != AsJsonArray.YES && getValue === null)
|
||||
continue
|
||||
|
||||
if (!asJsonArray)
|
||||
if (asJsonArray != AsJsonArray.YES)
|
||||
out.name(field.name)
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
@ -200,7 +204,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (asJsonArray)
|
||||
if (asJsonArray == AsJsonArray.YES)
|
||||
out.endArray()
|
||||
else
|
||||
out.endObject()
|
||||
@ -216,9 +220,10 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
|
||||
@Suppress("name_shadowing")
|
||||
var reader = reader
|
||||
val readingAsArray = asJsonArray == AsJsonArray.YES || asJsonArray == AsJsonArray.EITHER && reader.peek() == JsonToken.BEGIN_ARRAY
|
||||
|
||||
// Если нам необходимо читать объект как набор данных массива, то давай
|
||||
if (asJsonArray) {
|
||||
if (readingAsArray) {
|
||||
reader.beginArray()
|
||||
val iterator = types.iterator()
|
||||
var fieldId = 0
|
||||
@ -333,7 +338,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
if (asJsonArray) {
|
||||
if (readingAsArray) {
|
||||
reader.endArray()
|
||||
} else {
|
||||
reader.endObject()
|
||||
@ -415,8 +420,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
* Позволяет построить класс [FactoryAdapter] на основе заданных параметров
|
||||
*/
|
||||
class Builder<T : Any>(val clazz: KClass<T>, vararg fields: KProperty1<T, *>) : TypeAdapterFactory {
|
||||
private var asList = false
|
||||
private var storesJson = false
|
||||
private var asList = AsJsonArray.NO
|
||||
private val types = ArrayList<ReferencedProperty<T, *>>()
|
||||
private val aliases = Object2ObjectArrayMap<String, ArrayList<String>>()
|
||||
var stringInterner: Interner<String> = Interner { it }
|
||||
@ -440,7 +444,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
* Собирает этот [FactoryAdapter] с указанным GSON объектом
|
||||
*/
|
||||
fun build(gson: Gson): TypeAdapter<T> {
|
||||
check(!asList || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }
|
||||
check(asList == AsJsonArray.NO || types.none { it.isFlat }) { "Can't have both flat properties and json data array layout" }
|
||||
|
||||
return FactoryAdapter(
|
||||
clazz = clazz,
|
||||
@ -478,7 +482,12 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
* @see inputAsList
|
||||
*/
|
||||
fun inputAsMap(): Builder<T> {
|
||||
asList = false
|
||||
asList = AsJsonArray.NO
|
||||
return this
|
||||
}
|
||||
|
||||
fun inputAsMapOrList(): Builder<T> {
|
||||
asList = AsJsonArray.EITHER
|
||||
return this
|
||||
}
|
||||
|
||||
@ -492,7 +501,7 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
* @see inputAsMap
|
||||
*/
|
||||
fun inputAsList(): Builder<T> {
|
||||
asList = true
|
||||
asList = AsJsonArray.YES
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -504,7 +513,11 @@ class FactoryAdapter<T : Any> private constructor(
|
||||
val builder = Builder(kclass)
|
||||
val properties = kclass.declaredMembers.filterIsInstance<KProperty1<T, *>>()
|
||||
|
||||
if (config.asList) {
|
||||
if (config.asList < 0) {
|
||||
builder.inputAsMap()
|
||||
} else if (config.asList == 0) {
|
||||
builder.inputAsMapOrList()
|
||||
} else {
|
||||
builder.inputAsList()
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import ru.dbotthepony.kstarbound.math.Line2d
|
||||
import ru.dbotthepony.kstarbound.world.entities.AbstractEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.ActorEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.NPCEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject
|
||||
|
||||
@ -24,6 +25,9 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) {
|
||||
if (self is ActorEntity)
|
||||
provideStatusControllerBindings(self.statusController, lua)
|
||||
|
||||
if (self is NPCEntity)
|
||||
provideNPCBindings(self, lua)
|
||||
|
||||
provideWorldBindings(self.world, lua)
|
||||
|
||||
val table = lua.newTable()
|
||||
|
@ -27,13 +27,6 @@ import ru.dbotthepony.kstarbound.world.entities.ActorEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.MonsterEntity
|
||||
|
||||
fun provideMonsterBindings(self: MonsterEntity, lua: LuaEnvironment) {
|
||||
val config = lua.newTable()
|
||||
lua.globals["config"] = config
|
||||
|
||||
config["getParameter"] = createConfigBinding { key, default ->
|
||||
key.find(self.variant.parameters) ?: default
|
||||
}
|
||||
|
||||
val callbacks = lua.newTable()
|
||||
lua.globals["monster"] = callbacks
|
||||
|
||||
|
@ -18,7 +18,7 @@ import ru.dbotthepony.kstarbound.lua.toVector2d
|
||||
import ru.dbotthepony.kstarbound.math.vector.Vector2d
|
||||
import ru.dbotthepony.kstarbound.world.Direction
|
||||
import ru.dbotthepony.kstarbound.world.entities.ActorMovementController
|
||||
import ru.dbotthepony.kstarbound.world.entities.AnchorState
|
||||
import ru.dbotthepony.kstarbound.world.entities.AnchorNetworkState
|
||||
import ru.dbotthepony.kstarbound.world.physics.Poly
|
||||
import kotlin.math.PI
|
||||
|
||||
@ -82,15 +82,15 @@ class MovementControllerBindings(val self: ActorMovementController) {
|
||||
}
|
||||
|
||||
callbacks["setAnchorState"] = luaFunction { anchor: Number, index: Number ->
|
||||
self.anchorState = AnchorState(anchor.toInt(), index.toInt())
|
||||
self.anchorNetworkState = AnchorNetworkState(anchor.toInt(), index.toInt())
|
||||
}
|
||||
|
||||
callbacks["resetAnchorState"] = luaFunction {
|
||||
self.anchorState = null
|
||||
self.anchorNetworkState = null
|
||||
}
|
||||
|
||||
callbacks["anchorState"] = luaFunction {
|
||||
val anchorState = self.anchorState
|
||||
val anchorState = self.anchorNetworkState
|
||||
|
||||
if (anchorState != null) {
|
||||
returnBuffer.setTo(anchorState.entityID, anchorState.positionIndex)
|
||||
@ -146,8 +146,8 @@ class MovementControllerBindings(val self: ActorMovementController) {
|
||||
callbacks["baseParameters"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.actorMovementParameters))) }
|
||||
callbacks["walking"] = luaFunction { returnBuffer.setTo(self.isWalking) }
|
||||
callbacks["running"] = luaFunction { returnBuffer.setTo(self.isRunning) }
|
||||
callbacks["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.luaValue) }
|
||||
callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.luaValue) }
|
||||
callbacks["movingDirection"] = luaFunction { returnBuffer.setTo(self.movingDirection.numericalValue) }
|
||||
callbacks["facingDirection"] = luaFunction { returnBuffer.setTo(self.facingDirection.numericalValue) }
|
||||
callbacks["crouching"] = luaFunction { returnBuffer.setTo(self.isCrouching) }
|
||||
callbacks["flying"] = luaFunction { returnBuffer.setTo(self.isFlying) }
|
||||
callbacks["falling"] = luaFunction { returnBuffer.setTo(self.isFalling) }
|
||||
|
@ -0,0 +1,222 @@
|
||||
package ru.dbotthepony.kstarbound.lua.bindings
|
||||
|
||||
import org.classdump.luna.ByteString
|
||||
import org.classdump.luna.Table
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
|
||||
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
|
||||
import ru.dbotthepony.kstarbound.defs.quest.QuestArcDescriptor
|
||||
import ru.dbotthepony.kstarbound.fromJsonFast
|
||||
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
|
||||
import ru.dbotthepony.kstarbound.lua.from
|
||||
import ru.dbotthepony.kstarbound.lua.get
|
||||
import ru.dbotthepony.kstarbound.lua.iterator
|
||||
import ru.dbotthepony.kstarbound.lua.luaFunction
|
||||
import ru.dbotthepony.kstarbound.lua.set
|
||||
import ru.dbotthepony.kstarbound.lua.tableOf
|
||||
import ru.dbotthepony.kstarbound.lua.toByteString
|
||||
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
|
||||
import ru.dbotthepony.kstarbound.lua.toVector2d
|
||||
import ru.dbotthepony.kstarbound.util.SBPattern
|
||||
import ru.dbotthepony.kstarbound.util.sbIntern
|
||||
import ru.dbotthepony.kstarbound.util.valueOfOrNull
|
||||
import ru.dbotthepony.kstarbound.world.entities.AnchorNetworkState
|
||||
import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.NPCEntity
|
||||
import ru.dbotthepony.kstarbound.world.entities.api.LoungeableEntity
|
||||
|
||||
fun provideNPCBindings(self: NPCEntity, lua: LuaEnvironment) {
|
||||
val callbacks = lua.newTable()
|
||||
lua.globals["npc"] = callbacks
|
||||
|
||||
callbacks["toAbsolutePosition"] = luaFunction { pos: Table ->
|
||||
returnBuffer.setTo(from(self.toAbsolutePosition(toVector2d(pos))))
|
||||
}
|
||||
|
||||
callbacks["species"] = luaFunction { returnBuffer.setTo(self.variant.species.key.toByteString()) }
|
||||
callbacks["gender"] = luaFunction { returnBuffer.setTo(self.variant.humanoidIdentity.gender.jsonName.toByteString()) }
|
||||
callbacks["humanoidIdentity"] = luaFunction { returnBuffer.setTo(from(Starbound.gson.toJsonTree(self.variant.humanoidIdentity))) }
|
||||
callbacks["npcType"] = luaFunction { returnBuffer.setTo(self.variant.typeName.toByteString()) }
|
||||
callbacks["seed"] = luaFunction { returnBuffer.setTo(self.variant.seed) }
|
||||
callbacks["level"] = luaFunction { returnBuffer.setTo(self.variant.level) }
|
||||
callbacks["dropPools"] = luaFunction { returnBuffer.setTo(tableOf(*self.dropPools.map { it.key.left().toByteString() }.toTypedArray())) }
|
||||
|
||||
callbacks["setDropPools"] = luaFunction { dropPools: Table? ->
|
||||
self.dropPools.clear()
|
||||
|
||||
if (dropPools != null) {
|
||||
for ((_, pool) in dropPools) {
|
||||
self.dropPools.add(Registries.treasurePools.ref(pool.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lol why
|
||||
callbacks["energy"] = luaFunction { returnBuffer.setTo(self.energy) }
|
||||
callbacks["maxEnergy"] = luaFunction { returnBuffer.setTo(self.maxEnergy) }
|
||||
|
||||
callbacks["say"] = luaFunction { line: ByteString, tags: Table?, config: Any? ->
|
||||
val actualLine = if (tags != null) {
|
||||
SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() })
|
||||
} else {
|
||||
line.decode()
|
||||
}
|
||||
|
||||
val isNotEmpty = actualLine.isNotEmpty()
|
||||
|
||||
if (isNotEmpty)
|
||||
self.addChatMessage(actualLine, toJsonFromLua(config))
|
||||
|
||||
returnBuffer.setTo(isNotEmpty)
|
||||
}
|
||||
|
||||
callbacks["sayPortrait"] = luaFunction { line: ByteString, portrait: ByteString, tags: Table?, config: Any? ->
|
||||
val actualLine = if (tags != null) {
|
||||
SBPattern.of(line.decode()).resolveOrSkip({ tags[it]?.toString() })
|
||||
} else {
|
||||
line.decode()
|
||||
}
|
||||
|
||||
val isNotEmpty = actualLine.isNotEmpty()
|
||||
|
||||
if (isNotEmpty)
|
||||
self.addChatMessage(actualLine, toJsonFromLua(config), portrait.decode())
|
||||
|
||||
returnBuffer.setTo(isNotEmpty)
|
||||
}
|
||||
|
||||
callbacks["emote"] = luaFunction { emote: ByteString ->
|
||||
self.addEmote(emote.decode())
|
||||
}
|
||||
|
||||
callbacks["dance"] = luaFunction { dance: ByteString ->
|
||||
self.setDance(dance.decode())
|
||||
}
|
||||
|
||||
callbacks["setInteractive"] = luaFunction { isInteractive: Boolean ->
|
||||
self.isInteractive = isInteractive
|
||||
}
|
||||
|
||||
callbacks["setLounging"] = luaFunction { loungeable: Number, oAnchorIndex: Number? ->
|
||||
val anchorIndex = oAnchorIndex?.toInt() ?: 0
|
||||
val entity = self.world.entities[loungeable.toInt()] as? LoungeableEntity
|
||||
|
||||
if (entity == null || anchorIndex !in 0 until entity.sitPositions.size || entity.entitiesLoungingIn(anchorIndex).isNotEmpty()) {
|
||||
returnBuffer.setTo(false)
|
||||
} else {
|
||||
self.movement.anchorNetworkState = AnchorNetworkState(loungeable.toInt(), anchorIndex)
|
||||
returnBuffer.setTo(true)
|
||||
}
|
||||
}
|
||||
|
||||
callbacks["resetLounging"] = luaFunction {
|
||||
self.movement.anchorNetworkState = null
|
||||
}
|
||||
|
||||
callbacks["isLounging"] = luaFunction {
|
||||
returnBuffer.setTo(self.movement.anchorNetworkState != null)
|
||||
}
|
||||
|
||||
callbacks["loungingIn"] = luaFunction {
|
||||
returnBuffer.setTo(self.movement.anchorNetworkState?.entityID)
|
||||
}
|
||||
|
||||
callbacks["setOfferedQuests"] = luaFunction { values: Table? ->
|
||||
self.offeredQuests.clear()
|
||||
|
||||
if (values != null) {
|
||||
for ((_, quest) in values) {
|
||||
self.offeredQuests.add(Starbound.gson.fromJsonFast(toJsonFromLua(quest), QuestArcDescriptor::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callbacks["setTurnInQuests"] = luaFunction { values: Table? ->
|
||||
self.turnInQuests.clear()
|
||||
|
||||
if (values != null) {
|
||||
for ((_, value) in values) {
|
||||
self.turnInQuests.add(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callbacks["setItemSlot"] = luaFunction { slot: ByteString, descriptor: Any ->
|
||||
returnBuffer.setTo(self.setItem(slot.decode(), ItemDescriptor(descriptor)))
|
||||
}
|
||||
|
||||
callbacks["getItemSlot"] = luaFunction { slot: ByteString ->
|
||||
val decoded = slot.decode()
|
||||
val slotType = HumanoidActorEntity.ItemSlot.entries.valueOfOrNull(decoded.lowercase())
|
||||
|
||||
if (slotType == null) {
|
||||
if (decoded in self.variant.items) {
|
||||
returnBuffer.setTo(self.variant.items[decoded]!!.toTable(this))
|
||||
} else {
|
||||
returnBuffer.setTo()
|
||||
}
|
||||
} else {
|
||||
returnBuffer.setTo(self.getItem(slotType).toTable(this))
|
||||
}
|
||||
}
|
||||
|
||||
callbacks["disableWornArmor"] = luaFunction { disable: Boolean ->
|
||||
self.disableWornArmor = disable
|
||||
}
|
||||
|
||||
callbacks["beginPrimaryFire"] = luaFunction { self.beginPrimaryFire() }
|
||||
callbacks["beginAltFire"] = luaFunction { self.beginSecondaryFire() }
|
||||
callbacks["beginSecondaryFire"] = luaFunction { self.beginSecondaryFire() }
|
||||
callbacks["endPrimaryFire"] = luaFunction { self.endPrimaryFire() }
|
||||
callbacks["endAltFire"] = luaFunction { self.endSecondaryFire() }
|
||||
callbacks["endSecondaryFire"] = luaFunction { self.endSecondaryFire() }
|
||||
|
||||
callbacks["setShifting"] = luaFunction { value: Boolean ->
|
||||
self.isShifting = value
|
||||
}
|
||||
|
||||
callbacks["setDamageOnTouch"] = luaFunction { value: Boolean ->
|
||||
self.damageOnTouch = value
|
||||
}
|
||||
|
||||
callbacks["aimPosition"] = luaFunction {
|
||||
returnBuffer.setTo(from(self.aimPosition))
|
||||
}
|
||||
|
||||
callbacks["setAimPosition"] = luaFunction { pos: Table ->
|
||||
self.aimPosition = self.world.geometry.diff(toVector2d(pos), self.position)
|
||||
}
|
||||
|
||||
callbacks["setDeathParticleBurst"] = luaFunction { value: ByteString? ->
|
||||
self.deathParticleBurst = value?.decode()?.sbIntern()
|
||||
}
|
||||
|
||||
callbacks["setStatusText"] = luaFunction { value: ByteString? ->
|
||||
self.statusText = value?.decode()?.sbIntern()
|
||||
}
|
||||
|
||||
callbacks["setDisplayNametag"] = luaFunction { value: Boolean ->
|
||||
self.displayNametag = value
|
||||
}
|
||||
|
||||
callbacks["setPersistent"] = luaFunction { value: Boolean ->
|
||||
self.isPersistent = value
|
||||
}
|
||||
|
||||
callbacks["setKeepAlive"] = luaFunction { value: Boolean ->
|
||||
self.keepAlive = value
|
||||
}
|
||||
|
||||
callbacks["setAggressive"] = luaFunction { value: Boolean ->
|
||||
self.isAggressive = value
|
||||
}
|
||||
|
||||
callbacks["setDamageTeam"] = luaFunction { value: Table ->
|
||||
self.team.accept(Starbound.gson.fromJsonFast(toJsonFromLua(value), EntityDamageTeam::class.java))
|
||||
}
|
||||
|
||||
callbacks["setUniqueId"] = luaFunction { value: ByteString? ->
|
||||
self.uniqueID.accept(value?.decode()?.sbIntern())
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ import ru.dbotthepony.kstarbound.defs.actor.PersistentStatusEffect
|
||||
import ru.dbotthepony.kstarbound.fromJsonFast
|
||||
import ru.dbotthepony.kstarbound.lua.LuaEnvironment
|
||||
import ru.dbotthepony.kstarbound.lua.from
|
||||
import ru.dbotthepony.kstarbound.lua.get
|
||||
import ru.dbotthepony.kstarbound.lua.iterator
|
||||
import ru.dbotthepony.kstarbound.lua.luaFunction
|
||||
import ru.dbotthepony.kstarbound.lua.set
|
||||
@ -23,7 +22,6 @@ import ru.dbotthepony.kstarbound.lua.toByteString
|
||||
import ru.dbotthepony.kstarbound.lua.toJsonFromLua
|
||||
import ru.dbotthepony.kstarbound.util.sbIntern
|
||||
import ru.dbotthepony.kstarbound.world.entities.StatusController
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
private object PersistentStatusEffectToken : TypeToken<PersistentStatusEffect>()
|
||||
private object CPersistentStatusEffectToken : TypeToken<ArrayList<PersistentStatusEffect>>()
|
||||
@ -47,7 +45,7 @@ fun provideStatusControllerBindings(self: StatusController, lua: LuaEnvironment)
|
||||
}
|
||||
|
||||
callbacks["stat"] = luaFunction { name: ByteString ->
|
||||
returnBuffer.setTo(self.liveStats[name.decode()]?.effectiveModifiedValue ?: 0.0)
|
||||
returnBuffer.setTo(self.effectiveStats[name.decode()]?.effectiveModifiedValue ?: 0.0)
|
||||
}
|
||||
|
||||
callbacks["statPositive"] = luaFunction { name: ByteString ->
|
||||
|
@ -346,12 +346,12 @@ fun provideWorldEntitiesBindings(self: World<*, *>, callbacks: Table, lua: LuaEn
|
||||
|
||||
callbacks["entitySpecies"] = luaFunction { id: Number ->
|
||||
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
|
||||
returnBuffer.setTo(entity.species.toByteString())
|
||||
returnBuffer.setTo(entity.humanoidIdentity.species.key.left().toByteString())
|
||||
}
|
||||
|
||||
callbacks["entityGender"] = luaFunction { id: Number ->
|
||||
val entity = self.entities[id.toInt()] as? HumanoidActorEntity ?: return@luaFunction returnBuffer.setTo()
|
||||
returnBuffer.setTo(entity.gender.jsonName.toByteString())
|
||||
returnBuffer.setTo(entity.humanoidIdentity.gender.jsonName.toByteString())
|
||||
}
|
||||
|
||||
callbacks["entityName"] = luaFunction { id: Number ->
|
||||
|
@ -6,7 +6,6 @@ import com.google.gson.JsonPrimitive
|
||||
import org.classdump.luna.ByteString
|
||||
import org.classdump.luna.LuaRuntimeException
|
||||
import org.classdump.luna.Table
|
||||
import ru.dbotthepony.kommons.util.KOptional
|
||||
import ru.dbotthepony.kstarbound.Registries
|
||||
import ru.dbotthepony.kstarbound.Starbound
|
||||
import ru.dbotthepony.kstarbound.defs.DamageSource
|
||||
@ -51,7 +50,7 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) {
|
||||
lua.globals["object"] = table
|
||||
|
||||
table["name"] = luaFunction { returnBuffer.setTo(self.config.key.toByteString()) }
|
||||
table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) }
|
||||
table["direction"] = luaFunction { returnBuffer.setTo(self.direction.numericalValue) }
|
||||
table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) }
|
||||
table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive }
|
||||
table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().toByteString()) }
|
||||
|
@ -92,9 +92,13 @@ interface IPacket {
|
||||
}
|
||||
|
||||
interface IServerPacket : IPacket {
|
||||
fun play(connection: ServerConnection)
|
||||
suspend fun play(connection: ServerConnection)
|
||||
val handleImmediatelyOnServer: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
interface IClientPacket : IPacket {
|
||||
fun play(connection: ClientConnection)
|
||||
suspend fun play(connection: ClientConnection)
|
||||
val handleImmediatelyOnClient: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) :
|
||||
inGame()
|
||||
}
|
||||
|
||||
fun bind(channel: Channel) {
|
||||
open fun bind(channel: Channel) {
|
||||
scope = CoroutineScope(channel.eventLoop().asCoroutineDispatcher() + SupervisorJob() + CoroutineExceptionHandler { coroutineContext, throwable ->
|
||||
disconnect("Uncaught exception in one of connection' coroutines: $throwable")
|
||||
LOGGER.fatal("Uncaught exception in one of $this coroutines", throwable)
|
||||
|
@ -219,6 +219,10 @@ class PacketRegistry(val isLegacy: Boolean) {
|
||||
i++
|
||||
|
||||
try {
|
||||
// TODO: separate "immediate" and "sequential" packets, so immediate packets are decompressed/parsed right away,
|
||||
// and sequential packets are decompressed/parsed only when they need to be handled
|
||||
// This way, malicious clients won't be able to fill up server memory by repeatedly sending huge packets
|
||||
// which can take up long time to process
|
||||
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}; packet No. $i in stream)", err)
|
||||
@ -289,6 +293,13 @@ class PacketRegistry(val isLegacy: Boolean) {
|
||||
networkReadBuffer.size(index + msg.readableBytes())
|
||||
msg.readBytes(networkReadBuffer.elements(), index, msg.readableBytes())
|
||||
drainNetworkBuffer(ctx)
|
||||
|
||||
if (networkReadBuffer.size >= MAX_BUFFER_SIZE) {
|
||||
// if client sends billions of data, tell them to fuck off (the rude way)
|
||||
networkReadBuffer.clear()
|
||||
networkReadBuffer.trim()
|
||||
ctx.channel().close()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
msg.release()
|
||||
@ -383,6 +394,7 @@ class PacketRegistry(val isLegacy: Boolean) {
|
||||
companion object {
|
||||
const val LOG_PACKETS = false
|
||||
const val MAX_PACKET_SIZE = 64L * 1024L * 1024L // 64 MiB
|
||||
const val MAX_BUFFER_SIZE = 128L * 1024L * 1024L // 128 MiB
|
||||
// this includes both compressed and uncompressed
|
||||
// Original game allows 16 mebibyte packets
|
||||
// but it doesn't account for compression bomb (packets are fully uncompressed
|
||||
|
@ -76,11 +76,11 @@ class ClientContextUpdatePacket(
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.rpc.read(rpcEntries)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.rpc.read(rpcEntries)
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class DamageNotificationPacket(val source: Int, val notification: DamageNotifica
|
||||
client.isTracking(notification.position)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
pushRemoteDamageNotification(this@DamageNotificationPacket)
|
||||
|
||||
@ -37,7 +37,7 @@ class DamageNotificationPacket(val source: Int, val notification: DamageNotifica
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.pushRemoteDamageNotification(this@DamageNotificationPacket)
|
||||
}
|
||||
|
@ -19,13 +19,13 @@ class DamageRequestPacket(val inflictor: Int, val target: Int, val request: Dama
|
||||
val destinationConnection: Int
|
||||
get() = Connection.connectionForEntityID(target)
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueueAndSuspend {
|
||||
pushRemoteDamageRequest(this@DamageRequestPacket)
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.pushRemoteDamageRequest(this@DamageRequestPacket)
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArrayLis
|
||||
stream.writeSignedVarInt(entityID)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
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}!")
|
||||
connection.disconnect("Creating entity with ID $entityID outside of allowed range ${connection.entityIDRange}")
|
||||
@ -46,13 +46,13 @@ class EntityCreatePacket(val entityType: EntityType, val storeData: ByteArrayLis
|
||||
entity.isRemote = true
|
||||
entity.networkGroup.upstream.enableInterpolation(0.0)
|
||||
|
||||
connection.enqueue {
|
||||
connection.enqueueAndSuspend {
|
||||
entity.joinWorld(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v
|
||||
stream.writeBoolean(isDeath)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
if (entityID !in connection.entityIDRange) {
|
||||
LOGGER.error("Client $connection tried to remove entity with ID $entityID, but that's outside of allowed range ${connection.entityIDRange}!")
|
||||
connection.disconnect("Removing entity with ID $entityID outside of allowed range ${connection.entityIDRange}")
|
||||
@ -34,7 +34,7 @@ class EntityDestroyPacket(val entityID: Int, val finalNetState: ByteArrayList, v
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
@ -44,17 +44,18 @@ class EntityMessagePacket(val entity: Either<Int, String>, val message: String,
|
||||
stream.writeShort(sourceConnection)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
val tracker = connection.tracker
|
||||
|
||||
if (tracker == null) {
|
||||
connection.send(EntityMessageResponsePacket(Either.left("Not in world"), this@EntityMessagePacket.id))
|
||||
} else {
|
||||
// Don't suspend here, entity messages are generally not reliable
|
||||
tracker.handleEntityMessage(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
val world = world
|
||||
|
||||
|
@ -31,7 +31,8 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
|
||||
stream.writeUUID(id)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
// Don't suspend here, entity messages are generally not reliable
|
||||
connection.enqueue {
|
||||
val message = pendingEntityMessages.asMap().remove(id)
|
||||
|
||||
@ -45,7 +46,7 @@ class EntityMessageResponsePacket(val response: Either<String, JsonElement>, val
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
val message = world?.pendingEntityMessages?.asMap()?.remove(this@EntityMessageResponsePacket.id)
|
||||
|
||||
|
@ -29,8 +29,8 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<By
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueueAndSuspend {
|
||||
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}!")
|
||||
@ -45,7 +45,7 @@ class EntityUpdateSetPacket(val forConnection: Int, val deltas: Int2ObjectMap<By
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
@ -19,13 +19,13 @@ class HitRequestPacket(val inflictor: Int, val target: Int, val request: DamageD
|
||||
val destinationConnection: Int
|
||||
get() = Connection.connectionForEntityID(inflictor)
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueueAndSuspend {
|
||||
pushRemoteHitRequest(this@HitRequestPacket)
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.pushRemoteHitRequest(this@HitRequestPacket)
|
||||
}
|
||||
|
@ -8,11 +8,14 @@ import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
|
||||
object PongPacket : IClientPacket {
|
||||
override val handleImmediatelyOnClient: Boolean
|
||||
get() = true
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
if (isLegacy) stream.write(0)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@ -23,11 +26,14 @@ object PongPacket : IClientPacket {
|
||||
}
|
||||
|
||||
object PingPacket : IServerPacket {
|
||||
override val handleImmediatelyOnServer: Boolean
|
||||
get() = true
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
if (isLegacy) stream.write(0)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.send(PongPacket)
|
||||
}
|
||||
|
||||
|
@ -10,11 +10,14 @@ import java.io.DataOutputStream
|
||||
data class ProtocolRequestPacket(val version: Int) : IServerPacket {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readInt())
|
||||
|
||||
override val handleImmediatelyOnServer: Boolean
|
||||
get() = true
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeInt(version)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
if (version == Starbound.NATIVE_PROTOCOL_VERSION) {
|
||||
connection.channel.write(ProtocolResponsePacket(true))
|
||||
connection.channel.flush()
|
||||
|
@ -16,7 +16,7 @@ data class ProtocolResponsePacket(val allowed: Boolean) : IClientPacket {
|
||||
stream.writeBoolean(allowed)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
if (allowed) {
|
||||
if (connection.isLegacy) {
|
||||
connection.setupLegacy()
|
||||
|
@ -12,15 +12,18 @@ import java.io.DataOutputStream
|
||||
class StepUpdatePacket(val remoteStep: Long) : IServerPacket, IClientPacket {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readVarLong())
|
||||
|
||||
override val handleImmediatelyOnServer: Boolean
|
||||
get() = true
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeVarLong(remoteStep)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ class CelestialResponsePacket(val responses: Collection<Either<ChunkData, System
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ class CentralStructureUpdatePacket(val data: JsonElement) : IClientPacket {
|
||||
stream.writeJsonElement(data)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
val read = Starbound.gson.fromJson(data, WorldStructure::class.java)
|
||||
|
||||
connection.enqueue {
|
||||
|
@ -26,7 +26,7 @@ class ChatReceivePacket(val data: ChatMessage) : IClientPacket {
|
||||
stream.writeBinaryString(data.text)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class ConnectFailurePacket(val reason: String = "") : IClientPacket {
|
||||
stream.writeBinaryString(reason)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.disconnectNow()
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ class ConnectSuccessPacket(val connectionID: Int, val serverUUID: UUID, val cele
|
||||
celestialInformation.write(stream, isLegacy)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.connectionID = connectionID
|
||||
}
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ class EntityInteractResultPacket(val action: InteractAction, val id: UUID, val s
|
||||
stream.writeInt(source)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ class EnvironmentUpdatePacket(val sky: ByteArrayList, val weather: ByteArrayList
|
||||
stream.writeByteArray(weather.elements(), 0, weather.size)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class FindUniqueEntityResponsePacket(val name: String, val position: Vector2d?)
|
||||
stream.writeStruct2d(position, isLegacy)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.pendingUniqueEntityRequests?.asMap()?.remove(name)?.complete(position)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class GiveItemPacket(val descriptor: ItemDescriptor) : IClientPacket {
|
||||
descriptor.write(stream)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class HandshakeChallengePacket(val passwordSalt: ByteArray) : IClientPacket {
|
||||
stream.writeByteArray(passwordSalt)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class LegacyTileUpdatePacket(val position: Vector2i, val tile: LegacyNetworkCell
|
||||
tile.write(stream)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ class LegacyTileArrayUpdatePacket(val origin: Vector2i, val data: Object2DArray<
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ class PlayerWarpResultPacket(val success: Boolean, val target: WarpAction, val w
|
||||
stream.writeBoolean(warpActionInvalid)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class ServerDisconnectPacket(val reason: String = "") : IClientPacket {
|
||||
stream.writeBinaryString(reason)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.disconnectNow()
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class ServerInfoPacket(val players: Int, val maxPlayers: Int) : IClientPacket {
|
||||
stream.writeShort(maxPlayers)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ class SetPlayerStartPacket(val position: Vector2d, val respawnInWorld: Boolean)
|
||||
stream.writeBoolean(respawnInWorld)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.setPlayerSpawn(position, respawnInWorld)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class SystemObjectCreatePacket(val data: ByteArrayList) : IClientPacket {
|
||||
stream.writeByteArray(data.elements(), 0, data.size)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class SystemObjectDestroyPacket(val uuid: UUID) : IClientPacket {
|
||||
stream.writeUUID(uuid)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class SystemShipCreatePacket(val data: ByteArrayList) : IClientPacket {
|
||||
stream.writeByteArray(data.elements(), 0, data.size)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class SystemShipDestroyPacket(val uuid: UUID) : IClientPacket {
|
||||
stream.writeUUID(uuid)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class SystemWorldStartPacket(val location: Vector3i, val objects: Collection<Byt
|
||||
shipLocation.write(stream, isLegacy)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ class SystemWorldUpdatePacket(val objects: Map<UUID, ByteArrayList>, val ships:
|
||||
stream.writeMap(ships, { writeUUID(it) }, { writeByteArray(it.elements(), 0, it.size) })
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ class TileDamageUpdatePacket(val x: Int, val y: Int, val isBackground: Boolean,
|
||||
health.write(stream, isLegacy)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class TileModificationFailurePacket(val modifications: Collection<Pair<Vector2i,
|
||||
stream.writeCollection(modifications) { stream.writeStruct2i(it.first); it.second.write(stream, isLegacy) }
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class UniverseTimeUpdatePacket(val time: Double) : IClientPacket {
|
||||
stream.writeDouble(time)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ class UpdateDungeonBreathablePacket(val id: Int, val breathable: Boolean?) : ICl
|
||||
stream.writeNullable(breathable) { writeBoolean(it) }
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.setDungeonBreathable(this@UpdateDungeonBreathablePacket.id, breathable)
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class UpdateDungeonGravityPacket(val id: Int, val gravity: Vector2d?) : IClientP
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.setDungeonGravity(this@UpdateDungeonGravityPacket.id, gravity)
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class UpdateDungeonProtectionPacket(val id: Int, val isProtected: Boolean) : ICl
|
||||
stream.writeBoolean(isProtected)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.switchDungeonIDProtection(this@UpdateDungeonProtectionPacket.id, isProtected)
|
||||
}
|
||||
|
@ -19,13 +19,13 @@ class UpdateWorldPropertiesPacket(val update: JsonObject) : IClientPacket, IServ
|
||||
stream.writeJsonObject(update)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.enqueue {
|
||||
world?.updateProperties(update)
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
updateProperties(update)
|
||||
broadcast(this@UpdateWorldPropertiesPacket)
|
||||
|
@ -78,7 +78,7 @@ class WorldStartPacket(
|
||||
stream.writeBoolean(localInterpolationMode)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class WorldStopPacket(val reason: String = "") : IClientPacket {
|
||||
stream.writeBinaryString(reason)
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
connection.resetOccupiedEntityIDs()
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -29,7 +29,10 @@ class CelestialRequestPacket(val requests: Collection<Either<Vector2i, Vector3i>
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override val handleImmediatelyOnServer: Boolean
|
||||
get() = true
|
||||
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.pushCelestialRequests(requests)
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,15 @@ import java.io.DataOutputStream
|
||||
class ChatSendPacket(val text: String, val mode: ChatSendMode) : IServerPacket {
|
||||
constructor(stream: DataInputStream, isLegacy: Boolean) : this(stream.readBinaryString(), ChatSendMode.entries[stream.readUnsignedByte()])
|
||||
|
||||
override val handleImmediatelyOnServer: Boolean
|
||||
get() = true
|
||||
|
||||
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
|
||||
stream.writeBinaryString(text)
|
||||
stream.writeByte(mode.ordinal)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.server.chat.handle(connection, this)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ data class ClientConnectPacket(
|
||||
stream.writeBinaryString(account)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
if (connection.server.clientByUUID(playerUuid) != null) {
|
||||
connection.send(ConnectFailurePacket("Duplicate player UUID $playerUuid"))
|
||||
LOGGER.warn("Unable to accept player $playerName/$playerUuid because such UUID is already taken")
|
||||
|
@ -10,7 +10,7 @@ object ClientDisconnectRequestPacket : IServerPacket {
|
||||
if (isLegacy) stream.writeBoolean(false)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.disconnect("Disconnect by user.")
|
||||
}
|
||||
|
||||
|
@ -15,13 +15,13 @@ class ConnectWirePacket(val target: WireConnection, val source: WireConnection)
|
||||
source.write(stream)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueue
|
||||
val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueue
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueueAndSuspend {
|
||||
val target = entityIndex.tileEntityAt(target.entityLocation, WorldObject::class) ?: return@enqueueAndSuspend
|
||||
val source = entityIndex.tileEntityAt(source.entityLocation, WorldObject::class) ?: return@enqueueAndSuspend
|
||||
|
||||
val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue
|
||||
val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue
|
||||
val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueueAndSuspend
|
||||
val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueueAndSuspend
|
||||
|
||||
if (this@ConnectWirePacket.source in targetNode.connections && this@ConnectWirePacket.target in sourceNode.connections) {
|
||||
// disconnect
|
||||
|
@ -41,7 +41,7 @@ class DamageTileGroupPacket(val tiles: Collection<Vector2i>, val isBackground: B
|
||||
stream.writeInt(source)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.tracker?.damageTiles(tiles, isBackground, sourcePosition, damage, source)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ class DisconnectAllWiresPacket(val pos: Vector2i, val node: WireNode) : IServerP
|
||||
node.write(stream, isLegacy)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
connection.enqueue {
|
||||
val target = entityIndex.tileEntityAt(pos, WorldObject::class) ?: return@enqueue
|
||||
val node = if (node.isInput) target.inputNodes.getOrNull(node.index) else target.outputNodes.getOrNull(node.index)
|
||||
|
@ -26,7 +26,7 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer
|
||||
stream.writeUUID(id)
|
||||
}
|
||||
|
||||
override fun play(connection: ServerConnection) {
|
||||
override suspend fun play(connection: ServerConnection) {
|
||||
if (request.target >= 0) {
|
||||
connection.enqueue {
|
||||
connection.send(EntityInteractResultPacket((entities[request.target] as? InteractiveEntity)?.interact(request) ?: InteractAction.NONE, id, request.source))
|
||||
@ -41,7 +41,7 @@ class EntityInteractPacket(val request: InteractRequest, val id: UUID) : IServer
|
||||
}
|
||||
}
|
||||
|
||||
override fun play(connection: ClientConnection) {
|
||||
override suspend fun play(connection: ClientConnection) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user