More work towards implementing legacy protocol, new entity class tree

This commit is contained in:
DBotThePony 2024-03-20 20:18:41 +07:00
parent afc45aac92
commit 8149fcb48d
Signed by: DBot
GPG Key ID: DCC23B5715498507
47 changed files with 729 additions and 465 deletions

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
kotlinVersion=1.9.10
kotlinCoroutinesVersion=1.8.0
kommonsVersion=2.9.23
kommonsVersion=2.9.25
ffiVersion=2.2.13
lwjglVersion=3.3.0

View File

@ -14,14 +14,11 @@ import ru.dbotthepony.kstarbound.server.world.LegacyWorldStorage
import ru.dbotthepony.kstarbound.server.world.ServerUniverse
import ru.dbotthepony.kstarbound.server.world.ServerWorld
import ru.dbotthepony.kstarbound.world.WorldGeometry
import ru.dbotthepony.kstarbound.world.entities.ItemEntity
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.DataInputStream
import java.io.File
import java.net.InetSocketAddress
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.zip.Inflater
import java.util.zip.InflaterInputStream
@ -116,19 +113,6 @@ fun main() {
//client.world!!.parallax = Starbound.parallaxAccess["garden"]
val rand = Random()
for (i in 0 until 0) {
val item = ItemEntity(Registries.items.keys.values.random().value)
item.position = Vector2d(225.0 - i, 785.0)
item.joinWorld(world)
item.movement.velocity = Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0)
item.mailbox.scheduleAtFixedRate({ item.movement.velocity += Vector2d(rand.nextDouble() * 32.0 - 16.0, rand.nextDouble() * 32.0 - 16.0) }, 1000 + rand.nextLong(-100, 100), 1000 + rand.nextLong(-100, 100), TimeUnit.MILLISECONDS)
//item.movement.applyVelocity(Vector2d(rand.nextDouble() * 1000.0 - 500.0, rand.nextDouble() * 1000.0 - 500.0))
}
client.connectToLocalServer(server.channels.createLocalChannel())
//client.connectToRemoteServer(InetSocketAddress("127.0.0.1", 21025))
//client2.connectToLocalServer(server.channels.createLocalChannel(), UUID.randomUUID())

View File

@ -233,7 +233,7 @@ object Starbound : ISBFileLocator {
registerTypeAdapterFactory(ThingDescription.Factory(STRINGS))
registerTypeAdapterFactory(TerrainSelectorType.Companion)
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.NORMAL))
registerTypeAdapter(EnumAdapter(DamageType::class, default = DamageType.DAMAGE))
registerTypeAdapter(InventoryIcon.Companion)

View File

@ -19,7 +19,7 @@ class ForgetEntityPacket(val uuid: UUID) : IClientPacket {
val world = connection.client.world ?: return
world.mailbox.execute {
world.entities.firstOrNull { it.uuid == uuid }?.remove()
world.entities.firstOrNull { it.entityID == 0 }?.remove()
}
}
}

View File

@ -24,7 +24,7 @@ class SpawnWorldObjectPacket(val uuid: UUID, val data: JsonObject) : IClientPack
connection.client.mailbox.execute {
val world = connection.client.world ?: return@execute
val obj = WorldObject.fromJson(data)
obj.uuid = uuid
//obj.uuid = uuid
obj.joinWorld(world)
}
}

View File

@ -0,0 +1,74 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
import java.io.DataOutputStream
enum class TeamType(override val jsonName: String) : IStringSerializable {
NULL("null"),
// non-PvP-enabled players and player allied NPCs
FRIENDLY("friendly"),
// hostile and neutral NPCs and monsters
ENEMY("enemy"),
// PvP-enabled players
PVP("pvp"),
// cannot damage anything, can be damaged by Friendly/PVP/Assistant
PASSIVE("passive"),
// cannot damage or be damaged
GHOSTLY("ghostly"),
// cannot damage enemies, can be damaged by anything except enemy
ENVIRONMENT("environment"),
// damages anything except ghostly, damaged by anything except ghostly/passive
// used for self damage
INDISCRIMINATE("indiscriminate"),
// cannot damage friendlies and cannot be damaged by anything
ASSISTANT("assistant");
}
enum class HitType(override val jsonName: String) : IStringSerializable {
HIT("Hit"),
STRONG_HIT("StrongHit"),
WEAK_HIT("WeakHit"),
SHIELD_HIT("ShieldHit"),
KILL("Kill");
}
enum class DamageType(override val jsonName: String) : IStringSerializable {
NO_DAMAGE("NoDamage"),
DAMAGE("Damage"),
IGNORE_DEFENCE("IgnoresDef"),
KNOCKBACK("IgnoresDef"),
ENVIRONMENT("Environment"),
STATUS("Environment");
}
@JsonFactory
data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = 0) {
constructor(stream: DataInputStream, isLegacy: Boolean) : this(TeamType.entries[stream.readUnsignedByte()], stream.readUnsignedShort())
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeByte(type.ordinal)
stream.writeShort(team)
}
companion object {
val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write)
val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write)
}
}
@JsonFactory
data class TouchDamage(
val poly: ImmutableList<Vector2d> = ImmutableList.of(),
val teamType: TeamType = TeamType.ENVIRONMENT,
val damage: Double = 0.0,
val damageSourceKind: String = "",
val knockback: Double = 0.0,
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
)

View File

@ -1,23 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class DamageType(private vararg val aliases: String) : IStringSerializable {
NORMAL,
IGNORE_DEFENCE("IGNORESDEF", "IGNOREDEF"),
STATUS,
NO_DAMAGE("NODAMAGE");
override fun match(name: String): Boolean {
for (alias in aliases)
if (name == alias)
return true
return name == this.name
}
override fun write(out: JsonWriter) {
out.value(this.name)
}
}

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class EntityType(val jsonName: String) : IStringSerializable {
enum class EntityType(override val jsonName: String) : IStringSerializable {
PLANT("PlantEntity"),
OBJECT("ObjectEntity"),
VEHICLE("VehicleEntity"),
@ -14,12 +14,4 @@ enum class EntityType(val jsonName: String) : IStringSerializable {
MONSTER("MonsterEntity"),
NPC("NpcEntity"),
PLAYER("PlayerEntity");
override fun match(name: String): Boolean {
return name == jsonName
}
override fun write(out: JsonWriter) {
out.name(jsonName)
}
}

View File

@ -22,7 +22,7 @@ data class PerlinNoiseParameters(
require(scale >= 16) { "Too little perlin noise scale" }
}
enum class Type(val jsonName: String) : IStringSerializable {
enum class Type(override val jsonName: String) : IStringSerializable {
PERLIN("perlin"),
BILLOW("billow"),
RIDGED_MULTI("ridgedmulti");
@ -30,10 +30,6 @@ data class PerlinNoiseParameters(
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
}
companion object {

View File

@ -35,7 +35,7 @@ sealed class AbstractWarpTarget {
sealed class WarpAlias(val index: Int) : AbstractWarpTarget() {
override fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.write(3)
// because it is defined as enum class WarpAlias, which defaults to int32_t for some reason.
// because it is defined as enum class WarpAlias, without specifying uint8_t as type
stream.writeInt(index)
}

View File

@ -1,22 +0,0 @@
package ru.dbotthepony.kstarbound.defs
enum class TeamType {
NULL,
// non-PvP-enabled players and player allied NPCs
FRIENDLY,
// hostile and neutral NPCs and monsters
ENEMY,
// PvP-enabled players
PVP,
// cannot damage anything, can be damaged by Friendly/PVP/Assistant
PASSIVE,
// cannot damage or be damaged
GHOSTLY,
// cannot damage enemies, can be damaged by anything except enemy
ENVIRONMENT,
// damages anything except ghostly, damaged by anything except ghostly/passive
// used for self damage
INDISCRIMINATE,
// cannot damage friendlies and cannot be damaged by anything
ASSISTANT
}

View File

@ -1,16 +0,0 @@
package ru.dbotthepony.kstarbound.defs
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
@JsonFactory
data class TouchDamage(
val poly: ImmutableList<Vector2d> = ImmutableList.of(),
val teamType: TeamType = TeamType.ENVIRONMENT,
val damage: Double = 0.0,
val damageSourceKind: String = "",
val knockback: Double = 0.0,
val statusEffects: ImmutableSet<String> = ImmutableSet.of(),
)

View File

@ -0,0 +1,120 @@
package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
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.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readColor
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.writeColor
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import java.io.DataInputStream
import java.io.DataOutputStream
@JsonFactory
data class HumanoidData(
val name: String = "Humanoid",
val species: String = "human",
val gender: Gender = Gender.MALE,
val hairGroup: String = "hair",
// Must have :normal and :climb
val hairType: String = "male1",
val hairDirectives: String = "",
val bodyDirectives: String = "",
val emoteDirectives: String = bodyDirectives,
val facialHairGroup: String = "",
val facialHairType: String = "",
val facialHairDirectives: String = "",
val facialMaskGroup: String = "",
val facialMaskType: String = "",
val facialMaskDirectives: String = "",
val color: RGBAColor = RGBAColor.BLACK,
val personalityIdle: String = "",
val personalityArmIdle: String = "",
val personalityHeadOffset: Vector2d = Vector2d.ZERO,
val personalityArmOffset: Vector2d = Vector2d.ZERO,
val imagePath: String? = null,
) : IPersonality {
override val idle: String
get() = personalityIdle
override val armIdle: String
get() = personalityArmIdle
override val handOffset: Vector2d
get() = personalityHeadOffset
override val armOffset: Vector2d
get() = personalityArmOffset
fun write(stream: DataOutputStream, isLegacy: Boolean) {
stream.writeBinaryString(name)
stream.writeBinaryString(species)
stream.writeByte(gender.ordinal)
stream.writeBinaryString(hairGroup)
stream.writeBinaryString(hairType)
stream.writeBinaryString(hairDirectives)
stream.writeBinaryString(bodyDirectives)
stream.writeBinaryString(emoteDirectives)
stream.writeBinaryString(facialHairGroup)
stream.writeBinaryString(facialHairType)
stream.writeBinaryString(facialHairDirectives)
stream.writeBinaryString(facialMaskGroup)
stream.writeBinaryString(facialMaskType)
stream.writeBinaryString(facialMaskDirectives)
stream.writeBinaryString(personalityIdle)
stream.writeBinaryString(personalityArmIdle)
if (isLegacy) stream.writeStruct2f(personalityHeadOffset.toFloatVector()) else stream.writeStruct2d(personalityHeadOffset)
if (isLegacy) stream.writeStruct2f(personalityArmOffset.toFloatVector()) else stream.writeStruct2d(personalityArmOffset)
stream.writeColor(color)
stream.writeBoolean(imagePath != null)
if (imagePath != null)
stream.writeBinaryString(imagePath)
}
companion object {
val CODEC = nativeCodec(::read, HumanoidData::write)
val LEGACY_CODEC = legacyCodec(::read, HumanoidData::write)
fun read(stream: DataInputStream, isLegacy: Boolean): HumanoidData {
val name: String = stream.readInternedString()
val species: String = stream.readInternedString()
val gender: Gender = Gender.entries[stream.readUnsignedByte()]
val hairGroup = stream.readInternedString()
val hairType = stream.readInternedString()
val hairDirectives = stream.readInternedString()
val bodyDirectives = stream.readInternedString()
val emoteDirectives = stream.readInternedString()
val facialHairGroup = stream.readInternedString()
val facialHairType = stream.readInternedString()
val facialHairDirectives = stream.readInternedString()
val facialMaskGroup = stream.readInternedString()
val facialMaskType = stream.readInternedString()
val facialMaskDirectives = stream.readInternedString()
val color: RGBAColor = stream.readColor()
val personalityIdle: String = stream.readInternedString()
val personalityArmIdle: String = stream.readInternedString()
val personalityHeadOffset: Vector2d = if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d()
val personalityArmOffset: Vector2d = if (isLegacy) stream.readVector2f().toDoubleVector() else stream.readVector2d()
val imagePath: String? = if (stream.readBoolean()) stream.readInternedString() else null
return HumanoidData(
name, species, gender, hairGroup, hairType, hairDirectives, bodyDirectives,
emoteDirectives, facialHairGroup, facialHairType, facialHairDirectives,
facialMaskGroup, facialMaskType, facialMaskDirectives, color, personalityIdle,
personalityArmIdle, personalityHeadOffset, personalityArmOffset, imagePath)
}
}
}

View File

@ -0,0 +1,20 @@
package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
// 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 armOffset: Vector2d
}
@JsonFactory(asList = true)
data class Personality(
override val idle: String,
override val armIdle: String,
override val handOffset: Vector2d,
override val armOffset: Vector2d
) : IPersonality

View File

@ -0,0 +1,24 @@
package ru.dbotthepony.kstarbound.defs.actor
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class Gender(override val jsonName: String) : IStringSerializable {
MALE("Male"), FEMALE("Female");
}
enum class HumanoidEmote(override val jsonName: String) : IStringSerializable {
IDLE("Idle"),
BLABBERING("Blabbering"),
SHOUTING("Shouting"),
HAPPY("Happy"),
SAD("Sad"),
NEUTRAL("NEUTRAL"),
LAUGH("Laugh"),
ANNOYED("Annoyed"),
OH("Oh"),
OOOH("OOOH"),
BLINK("Blink"),
WINK("Wink"),
EAT("Eat"),
SLEEP("Sleep");
}

View File

@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.defs.item
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class ItemRarity(val canonical: String) : IStringSerializable {
enum class ItemRarity(override val jsonName: String) : IStringSerializable {
COMMON("Common"),
UNCOMMON("Uncommon"),
RARE("Rare"),
@ -11,10 +11,6 @@ enum class ItemRarity(val canonical: String) : IStringSerializable {
ESSENTIAL("Essential");
override fun match(name: String): Boolean {
return name == this.canonical || name.lowercase() == this.name.lowercase()
}
override fun write(out: JsonWriter) {
out.value(canonical)
return name == this.jsonName || name.lowercase() == this.name.lowercase()
}
}

View File

@ -47,19 +47,11 @@ enum class WarpPhase(val stupidassbitch: Int) {
SPEEDING_UP(1)
}
enum class SkyOrbiterType(val sname: String) : IStringSerializable {
enum class SkyOrbiterType(override val jsonName: String) : IStringSerializable {
SUN("sun"),
MOON("moon"),
HORIZON_CLOUD("horizoncloud"),
SPACE_DEBRIS("scapedebris");
override fun match(name: String): Boolean {
return name.lowercase() == sname
}
override fun write(out: JsonWriter) {
out.value(sname)
}
}
@JsonFactory

View File

@ -20,49 +20,25 @@ import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import kotlin.properties.Delegates
enum class BeamUpRule(val jsonName: String) : IStringSerializable {
NOWHERE("nowhere"),
SURFACE("surface"),
ANYWHERE("anywhere"),
ANYWHERE_WITH_WARNING("anywherewithwarning");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
enum class BeamUpRule(override val jsonName: String) : IStringSerializable {
NOWHERE("Nowhere"),
SURFACE("Surface"),
ANYWHERE("Anywhere"),
ANYWHERE_WITH_WARNING("AnywhereWithWarning");
}
enum class WorldEdgeForceRegion(val sname: String) : IStringSerializable {
NONE("none"),
TOP("top"),
BOTTOM("bottom"),
TOP_AND_BOTTOM("topandbottom");
override fun match(name: String): Boolean {
return name.lowercase() == sname
}
override fun write(out: JsonWriter) {
out.value(sname)
}
enum class WorldEdgeForceRegion(override val jsonName: String) : IStringSerializable {
NONE("None"),
TOP("Top"),
BOTTOM("Bottom"),
TOP_AND_BOTTOM("TopAndBottom");
}
enum class VisitableWorldParametersType(val jsonName: String, val token: TypeToken<out VisitableWorldParameters>) : IStringSerializable {
enum class VisitableWorldParametersType(override val jsonName: String, val token: TypeToken<out VisitableWorldParameters>) : IStringSerializable {
TERRESTRIAL("TerrestrialWorldParameters", TypeToken.get(TerrestrialWorldParameters::class.java)),
ASTEROIDS("AsteroidsWorldParameters", TypeToken.get(AsteroidsWorldParameters::class.java)),
FLOATING_DUNGEON("FloatingDungeonWorldParameters", TypeToken.get(FloatingDungeonWorldParameters::class.java));
override fun match(name: String): Boolean {
return name == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
companion object : TypeAdapterFactory {
val ADAPTER = DispatchingAdapter("type", { type }, { token }, entries)

View File

@ -9,7 +9,7 @@ import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Direction1D
@JsonFactory
data class WorldStructure(
@ -33,7 +33,7 @@ data class WorldStructure(
data class Obj(
val position: Vector2i,
val name: String,
val direction: Direction,
val direction: Direction1D,
val parameters: JsonElement,
val residual: Boolean = false,
)

View File

@ -36,7 +36,7 @@ import java.util.random.RandomGenerator
import java.util.stream.IntStream
enum class BiomePlacementDistributionType(
val jsonName: String,
override val jsonName: String,
val def: TypeToken<out BiomePlaceablesDefinition.DistributionData>,
val data: TypeToken<out BiomePlaceables.DistributionData>,
) : IStringSerializable {
@ -47,10 +47,6 @@ enum class BiomePlacementDistributionType(
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
companion object {
val DEFINITION_ADAPTER = DispatchingAdapter("type", { type }, { def }, entries)
val DATA_ADAPTER = DispatchingAdapter("type", { type }, { data }, entries)
@ -58,7 +54,7 @@ enum class BiomePlacementDistributionType(
}
enum class BiomePlacementItemType(
val jsonName: String,
override val jsonName: String,
val def: TypeToken<out BiomePlaceablesDefinition.DistributionItemData>,
val data: TypeToken<out BiomePlaceables.Item>,
) : IStringSerializable {
@ -74,10 +70,6 @@ enum class BiomePlacementItemType(
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
}
companion object {
val DEFINITION_ADAPTER = DispatchingAdapter("type", { type }, { def }, entries)
val DATA_ADAPTER = DispatchingAdapter("type", { type }, { data }, entries)

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.io
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.io.readDouble
@ -14,6 +15,7 @@ import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.ChunkPos
import java.io.DataInput
import java.io.DataOutput
@ -54,3 +56,5 @@ fun OutputStream.writeColor(color: RGBAColor) {
fun InputStream.readColor(): RGBAColor {
return RGBAColor(readFloat(), readFloat(), readFloat(), readFloat())
}
fun InputStream.readInternedString(): String = Starbound.STRINGS.intern(readBinaryString())

View File

@ -22,8 +22,11 @@ import kotlin.reflect.KClass
import kotlin.reflect.full.isSuperclassOf
interface IStringSerializable {
fun match(name: String): Boolean
fun write(out: JsonWriter)
val jsonName: String
fun match(name: String): Boolean {
return name == jsonName
}
}
inline fun <reified T : Enum<T>> EnumAdapter(values: Stream<T> = Arrays.stream(T::class.java.enumConstants), default: T? = null): EnumAdapter<T> {
@ -85,7 +88,7 @@ class EnumAdapter<T : Enum<T>>(private val enum: KClass<T>, values: Stream<T> =
if (value == null) {
out.nullValue()
} else if (value is IStringSerializable) {
value.write(out)
out.value(value.jsonName)
} else {
out.value(value.name)
}

View File

@ -19,7 +19,7 @@ object RGBAColorTypeAdapter : TypeAdapter<RGBAColor>() {
out.value(value.redInt)
out.value(value.greenInt)
out.value(value.blueInt)
if (value.alphaInt != 255) out.value(value.alphaInt)
out.value(value.alphaInt)
out.endArray()
}

View File

@ -14,6 +14,7 @@ import ru.dbotthepony.kommons.io.readKOptional
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeKOptional
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonElement
import java.io.DataInputStream
@ -48,7 +49,7 @@ class JsonRPC {
companion object {
fun native(stream: DataInputStream): Entry {
return Entry(Command.entries[stream.read()], stream.readInt(), stream.readKOptional { readBinaryString() }, stream.readKOptional { readJsonElement() })
return Entry(Command.entries[stream.read()], stream.readInt(), stream.readKOptional { Starbound.STRINGS.intern(readBinaryString()) }, stream.readKOptional { readJsonElement() })
}
fun legacy(stream: DataInputStream): Entry {

View File

@ -28,6 +28,7 @@ object PingPacket : IServerPacket {
}
override fun play(connection: ServerConnection) {
// immediately respond to ping packets
connection.sendAndFlush(PongPacket)
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.network.syncher
import ru.dbotthepony.kommons.io.UnsignedVarLongCodec
import java.io.DataInputStream
import java.util.function.Consumer

View File

@ -1,8 +1,11 @@
package ru.dbotthepony.kstarbound.network.syncher
import com.google.gson.TypeAdapter
import ru.dbotthepony.kommons.io.BinaryStringCodec
import ru.dbotthepony.kommons.io.BooleanValueCodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.UnsignedVarIntCodec
import ru.dbotthepony.kommons.io.UnsignedVarLongCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.io.VarLongValueCodec
import ru.dbotthepony.kommons.io.Vector2dCodec
@ -16,22 +19,49 @@ import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.InputStream
import java.io.OutputStream
fun <TYPE> BasicNetworkedElement(value: TYPE, codec: StreamCodec<TYPE>): BasicNetworkedElement<TYPE, TYPE> {
return BasicNetworkedElement(value, codec, codec, { it }, { it })
}
val UnsignedVarLongCodec = StreamCodec.Impl(DataInputStream::readVarLong, DataOutputStream::writeVarLong)
val UnsignedVarIntCodec = StreamCodec.Impl(DataInputStream::readVarInt, DataOutputStream::writeVarInt)
val ByteArrayCodec = StreamCodec.Impl(DataInputStream::readByteArray, DataOutputStream::writeByteArray)
// networking size_t...
// god help us all
val SizeTCodec = StreamCodec.Impl({ stream -> stream.readVarLong().let { if (it == 0L) -1L else it - 1L } }, { stream, value -> if (value in 0L..<Long.MAX_VALUE) stream.writeVarLong(value + 1L) else stream.writeVarLong(0L) })
fun InputStream.readPointer(): Long {
val read = readVarLong()
if (read == 0L) {
return -1L
} else {
return read - 1L
}
}
fun OutputStream.writePointer(value: Int) {
if (value < 0L) {
writeVarLong(0L)
} else {
writeVarLong(value + 1L)
}
}
fun OutputStream.writePointer(value: Long) {
if (value in 0L..<Long.MAX_VALUE) {
writeVarLong(value + 1L)
} else {
writeVarLong(0L)
}
}
fun networkedFloat(value: Double = 0.0) = FloatingNetworkedElement.float(value)
fun networkedDouble(value: Double = 0.0) = FloatingNetworkedElement.double(value)
fun networkedFixedPoint(base: Double, value: Double = 0.0) = FloatingNetworkedElement.fixed(base, value)
@ -45,6 +75,14 @@ fun networkedVec2f(value: Vector2d = Vector2d.ZERO) = BasicNetworkedElement(valu
fun networkedBytes(value: ByteArray = ByteArray(0)) = BasicNetworkedElement(value, ByteArrayCodec)
fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
fun <T> networkedData(value: T, codec: StreamCodec<T>) = BasicNetworkedElement(value, codec)
fun <T> networkedData(value: T, codec: StreamCodec<T>, legacyCodec: StreamCodec<T>) = BasicNetworkedElement(value, codec, legacyCodec, { it }, { it })
fun networkedEventCounter() = EventCounterElement()
fun networkedString(value: String = "") = BasicNetworkedElement(value, BinaryStringCodec)
fun networkedPoly(value: Poly) = networkedData(value, Poly.CODEC, Poly.LEGACY_CODEC)
inline fun <reified T> networkedJson(value: T, adapter: TypeAdapter<T> = Starbound.gson.getAdapter(T::class.java), legacyIsArray: Boolean = true): BasicNetworkedElement<T, *> {
if (legacyIsArray) {
return BasicNetworkedElement(value, JsonCodec(adapter), JsonCodec(adapter, true), { it }, { it })
@ -53,12 +91,22 @@ inline fun <reified T> networkedJson(value: T, adapter: TypeAdapter<T> = Starbou
}
}
/**
* properly networks an enum on native protocol;
* networks a signed variable length integer on legacy protocol.
* this is way too dumb beyond my comprehension
*/
fun <T> nativeCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec<T> {
return StreamCodec.Impl({ reader(it, false) }, { a, b -> writer(b, a, false) })
}
fun <T> legacyCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec<T> {
return StreamCodec.Impl({ reader(it, true) }, { a, b -> writer(b, a, true) })
}
// networks a signed variable length integer on legacy protocol
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
val codec = StreamCodec.Enum(value::class.java)
return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal.shl(1) }, { codec.values[it.ushr(1)] })
}
// networks enum as string on legacy protocol
fun <E : Enum<E>> networkedEnumExtraStupid(value: E): BasicNetworkedElement<E, String> {
val codec = StreamCodec.Enum(value::class.java)
return BasicNetworkedElement(value, codec, BinaryStringCodec, { if (it is IStringSerializable) it.jsonName else it.name }, { s -> codec.values.firstOrNull { if (it is IStringSerializable) it.match(s) else it.name == s } ?: throw NoSuchElementException(s) })
}

View File

@ -15,7 +15,7 @@ import java.util.function.DoubleSupplier
import kotlin.math.roundToLong
// works solely with doubles, but networks as either float, double or fixed point
class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, val legacyOps: Ops = ops, val interpolator: Interpolator = Interpolator.NearestMiddle) : NetworkedElement(), ListenableDelegate<Double>, DoubleSupplier {
class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, val legacyOps: Ops = ops, var interpolator: Interpolator = Interpolator.NearestMiddle) : NetworkedElement(), ListenableDelegate<Double>, DoubleSupplier {
interface Ops {
fun write(data: DataOutputStream, value: Double)
fun read(data: DataInputStream): Double
@ -251,7 +251,7 @@ class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, va
}
fun fixed(base: Double, value: Double = 0.0): FloatingNetworkedElement {
return FloatingNetworkedElement(value, FixedPointOps(base))
return FloatingNetworkedElement(value, DoubleOps, FixedPointOps(base))
}
}
}

View File

@ -42,7 +42,7 @@ class GroupElement() : NetworkedElement() {
elements.forEach { it.first.tickInterpolation(delta) }
}
fun add(element: NetworkedElement, propagateInterpolation: Boolean = true): GroupElement {
fun <E : NetworkedElement> add(element: E, propagateInterpolation: Boolean = true): E {
require(elements.none { it.first == element }) { "Already has element $element in $this" }
elements.add(element to propagateInterpolation)
@ -62,7 +62,7 @@ class GroupElement() : NetworkedElement() {
})
this.version = this.version.coerceAtLeast(element.version)
return this
return element
}
override fun readInitial(data: DataInputStream, isLegacy: Boolean) {

View File

@ -121,12 +121,12 @@ class ServerConnection(val server: StarboundServer, type: ConnectionType) : Conn
private inner class ChunkListener(val pos: ChunkPos) : IChunkListener {
override fun onEntityAdded(entity: AbstractEntity) {
if (entity is WorldObject && !isLegacy)
send(SpawnWorldObjectPacket(entity.uuid, entity.serialize()))
//if (entity is WorldObject && !isLegacy)
// send(SpawnWorldObjectPacket(entity.uuid, entity.serialize()))
}
override fun onEntityRemoved(entity: AbstractEntity) {
send(ForgetEntityPacket(entity.uuid))
//send(ForgetEntityPacket(entity.uuid))
}
override fun onCellChanges(x: Int, y: Int, cell: ImmutableCell) {

View File

@ -1,21 +1,36 @@
package ru.dbotthepony.kstarbound.world
import com.google.gson.stream.JsonWriter
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
enum class Direction(val normal: Vector2d, val jsonName: String) : IStringSerializable {
UP(Vector2d.POSITIVE_Y, "up"),
RIGHT(Vector2d.POSITIVE_X, "right"),
DOWN(Vector2d.NEGATIVE_Y, "down"),
enum class Direction(val normal: Vector2d, override val jsonName: String) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left"),
RIGHT(Vector2d.POSITIVE_X, "right"),
UP(Vector2d.POSITIVE_Y, "up"),
DOWN(Vector2d.NEGATIVE_Y, "down"),
NONE(Vector2d.ZERO, "any");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
override fun write(out: JsonWriter) {
out.value(jsonName)
companion object {
val CODEC = StreamCodec.Enum(Direction::class.java)
}
}
enum class Direction1D(val normal: Vector2d, override val jsonName: String) : IStringSerializable {
LEFT(Vector2d.NEGATIVE_X, "left"),
RIGHT(Vector2d.POSITIVE_X, "right");
override fun match(name: String): Boolean {
return name.lowercase() == jsonName
}
companion object {
val CODEC = StreamCodec.Enum(Direction1D::class.java)
}
}

View File

@ -31,6 +31,7 @@ import ru.dbotthepony.kstarbound.world.physics.getBlockPlatforms
import ru.dbotthepony.kstarbound.world.physics.getBlocksMarchingSquares
import java.io.Closeable
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Predicate
import java.util.random.RandomGenerator
@ -43,6 +44,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
val sky = Sky()
val geometry: WorldGeometry = template.geometry
val nextEntityID = AtomicInteger()
override fun getCellDirect(x: Int, y: Int): AbstractCell {
if (!geometry.x.inBoundsCell(x) || !geometry.y.inBoundsCell(y)) return AbstractCell.NULL
return getCell(x, y)

View File

@ -1,6 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.util.MailboxExecutorService
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.defs.JsonDriven
@ -8,6 +9,7 @@ import ru.dbotthepony.kstarbound.world.Chunk
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.World
import java.io.DataOutputStream
import java.util.UUID
import kotlin.concurrent.withLock
@ -41,8 +43,16 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
}
}
var uuid: UUID = UUID.randomUUID()
abstract val chunkPos: ChunkPos
abstract val position: Vector2d
var entityID: Int = 0
set(value) {
if (field != 0)
throw IllegalStateException("Already has Entity ID set (to $field)")
field = value
}
var mailbox = MailboxExecutorService()
private set
@ -55,6 +65,15 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
val isSpawned: Boolean
get() = innerWorld != null
/**
* If set, then the entity will be discoverable by its unique id and will be
* indexed in the stored world. Unique ids must be different across all
* entities in a single world.
*/
var uniqueID: String? = null
protected set
/**
* Whenever this entity should be removed when chunk containing it is being unloaded
*
@ -67,6 +86,8 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
protected open fun onJoinWorld(world: World<*, *>) { }
protected open fun onRemove(world: World<*, *>) { }
abstract fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean)
/**
* MUST be called by [World] itself
*/
@ -74,6 +95,9 @@ abstract class AbstractEntity(path: String) : JsonDriven(path) {
if (innerWorld != null)
throw IllegalStateException("Already spawned (in world $innerWorld)")
if (entityID == 0)
entityID = world.nextEntityID.incrementAndGet()
world.ensureSameThread()
if (mailbox.isShutdown)

View File

@ -0,0 +1,8 @@
package ru.dbotthepony.kstarbound.world.entities
/**
* Monsters, NPCs, Players
*/
abstract class ActorEntity(path: String) : DynamicEntity(path) {
final override val movement: ActorMovementController = ActorMovementController()
}

View File

@ -1,85 +1,93 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.JumpProfile
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.util.GameTimer
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Direction1D
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import kotlin.math.PI
import kotlin.math.absoluteValue
import kotlin.math.sign
abstract class AbstractActorMovementController : AbstractMovementController() {
abstract var controlRun: Boolean
abstract var controlCrouch: Boolean
abstract var controlDown: Boolean
abstract var lastControlDown: Boolean
abstract var controlFly: Vector2d?
abstract var controlFace: Direction?
class ActorMovementController : MovementController() {
var controlRun: Boolean = false
var controlCrouch: Boolean = false
var controlDown: Boolean = false
var lastControlDown: Boolean = false
var controlFly: Vector2d? = null
var controlFace: Direction1D? = null
abstract var isRunning: Boolean
protected set
abstract var isWalking: Boolean
protected set
abstract var isCrouching: Boolean
protected set
abstract var isFlying: Boolean
protected set
abstract var isFalling: Boolean
protected set
abstract var isJumping: Boolean
protected set
var isWalking: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
var isRunning: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
abstract var canJump: Boolean
abstract var controlJump: Boolean
abstract var controlJumpAnyway: Boolean
var movingDirection: Direction1D by networkGroup.upstream.add(networkedEnum(Direction1D.RIGHT))
private set
var facingDirection: Direction1D by networkGroup.upstream.add(networkedEnum(Direction1D.RIGHT))
private set
abstract var lastControlJump: Boolean
protected set
abstract var lastControlCrouch: Boolean
protected set
var isCrouching: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
abstract var isGroundMovement: Boolean
protected set
abstract var isLiquidMovement: Boolean
protected set
var isFlying: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
var isFalling: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
abstract var controlRotationRate: Double
abstract var controlAcceleration: Vector2d
abstract var controlForce: Vector2d
var canJump: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
var isJumping: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
abstract var fallThroughSustain: Int
protected set
var isGroundMovement: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
var isLiquidMovement: Boolean by networkGroup.upstream.add(networkedBoolean())
private set
var controlJump: Boolean = false
var controlJumpAnyway: Boolean = false
var lastControlJump: Boolean = false
private set
var lastControlCrouch: Boolean = false
private set
var controlRotationRate: Double = 0.0
var controlAcceleration: Vector2d = Vector2d.ZERO
var controlForce: Vector2d = Vector2d.ZERO
var fallThroughSustain: Int = 0
private set
// Target horizontal velocity for walking / running
abstract var targetHorizontalAmbulatingVelocity: Double
protected set
var targetHorizontalAmbulatingVelocity: Double = 0.0
private set
abstract var controlPathMove: Pair<Vector2d, Boolean>?
abstract var pathMoveResult: Pair<Vector2d, Boolean>?
var controlPathMove: Pair<Vector2d, Boolean>? = null
var pathMoveResult: Pair<Vector2d, Boolean>? = null
abstract var controlMove: Direction?
var controlMove: Direction? = null
abstract var actorMovementParameters: ActorMovementParameters
abstract var movementModifiers: ActorMovementModifiers
var actorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
abstract var controlActorMovementParameters: ActorMovementParameters
abstract var controlMovementModifiers: ActorMovementModifiers
var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
var controlMovementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
abstract val approachVelocities: MutableList<ApproachVelocityCommand>
abstract val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand>
val approachVelocities = ArrayList<ApproachVelocityCommand>()
val approachVelocityAngles = ArrayList<ApproachVelocityAngleCommand>()
abstract var movingDirection: Direction?
abstract var facingDirection: Direction?
// this is set internally on each move step
final override var movementParameters: MovementParameters = MovementParameters.EMPTY
abstract var anchorEntity: DynamicEntity?
var anchorEntity: DynamicEntity? = null
var pathController: PathController? = null
var groundMovementSustainTimer: GameTimer = GameTimer(0.0)
@ -177,7 +185,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
return params
}
open fun clearControls() {
fun clearControls() {
controlRotationRate = 0.0
controlAcceleration = Vector2d.ZERO
controlForce = Vector2d.ZERO
@ -268,7 +276,7 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
isLiquidMovement = liquidPercentage >= (actorMovementParameters.minimumLiquidPercentage ?: 0.0)
val liquidImpedance = liquidPercentage * (actorMovementParameters.liquidImpedance ?: 0.0)
var updatedMovingDirection: Direction? = null
var updatedMovingDirection: Direction1D? = null
val isRunning = controlRun && !movementModifiers.runningSuppressed
if (controlFly != null) {
@ -283,9 +291,9 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
approachVelocity(flyVelocity * movementModifiers.speedModifier, movementParameters.airForce ?: 0.0)
if (flyVelocity.x > 0.0)
updatedMovingDirection = Direction.RIGHT
updatedMovingDirection = Direction1D.RIGHT
else if (flyVelocity.x < 0.0)
updatedMovingDirection = Direction.LEFT
updatedMovingDirection = Direction1D.LEFT
groundMovementSustainTimer = GameTimer(0.0)
} else {
@ -365,10 +373,10 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
}
if (controlMove == Direction.LEFT) {
updatedMovingDirection = Direction.LEFT
updatedMovingDirection = Direction1D.LEFT
targetHorizontalAmbulatingVelocity = -1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
} else if (controlMove == Direction.RIGHT) {
updatedMovingDirection = Direction.RIGHT
updatedMovingDirection = Direction1D.RIGHT
targetHorizontalAmbulatingVelocity = 1.0 * (if (isRunning) movementParameters.runSpeed ?: 0.0 else movementParameters.walkSpeed ?: 0.0) * movementModifiers.speedModifier
}
@ -391,15 +399,16 @@ abstract class AbstractActorMovementController : AbstractMovementController() {
}
}
movingDirection = updatedMovingDirection
if (updatedMovingDirection != null)
movingDirection = updatedMovingDirection
if (!movementModifiers.facingSuppressed) {
if (controlFace != null)
facingDirection = controlFace
facingDirection = controlFace!!
else if (updatedMovingDirection != null)
facingDirection = updatedMovingDirection
else if (controlPathMove != null && pathController?.controlFace != null)
facingDirection = pathController?.controlFace
facingDirection = pathController?.controlFace!!
}
isGroundMovement = !groundMovementSustainTimer.hasFinished

View File

@ -8,6 +8,7 @@ import ru.dbotthepony.kstarbound.client.render.LayeredRenderer
import ru.dbotthepony.kstarbound.client.render.RenderLayer
import ru.dbotthepony.kstarbound.world.ChunkPos
import ru.dbotthepony.kstarbound.world.World
import java.util.function.Consumer
/**
* Entities with dynamics (Player, Drops, Projectiles, NPCs, etc)
@ -15,35 +16,33 @@ import ru.dbotthepony.kstarbound.world.World
abstract class DynamicEntity(path: String) : AbstractEntity(path) {
private var forceChunkRepos = false
var position = Vector2d()
override var position
get() = movement.position
set(value) {
val old = field
if (isSpawned) {
field = world.geometry.wrap(value)
val oldChunkPos = world.geometry.chunkFromCell(old)
val newChunkPos = world.geometry.chunkFromCell(field)
chunkPos = newChunkPos
if (oldChunkPos != newChunkPos || forceChunkRepos) {
chunk = world.chunkMap[newChunkPos]
forceChunkRepos = false
}
} else {
field = value
}
movement.position = value
}
abstract val movement: AbstractMovementController
abstract val movement: MovementController
final override var chunkPos: ChunkPos = ChunkPos.ZERO
private set
override fun onJoinWorld(world: World<*, *>) {
world.dynamicEntities.add(this)
movement.world = world
forceChunkRepos = true
position = position
/*movement.positionListeners.addListener(Consumer { field ->
val oldChunkPos = world.geometry.chunkFromCell(old)
val newChunkPos = world.geometry.chunkFromCell(field)
chunkPos = newChunkPos
if (oldChunkPos != newChunkPos || forceChunkRepos) {
chunk = world.chunkMap[newChunkPos]
forceChunkRepos = false
}
})*/
}
override fun onRemove(world: World<*, *>) {
@ -65,7 +64,5 @@ abstract class DynamicEntity(path: String) : AbstractEntity(path) {
companion object {
val BLOCK_COLLISION_COLOR = RGBAColor(65, 179, 217)
const val SEPARATION_STEPS = 3
const val SEPARATION_TOLERANCE = 0.001
}
}

View File

@ -1,67 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.defs.player.ActorMovementModifiers
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.World
class EntityActorMovementController(val entity: DynamicEntity) : AbstractActorMovementController() {
override val world: World<*, *> by entity::world
override var position: Vector2d by entity::position
override var actorMovementParameters: ActorMovementParameters = GlobalDefaults.actorMovementParameters
override var lastControlDown: Boolean = false
override var fallThroughSustain: Int = 0
override var targetHorizontalAmbulatingVelocity: Double = 0.0
override var controlRun: Boolean = false
override var controlCrouch: Boolean = false
override var controlDown: Boolean = false
override var controlFly: Vector2d? = null
override var isFlying: Boolean = false
override var isFalling: Boolean = false
override var canJump: Boolean = true
override var controlJump: Boolean = false
override var isGroundMovement: Boolean = false
override var isLiquidMovement: Boolean = false
override var controlPathMove: Pair<Vector2d, Boolean>? = null
override var pathMoveResult: Pair<Vector2d, Boolean>? = null
override var controlMove: Direction? = null
override var movementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
override var controlActorMovementParameters: ActorMovementParameters = ActorMovementParameters.EMPTY
override var controlMovementModifiers: ActorMovementModifiers = ActorMovementModifiers.EMPTY
override var isOnGround: Boolean = false
override var isColliding: Boolean = false
override var isCollisionStuck: Boolean = false
override var isCollidingWithNull: Boolean = false
override var stickingDirection: Double? = null
override var surfaceSlope: Vector2d = Vector2d.ZERO
override var surfaceVelocity: Vector2d = Vector2d.ZERO
override var collisionCorrection: Vector2d = Vector2d.ZERO
override var liquidPercentage: Double = 0.0
override var appliedForceRegion: Boolean = false
override var isZeroGravity: Boolean = false
override var controlRotationRate: Double = 0.0
override var controlAcceleration: Vector2d = Vector2d.ZERO
override var controlForce: Vector2d = Vector2d.ZERO
override var rotation: Double = 0.0
override var lastControlCrouch: Boolean = false
override var controlFace: Direction? = null
override var isRunning: Boolean = false
override var isWalking: Boolean = false
override var isCrouching: Boolean = false
override var isJumping: Boolean = false
override var lastControlJump: Boolean = false
override var controlJumpAnyway: Boolean = false
override val approachVelocities: MutableList<ApproachVelocityCommand> = ArrayList()
override val approachVelocityAngles: MutableList<ApproachVelocityAngleCommand> = ArrayList()
override var movingDirection: Direction? = null
override var facingDirection: Direction? = null
override var anchorEntity: DynamicEntity? = null
}

View File

@ -1,25 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.GlobalDefaults
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.world.World
class EntityMovementController(val entity: DynamicEntity) : AbstractMovementController() {
override val world: World<*, *> by entity::world
override var position: Vector2d by entity::position
override var movementParameters: MovementParameters = GlobalDefaults.movementParameters
override var isOnGround: Boolean = false
override var isColliding: Boolean = false
override var isCollisionStuck: Boolean = false
override var isCollidingWithNull: Boolean = false
override var stickingDirection: Double? = null
override var surfaceSlope: Vector2d = Vector2d.ZERO
override var surfaceVelocity: Vector2d = Vector2d.ZERO
override var collisionCorrection: Vector2d = Vector2d.ZERO
override var liquidPercentage: Double = 0.0
override var appliedForceRegion: Boolean = false
override var isZeroGravity: Boolean = false
override var rotation: Double = 0.0
}

View File

@ -0,0 +1,10 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.vector.Vector2d
/**
* Players and NPCs
*/
abstract class HumanoidActorEntity(path: String) : ActorEntity(path) {
abstract val aimPosition: Vector2d
}

View File

@ -1,21 +0,0 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.item.api.IItemDefinition
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.Poly
class ItemEntity(val def: IItemDefinition) : DynamicEntity("/") {
override val movement = EntityMovementController(this)
override fun defs(): Collection<JsonObject> {
return emptyList()
}
init {
movement.movementParameters = movement.movementParameters.copy(collisionPoly = Either.left(Poly(AABB.rectangle(Vector2d.ZERO, 0.75, 0.75))))
}
}

View File

@ -1,11 +1,28 @@
package ru.dbotthepony.kstarbound.world.entities
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.DoubleValueCodec
import ru.dbotthepony.kommons.io.FloatValueCodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.times
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.MovementParameters
import ru.dbotthepony.kstarbound.network.syncher.GroupElement
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedPoly
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.physics.CollisionPoly
import ru.dbotthepony.kstarbound.world.physics.CollisionType
@ -16,15 +33,14 @@ import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.sin
import kotlin.properties.Delegates
abstract class AbstractMovementController() {
abstract val world: World<*, *>
open class MovementController() {
var world: World<*, *> by Delegates.notNull()
val localHitboxes: Stream<Poly>
get() { return (movementParameters.collisionPoly?.map({ Stream.of(it) }, { it.stream() }) ?: return Stream.of()).map { it.rotate(rotation) + position } }
abstract var position: Vector2d
open fun shouldCollideWithType(type: CollisionType): Boolean {
return type !== CollisionType.NONE
}
@ -33,43 +49,75 @@ abstract class AbstractMovementController() {
return shouldCollideWithType(body.type)
}
private val legacyPoly = networkedPoly(Poly.EMPTY)
protected val networkGroup = MasterElement(GroupElement(legacyPoly))
var mass by networkGroup.upstream.add(networkedFloat())
private var xPosition by networkGroup.upstream.add(networkedFixedPoint(0.0125))
private var yPosition by networkGroup.upstream.add(networkedFixedPoint(0.0125))
private var xVelocity by networkGroup.upstream.add(networkedFixedPoint(0.00625))
private var yVelocity by networkGroup.upstream.add(networkedFixedPoint(0.00625))
var rotation by networkGroup.upstream.add(networkedFixedPoint(0.01))
var isColliding: Boolean by networkGroup.upstream.add(networkedBoolean())
protected set
var isCollisionStuck: Boolean by networkGroup.upstream.add(networkedBoolean())
protected set
var isCollidingWithNull: Boolean by networkGroup.upstream.add(networkedBoolean())
protected set
private val stickingDirectionField = networkGroup.upstream.add(networkedData(KOptional(), DoubleValueCodec.koptional(), FloatValueCodec.map({ toDouble() }, { toFloat() }).koptional()))
var isOnGround: Boolean by networkGroup.upstream.add(networkedBoolean())
protected set
var isZeroGravity: Boolean by networkGroup.upstream.add(networkedBoolean())
protected set
private val surfaceMovingCollisionField = networkGroup.upstream.add(networkedData(KOptional(), StreamCodec.Impl(::MovingCollisionID, { a, b -> b.write(a) }).koptional()))
private val relativeXSurfaceVelocity = networkGroup.upstream.add(networkedFloat())
private val relativeYSurfaceVelocity = networkGroup.upstream.add(networkedFloat())
var position: Vector2d
get() = Vector2d(xPosition, yPosition)
set(value) {
xPosition = value.x
yPosition = value.y
}
val positionListeners = Listenable.Impl<Vector2d>()
// Movement variables
abstract var isOnGround: Boolean
var stickingDirection: Double?
get() = stickingDirectionField.get().orNull()
protected set(value) {
stickingDirectionField.accept(KOptional.ofNullable(value))
}
var surfaceSlope: Vector2d = Vector2d.ZERO
protected set
abstract var isColliding: Boolean
var surfaceVelocity: Vector2d = Vector2d.ZERO
protected set
abstract var isCollisionStuck: Boolean
var collisionCorrection: Vector2d = Vector2d.ZERO
protected set
abstract var isCollidingWithNull: Boolean
protected set
abstract var stickingDirection: Double?
protected set
abstract var surfaceSlope: Vector2d
protected set
abstract var surfaceVelocity: Vector2d
protected set
abstract var collisionCorrection: Vector2d
protected set
abstract var liquidPercentage: Double
var liquidPercentage: Double = 0.0
protected set
abstract var appliedForceRegion: Boolean
var appliedForceRegion: Boolean = false
protected set
abstract var isZeroGravity: Boolean
protected set
var movementParameters: MovementParameters = MovementParameters.EMPTY
abstract var movementParameters: MovementParameters
var gravityMultiplier = 1.0
var isGravityDisabled = false
abstract var rotation: Double
open var gravityMultiplier = 1.0
open var isGravityDisabled = false
val mass: Double
get() = movementParameters.mass ?: 1.0
var velocity = Vector2d.ZERO
var velocity: Vector2d
get() = Vector2d(xVelocity, yVelocity)
set(value) {
xVelocity = value.x
yVelocity = value.y
}
fun determineGravity(): Vector2d {
if (isZeroGravity || isGravityDisabled)

View File

@ -0,0 +1,21 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.io.readInt
import ru.dbotthepony.kommons.io.writeInt
import ru.dbotthepony.kstarbound.network.syncher.readPointer
import ru.dbotthepony.kstarbound.network.syncher.writePointer
import java.io.InputStream
import java.io.OutputStream
data class MovingCollisionID(val entity: Int = 0, val collisionIndex: Int = -1) {
constructor(stream: InputStream) : this(stream.readInt(), stream.readPointer().toInt())
fun write(stream: OutputStream) {
stream.writeInt(entity)
stream.writePointer(collisionIndex)
}
companion object {
val ZERO = MovingCollisionID(0, -1)
}
}

View File

@ -1,7 +1,7 @@
package ru.dbotthepony.kstarbound.world.entities
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.world.Direction
import ru.dbotthepony.kstarbound.world.Direction1D
import ru.dbotthepony.kstarbound.world.World
class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) {
@ -9,7 +9,7 @@ class PathController(val world: World<*, *>, var edgeTimer: Double = 0.0) {
private set
var endPosition: Vector2d? = null
private set
var controlFace: Direction? = null
var controlFace: Direction1D? = null
private set
fun reset() {

View File

@ -1,12 +1,62 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.ActorMovementParameters
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.defs.EntityDamageTeam
import ru.dbotthepony.kstarbound.defs.actor.HumanoidData
import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.network.syncher.GroupElement
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedEnumExtraStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter
import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint
import ru.dbotthepony.kstarbound.network.syncher.networkedString
import java.io.DataOutputStream
import java.util.UUID
import kotlin.properties.Delegates
class PlayerEntity() : DynamicEntity("/") {
override val movement = EntityActorMovementController(this)
class PlayerEntity() : HumanoidActorEntity("/") {
enum class State(override val jsonName: String) : IStringSerializable {
IDLE("Idle"),
WALK("Walk"),
RUN("Run"),
JUMP("Jump"),
FALL("Fall"),
SWIM("Swim"),
SWIM_IDLE("SwimIdle"),
TELEPORT_IN("TeleportIn"),
TELEPORT_OUT("TeleportOut"),
CROUCH("Crouch"),
LOUNGE("Lounge");
}
override fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean) {
TODO("Not yet implemented")
}
val networkGroup = MasterElement(GroupElement())
var state by networkGroup.upstream.add(networkedEnum(State.IDLE))
var shifting by networkGroup.upstream.add(networkedBoolean())
private var xAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125))
private var yAimPosition by networkGroup.upstream.add(networkedFixedPoint(0.003125).also { it.interpolator = Interpolator.Linear })
var humanoidData by networkGroup.upstream.add(networkedData(HumanoidData(), HumanoidData.CODEC, HumanoidData.LEGACY_CODEC))
var teamState by networkGroup.upstream.add(networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC))
val landed = networkGroup.upstream.add(networkedEventCounter())
var chatMessage by networkGroup.upstream.add(networkedString())
val newChatMessage = networkGroup.upstream.add(networkedEventCounter())
var emote by networkGroup.upstream.add(networkedEnumExtraStupid(HumanoidEmote.IDLE))
override val aimPosition: Vector2d
get() = Vector2d(xAimPosition, yAimPosition)
override val isApplicableForUnloading: Boolean
get() = false
@ -15,36 +65,6 @@ class PlayerEntity() : DynamicEntity("/") {
return emptyList()
}
init {
movement.actorMovementParameters = movement.actorMovementParameters.merge(
Starbound.gson.fromJson("""
{
"standingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, 0.65], [0.35, 1.22], [-0.35, 1.22], [-0.75, 0.65] ],
"crouchingPoly" : [ [-0.75, -2.0], [-0.35, -2.5], [0.35, -2.5], [0.75, -2.0], [0.75, -1], [0.35, -0.5], [-0.35, -0.5], [-0.75, -1] ],
"mass" : 1.6,
// should keep the player from teleporting through walls
"maximumCorrection" : 3,
"maxMovementPerStep" : 0.4,
"liquidFriction" : 13.0,
"normalGroundFriction" : 35.0,
"groundForce" : 250.0,
"airForce" : 50.0,
"liquidForce" : 80.0
}
""".trimIndent(), ActorMovementParameters::class.java)
).merge(Starbound.gson.fromJson("""
{
"flySpeed" : 0,
"airFriction" : 0.5,
"airJumpProfile" : {
"jumpSpeed" : 23.0,
"jumpInitialPercentage" : 0.75,
"jumpHoldTime" : 0.2
}
}
""".trimIndent(), ActorMovementParameters::class.java))
}
var uuid: UUID by Delegates.notNull()
private set
}

View File

@ -1,5 +1,6 @@
package ru.dbotthepony.kstarbound.world.entities
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.world.ChunkPos
@ -11,7 +12,7 @@ import ru.dbotthepony.kstarbound.world.World
abstract class TileEntity(path: String) : AbstractEntity(path) {
private var forceChunkRepos = false
var position = Vector2i()
var tilePosition = Vector2i()
set(value) {
val old = field
@ -32,13 +33,16 @@ abstract class TileEntity(path: String) : AbstractEntity(path) {
}
}
override val position: Vector2d
get() = tilePosition.toDoubleVector()
final override var chunkPos: ChunkPos = ChunkPos.ZERO
private set
override fun onJoinWorld(world: World<*, *>) {
world.tileEntities.add(this)
forceChunkRepos = true
position = position
tilePosition = tilePosition
}
override fun onRemove(world: World<*, *>) {

View File

@ -27,6 +27,7 @@ import ru.dbotthepony.kstarbound.world.LightCalculator
import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf
import ru.dbotthepony.kstarbound.world.World
import ru.dbotthepony.kstarbound.world.api.TileColor
import java.io.DataOutputStream
import kotlin.properties.Delegates
open class WorldObject(
@ -47,10 +48,14 @@ open class WorldObject(
}
}
override fun writeToNetwork(stream: DataOutputStream, isLegacy: Boolean) {
TODO("Not yet implemented")
}
fun serialize(): JsonObject {
val into = JsonObject()
into["name"] = prototype.key
into["tilePosition"] = vectors.toJsonTree(position)
into["tilePosition"] = vectors.toJsonTree(tilePosition)
into["direction"] = directions.toJsonTree(direction)
into["orientationIndex"] = orientationIndex
into["interactive"] = interactive
@ -170,7 +175,7 @@ open class WorldObject(
color *= sample
}
lightCalculator.addPointLight(position.x - xOffset, position.y - yOffset, color)
lightCalculator.addPointLight(tilePosition.x - xOffset, tilePosition.y - yOffset, color)
}
}
@ -194,7 +199,7 @@ open class WorldObject(
val prototype = Registries.worldObjects[content["name"]?.asString ?: throw IllegalArgumentException("Missing object name")] ?: throw IllegalArgumentException("No such object defined for '${content["name"]}'")
val pos = content.get("tilePosition", vectors) { throw IllegalArgumentException("No tilePosition was present in saved data") }
val result = WorldObject(prototype)
result.position = pos
result.tilePosition = pos
result.deserialize(content)
return result
}

View File

@ -18,7 +18,16 @@ import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.client.StarboundClient
import ru.dbotthepony.kstarbound.client.gl.vertex.GeometryType
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kstarbound.json.listAdapter
import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sin
@ -42,7 +51,8 @@ private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Poly.Edge
newPoints.add(p1 - normal * 0.001)
newPoints.add(p0 - normal * 0.001)
return calculateEdges(newPoints.build())
val (f) = calculateEdges(newPoints.build())
return f to ImmutableList.copyOf(points)
} else {
val edges = ImmutableList.Builder<Poly.Edge>()
@ -77,10 +87,20 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
constructor(points: List<Vector2d>) : this(calculateEdges(points))
constructor(aabb: AABB) : this(listOf(aabb.bottomLeft, aabb.topLeft, aabb.topRight, aabb.bottomRight))
val aabb = AABB(
Vector2d(vertices.minOf { it.x }, vertices.minOf { it.y }),
Vector2d(vertices.maxOf { it.x }, vertices.maxOf { it.y }),
)
val aabb: AABB
val isEmpty: Boolean
get() = vertices.isEmpty()
init {
if (vertices.isEmpty()) {
aabb = AABB.ZERO
} else {
aabb = AABB(
Vector2d(vertices.minOf { it.x }, vertices.minOf { it.y }),
Vector2d(vertices.maxOf { it.x }, vertices.maxOf { it.y }),
)
}
}
data class Edge(val p0: Vector2d, val p1: Vector2d, val normal: Vector2d) {
operator fun plus(other: IStruct2d): Edge {
@ -109,6 +129,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
operator fun plus(value: Penetration): Poly {
if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
@ -119,6 +140,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
operator fun plus(value: IStruct2d): Poly {
if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
@ -129,6 +151,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
operator fun minus(value: IStruct2d): Poly {
if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
@ -139,6 +162,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
operator fun times(value: IStruct2d): Poly {
if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
@ -149,6 +173,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
operator fun times(value: Double): Poly {
if (isEmpty) return this
val vertices = ImmutableList.Builder<Vector2d>()
val edges = ImmutableList.Builder<Edge>()
@ -159,7 +184,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
fun rotate(radians: Double): Poly {
if (radians == 0.0)
if (radians == 0.0 || isEmpty)
return this
val sin = sin(radians)
@ -181,6 +206,8 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
// min / max
fun project(normal: Vector2d): IStruct2d {
check(!isEmpty) { "Poly is empty" }
var min = vertices.first().dot(normal)
var max = min
@ -198,7 +225,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
* @param axis separate ONLY along specified axis, that said, if axis is positive Y and we have collision, and closest separation axis is to up right, we instead separate only up until we no longer collide.
*/
fun intersect(other: Poly, axis: Vector2d? = null, strictAxis: Boolean = false): Penetration? {
if (!aabb.intersectWeak(other.aabb))
if (isEmpty || !aabb.intersectWeak(other.aabb))
return null
val normals = ObjectOpenHashSet<Vector2d>()
@ -284,6 +311,8 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
fun render(client: StarboundClient = StarboundClient.current(), color: RGBAColor = RGBAColor.LIGHT_GREEN) {
if (isEmpty) return
val program = client.programs.position
val lines = program.builder.builder
program.use()
@ -307,7 +336,7 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
}
override fun toString(): String {
return "Poly[aabb = $aabb; edges = $edges]"
return "Poly[aabb = $aabb; edges = $edges; vertices = $vertices]"
}
override fun equals(other: Any?): Boolean {
@ -318,9 +347,30 @@ class Poly private constructor(val edges: ImmutableList<Edge>, val vertices: Imm
return vertices.hashCode()
}
fun write(stream: DataOutputStream, isLegacy: Boolean) {
if (isLegacy) {
stream.writeCollection(vertices) { writeStruct2f(it.toFloatVector()) }
} else {
stream.writeCollection(vertices) { writeStruct2d(it) }
}
}
companion object : TypeAdapterFactory {
val CODEC = StreamCodec.Impl({ read(it, false) }, { a, b -> b.write(a, false) })
val LEGACY_CODEC = StreamCodec.Impl({ read(it, true) }, { a, b -> b.write(a, true) })
val EMPTY = Poly(ImmutableList.of(), ImmutableList.of())
private val identity = Matrix3f.identity()
fun read(stream: DataInputStream, isLegacy: Boolean): Poly {
if (isLegacy) {
return Poly(stream.readCollection { readVector2f().toDoubleVector() })
} else {
return Poly(stream.readCollection { readVector2d() })
}
}
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (type.rawType === Poly::class.java) {
return object : TypeAdapter<Poly>() {