diff --git a/gradle.properties b/gradle.properties index ec67d4ba..cc2195f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m kotlinVersion=1.9.10 kotlinCoroutinesVersion=1.8.0 -kommonsVersion=2.16.0 +kommonsVersion=2.16.1 ffiVersion=2.2.13 lwjglVersion=3.3.0 diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt index 76f97d13..0fec1891 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Registries.kt @@ -178,6 +178,7 @@ object Registries { tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name))) tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name))) tasks.addAll(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name))) + tasks.addAll(loadRegistry(projectiles, patchTree, fileTree["projectile"] ?: listOf(), key(ProjectileDefinition::projectileName))) tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree)) tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 04af3437..72b44574 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -401,6 +401,8 @@ object Starbound : BlockableEventLoop("Universe Thread"), Scheduler, ISBFileLoca registerTypeAdapterFactory(SystemWorldLocation.ADAPTER) + registerTypeAdapterFactory(PhysicsForceRegion.ADAPTER) + // register companion first, so it has lesser priority than dispatching adapter registerTypeAdapterFactory(VisitableWorldParametersType.Companion) registerTypeAdapterFactory(VisitableWorldParametersType.ADAPTER) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt index f18f7bff..79aff28e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/Damage.kt @@ -129,6 +129,7 @@ data class EntityDamageTeam(val type: TeamType = TeamType.NULL, val team: Int = } val NULL = EntityDamageTeam() + val FRIENDLY = EntityDamageTeam(TeamType.FRIENDLY) val PASSIVE = EntityDamageTeam(TeamType.PASSIVE) val CODEC = nativeCodec(::EntityDamageTeam, EntityDamageTeam::write) val LEGACY_CODEC = legacyCodec(::EntityDamageTeam, EntityDamageTeam::write) @@ -256,6 +257,16 @@ data class DamageSource( return damageArea.map({ it.intersect(other) != null }, { other.intersect(it) != null }) } + fun intersect(geometry: WorldGeometry, other: Poly): Boolean { + return damageArea.map({ geometry.polyIntersectsPoly(other, it) }, { geometry.lineIntersectsPoly(it, other) }) + } + + fun intersect(geometry: WorldGeometry, list: List): Boolean { + return list.any { other -> + damageArea.map({ geometry.polyIntersectsPoly(other, it) }, { geometry.lineIntersectsPoly(it, other) }) + } + } + operator fun plus(offset: Vector2d): DamageSource { return copy(damageArea = damageArea.flatMap({ it + offset }, { it + offset })) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt index fdc6220f..f24a83e2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/EntityType.kt @@ -2,12 +2,14 @@ package ru.dbotthepony.kstarbound.defs import com.google.gson.JsonElement import com.google.gson.JsonObject +import ru.dbotthepony.kommons.io.readBinaryString import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable 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.ProjectileEntity import ru.dbotthepony.kstarbound.world.entities.player.PlayerEntity import ru.dbotthepony.kstarbound.world.entities.tile.PlantEntity import ru.dbotthepony.kstarbound.world.entities.tile.PlantPieceEntity @@ -70,11 +72,11 @@ enum class EntityType(override val jsonName: String, val storeName: String, val PROJECTILE("projectile", "ProjectileEntity", true, true) { override fun fromNetwork(stream: DataInputStream, isLegacy: Boolean): AbstractEntity { - TODO("PROJECTILE") + return ProjectileEntity(Registries.projectiles.getOrThrow(stream.readBinaryString()), stream, isLegacy) } override fun fromStorage(data: JsonObject): AbstractEntity { - TODO("PROJECTILE") + throw UnsupportedOperationException("NYI: Projectiles are not persistent") } }, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsCategoryFilter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsCategoryFilter.kt new file mode 100644 index 00000000..1d755afc --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsCategoryFilter.kt @@ -0,0 +1,84 @@ +package ru.dbotthepony.kstarbound.defs + +import com.google.common.collect.ImmutableSet +import com.google.gson.Gson +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 ru.dbotthepony.kommons.io.readCollection +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeCollection +import ru.dbotthepony.kstarbound.io.readInternedString +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import java.io.DataInputStream +import java.io.DataOutputStream +import java.util.function.Predicate + +// enum class Type int32_t +// type -> isBlacklist +@JsonAdapter(PhysicsCategoryFilter.Adapter::class) +class PhysicsCategoryFilter(val isBlacklist: Boolean = false, val categories: ImmutableSet = ImmutableSet.of()) : Predicate { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readInt() > 0 else stream.readBoolean(), ImmutableSet.copyOf(stream.readCollection { readInternedString() })) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) + stream.writeInt(if (isBlacklist) 1 else 0) + else + stream.writeBoolean(isBlacklist) + + stream.writeCollection(categories) { writeBinaryString(it) } + } + + fun test(categories: Collection): Boolean { + if (isBlacklist) { + return categories.none { it in this.categories } + } else { + return categories.any { it in this.categories } + } + } + + override fun test(t: String): Boolean { + if (isBlacklist) { + return t !in this.categories + } else { + return t in this.categories + } + } + + @JsonFactory + data class JsonData( + val categoryWhitelist: ImmutableSet? = null, + val categoryBlacklist: ImmutableSet? = null, + ) + + class Adapter(gson: Gson) : TypeAdapter() { + private val data = gson.getAdapter(JsonData::class.java) + + override fun write(out: JsonWriter, value: PhysicsCategoryFilter) { + data.write(out, JsonData( + if (value.isBlacklist) null else value.categories, + if (!value.isBlacklist) null else value.categories, + )) + } + + override fun read(`in`: JsonReader): PhysicsCategoryFilter { + val read = data.read(`in`) + + if (read.categoryBlacklist != null && read.categoryWhitelist != null) { + throw JsonSyntaxException("Both categoryBlacklist and categoryWhitelist are specified") + } else if (read.categoryBlacklist != null) { + return PhysicsCategoryFilter(true, read.categoryBlacklist) + } else if (read.categoryWhitelist != null) { + return PhysicsCategoryFilter(true, read.categoryWhitelist) + } else { + return NEVER + } + } + } + + companion object { + val NEVER = PhysicsCategoryFilter() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsForceRegion.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsForceRegion.kt index 8d25ee77..b13d98f5 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsForceRegion.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsForceRegion.kt @@ -1,68 +1,57 @@ package ru.dbotthepony.kstarbound.defs -import com.google.common.collect.ImmutableSet -import ru.dbotthepony.kommons.io.readCollection -import ru.dbotthepony.kommons.io.writeBinaryString -import ru.dbotthepony.kommons.io.writeCollection -import ru.dbotthepony.kstarbound.math.vector.Vector2d +import com.google.gson.reflect.TypeToken import ru.dbotthepony.kstarbound.io.readDouble -import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.io.readNullableDouble import ru.dbotthepony.kstarbound.io.readVector2d import ru.dbotthepony.kstarbound.io.writeDouble import ru.dbotthepony.kstarbound.io.writeNullableDouble import ru.dbotthepony.kstarbound.io.writeStruct2d +import ru.dbotthepony.kstarbound.json.builder.DispatchingAdapter +import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.json.builder.JsonFactory import ru.dbotthepony.kstarbound.math.Line2d +import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.network.syncher.legacyCodec import ru.dbotthepony.kstarbound.network.syncher.nativeCodec import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.DataInputStream import java.io.DataOutputStream -import java.util.function.Predicate sealed class PhysicsForceRegion { + // ephemeral property from json + abstract val enabled: Boolean + abstract val filter: PhysicsCategoryFilter abstract fun write(stream: DataOutputStream, isLegacy: Boolean) - // enum class Type int32_t - // type -> isBlacklist - class Filter(val isBlacklist: Boolean, val categories: Set) : Predicate { - constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readInt() > 0 else stream.readBoolean(), ImmutableSet.copyOf(stream.readCollection { readInternedString() })) + abstract val type: Type - fun write(stream: DataOutputStream, isLegacy: Boolean) { - if (isLegacy) - stream.writeInt(if (isBlacklist) 1 else 0) - else - stream.writeBoolean(isBlacklist) - - stream.writeCollection(categories) { writeBinaryString(it) } - } - - fun test(categories: Collection): Boolean { - if (isBlacklist) { - return categories.any { it in this.categories } - } else { - return categories.any { it in this.categories } - } - } - - override fun test(t: String): Boolean { - if (isBlacklist) { - return t !in this.categories - } else { - return t in this.categories - } - } + enum class Type(override val jsonName: String, val token: TypeToken) : IStringSerializable { + DIRECTIONAL("DirectionalForceRegion", TypeToken.get(Directional::class.java)), + RADIAL("RadialForceRegion", TypeToken.get(Radial::class.java)), + GRADIENT("GradientForceRegion", TypeToken.get(Gradient::class.java)) } - data class Directional(val region: Poly, val xTargetVelocity: Double?, val yTargetVelocity: Double?, val controlForce: Double, val filter: Filter) : PhysicsForceRegion() { + @JsonFactory + data class Directional( + val region: Poly, + val xTargetVelocity: Double?, + val yTargetVelocity: Double?, + val controlForce: Double, + override val filter: PhysicsCategoryFilter, + override val enabled: Boolean = true + ) : PhysicsForceRegion() { constructor(stream: DataInputStream, isLegacy: Boolean) : this( Poly.read(stream, isLegacy), stream.readNullableDouble(isLegacy), stream.readNullableDouble(isLegacy), stream.readDouble(isLegacy), - Filter(stream, isLegacy) + PhysicsCategoryFilter(stream, isLegacy) ) + override val type: Type + get() = Type.DIRECTIONAL + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(0) region.write(stream, isLegacy) @@ -73,16 +62,28 @@ sealed class PhysicsForceRegion { } } - data class Radial(val center: Vector2d, val outerRadius: Double, val innerRadius: Double, val targetRadialVelocity: Double, val controlForce: Double, val filter: Filter) : PhysicsForceRegion() { + @JsonFactory + data class Radial( + val center: Vector2d = Vector2d.ZERO, + val outerRadius: Double, + val innerRadius: Double, + val targetRadialVelocity: Double, + val controlForce: Double, + override val filter: PhysicsCategoryFilter, + override val enabled: Boolean = true + ) : PhysicsForceRegion() { constructor(stream: DataInputStream, isLegacy: Boolean) : this( stream.readVector2d(isLegacy), stream.readDouble(isLegacy), stream.readDouble(isLegacy), stream.readDouble(isLegacy), stream.readDouble(isLegacy), - Filter(stream, isLegacy) + PhysicsCategoryFilter(stream, isLegacy) ) + override val type: Type + get() = Type.RADIAL + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(1) stream.writeStruct2d(center, isLegacy) @@ -94,15 +95,26 @@ sealed class PhysicsForceRegion { } } - data class Gradient(val region: Poly, val gradient: Line2d, val baseTargetVelocity: Double, val baseControlForce: Double, val filter: Filter) : PhysicsForceRegion() { + @JsonFactory + data class Gradient( + val region: Poly, + val gradient: Line2d, + val baseTargetVelocity: Double, + val baseControlForce: Double, + override val filter: PhysicsCategoryFilter, + override val enabled: Boolean = true + ) : PhysicsForceRegion() { constructor(stream: DataInputStream, isLegacy: Boolean) : this( Poly.read(stream, isLegacy), Line2d(stream, isLegacy), stream.readDouble(isLegacy), stream.readDouble(isLegacy), - Filter(stream, isLegacy) + PhysicsCategoryFilter(stream, isLegacy) ) + override val type: Type + get() = Type.GRADIENT + override fun write(stream: DataOutputStream, isLegacy: Boolean) { stream.writeByte(2) region.write(stream, isLegacy) @@ -117,6 +129,8 @@ sealed class PhysicsForceRegion { val CODEC = nativeCodec(::read, PhysicsForceRegion::write) val LEGACY_CODEC = legacyCodec(::read, PhysicsForceRegion::write) + val ADAPTER = DispatchingAdapter("type", { type }, { token }, Type.entries) + fun read(stream: DataInputStream, isLegacy: Boolean): PhysicsForceRegion { return when (val type = stream.readUnsignedByte()) { 0 -> Directional(stream, isLegacy) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsMovingCollision.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsMovingCollision.kt new file mode 100644 index 00000000..b300ed26 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PhysicsMovingCollision.kt @@ -0,0 +1,19 @@ +package ru.dbotthepony.kstarbound.defs + +import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.builder.JsonFlat +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.world.physics.CollisionType +import ru.dbotthepony.kstarbound.world.physics.Poly + +@JsonFactory +data class PhysicsMovingCollision( + val position: Vector2d = Vector2d.ZERO, + val collision: Poly, + val collisionKind: CollisionType = CollisionType.BLOCK, + @JsonFlat + val categoryFilter: PhysicsCategoryFilter, + + // ephemeral property from json + val enabled: Boolean = true, +) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt index 56b48f28..ad7b1a0a 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/PlayerWarping.kt @@ -63,7 +63,7 @@ sealed class SpawnTarget { } override suspend fun resolve(world: ServerWorld): Vector2d? { - return world.entities.values.firstOrNull { it.uniqueID.get().orNull() == id }?.position + return world.entities.values.firstOrNull { it.uniqueID.get() == id }?.position } override fun toString(): String { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt index da12a306..8ce30a38 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/ProjectileDefinition.kt @@ -1,19 +1,28 @@ package ru.dbotthepony.kstarbound.defs import com.google.common.collect.ImmutableList +import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject +import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.util.Either +import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.client.render.RenderLayer import ru.dbotthepony.kstarbound.defs.actor.StatModifier import ru.dbotthepony.kstarbound.defs.image.SpriteReference +import ru.dbotthepony.kstarbound.fromJson import ru.dbotthepony.kstarbound.json.builder.JsonFactory +import ru.dbotthepony.kstarbound.json.mergeJson import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNIT import ru.dbotthepony.kstarbound.world.physics.Poly +import java.util.concurrent.CompletableFuture +import java.util.function.Function @JsonFactory data class ProjectileDefinition( @@ -49,13 +58,14 @@ data class ProjectileDefinition( val lightPosition: Vector2d = Vector2d.ZERO, val pointLight: Boolean = false, val persistentAudio: AssetPath = AssetPath(""), + val damageTeam: EntityDamageTeam? = null, // Initialize timeToLive after animationCycle so we can have the default be // based on animationCycle val timeToLive: Double = if (animationLoops) animationCycle else 5.0, val damageKindImage: AssetPath = AssetPath(""), val damageKind: String = "", - val damageType: String = "", + val damageType: String = "Damage", val damageRepeatGroup: String? = null, val damageRepeatTimeout: Double? = null, val statusEffects: ImmutableList = ImmutableList.of(), @@ -68,10 +78,32 @@ data class ProjectileDefinition( val clientEntityMode: ClientEntityMode = ClientEntityMode.CLIENT_MASTER_ALLOWED, val masterOnly: Boolean = false, val scripts: ImmutableList = ImmutableList.of(), - val physicsForces: JsonObject = JsonObject(), - val physicsCollisions: JsonObject = JsonObject(), + val physicsForces: ImmutableMap = ImmutableMap.of(), + val physicsCollisions: ImmutableMap = ImmutableMap.of(), val persistentStatusEffects: ImmutableList> = ImmutableList.of(), val statusEffectArea: Poly = Poly.EMPTY, + + val physicsType: String = "default", + + @Deprecated("", replaceWith = ReplaceWith("this.actualMovementSettings")) + val movementSettings: JsonObject = JsonObject(), ) { val actualDamagePoly = if (damagePoly != null) damagePoly * (1.0 / PIXELS_IN_STARBOUND_UNIT) else null + + val actualMovementSettings: CompletableFuture = Starbound + .loadJsonAsset("/projectiles/physics.config:$physicsType") + .thenApplyAsync(Function { + Starbound.gson.fromJson(mergeJson(it?.deepCopy() ?: JsonNull.INSTANCE, movementSettings)) + }, Starbound.EXECUTOR) + + init { + actualMovementSettings.exceptionally { + LOGGER.error("Exception loading movement parameters for projectile with physics type $physicsType. Is it missing from /projectiles/physics.config?", it) + null + } + } + + companion object { + private val LOGGER = LogManager.getLogger() + } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/DamageTeam.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/DamageTeam.kt deleted file mode 100644 index dbb000f6..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/DamageTeam.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ru.dbotthepony.kstarbound.defs.`object` - -import ru.dbotthepony.kstarbound.defs.TeamType -import ru.dbotthepony.kstarbound.json.builder.JsonFactory - -@JsonFactory -data class DamageTeam( - val type: TeamType = TeamType.ENVIRONMENT, - val team: Int = 0 -) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt index 8a5d7422..670e6d06 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectDefinition.kt @@ -38,6 +38,7 @@ import ru.dbotthepony.kommons.gson.set import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.defs.AssetReference +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.animation.AnimationDefinition import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor import ru.dbotthepony.kstarbound.util.AssetPathStack @@ -72,7 +73,7 @@ data class ObjectDefinition( val unbreakable: Boolean = false, val damageShakeMagnitude: Double = 0.2, val damageMaterialKind: String = "solid", - val damageTeam: DamageTeam = DamageTeam(), + val damageTeam: EntityDamageTeam = EntityDamageTeam(), val lightColor: RGBAColor? = null, val lightColors: ImmutableMap = ImmutableMap.of(), val pointLight: Boolean = false, @@ -145,7 +146,7 @@ data class ObjectDefinition( val unbreakable: Boolean = false, val damageShakeMagnitude: Double = 0.2, val damageMaterialKind: String = "solid", - val damageTeam: DamageTeam = DamageTeam(), + val damageTeam: EntityDamageTeam = EntityDamageTeam(), val lightColor: RGBAColor? = null, val lightColors: ImmutableMap = ImmutableMap.of(), val pointLight: Boolean = false, @@ -166,7 +167,6 @@ data class ObjectDefinition( private val basic = gson.getAdapter(PlainData::class.java) private val damageConfig = gson.getAdapter(TileDamageParameters::class.java) - private val damageTeam = gson.getAdapter(DamageTeam::class.java) private val orientations = ObjectOrientation.Adapter(gson) private val emitter = gson.getAdapter(ParticleEmissionEntry::class.java) private val emitters = gson.listAdapter() @@ -222,7 +222,12 @@ data class ObjectDefinition( val path = AssetPathStack.last() for (v in ObjectOrientation.preprocess(read.getArray("orientations"))) { - val future = Starbound.GLOBAL_SCOPE.async { AssetPathStack(path) { this@Adapter.orientations.read(v as JsonObject) } }.asCompletableFuture() + val future = Starbound.GLOBAL_SCOPE.async { this@Adapter.orientations.read(v as JsonObject, path) }.asCompletableFuture() + + future.exceptionally { + LOGGER.error("Exception deserializing object orientation", it) + null + } future.thenAccept { if ("particleEmitter" in read) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt index 356d1d43..71b57298 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/object/ObjectOrientation.kt @@ -154,14 +154,24 @@ data class ObjectOrientation( private val spaces = gson.setAdapter() private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter>> - suspend fun read(obj: JsonObject): ObjectOrientation { + suspend fun read(obj: JsonObject, folder: String): ObjectOrientation { val drawables = ArrayList() val flipImages = obj.get("flipImages", false) val renderLayer = RenderLayer.parse(obj.get("renderLayer", "Object")) - if ("imageLayers" in obj) { - for (value in obj["imageLayers"].asJsonArray) { - var result = this.drawables.fromJsonTree(value) + AssetPathStack(folder) { + if ("imageLayers" in obj) { + for (value in obj["imageLayers"].asJsonArray) { + var result = this.drawables.fromJsonTree(value) + + if (flipImages) { + result = result.flop() + } + + drawables.add(result) + } + } else { + var result = this.drawables.fromJsonTree(obj) if (flipImages) { result = result.flop() @@ -169,14 +179,6 @@ data class ObjectOrientation( drawables.add(result) } - } else { - var result = this.drawables.fromJsonTree(obj) - - if (flipImages) { - result = result.flop() - } - - drawables.add(result) } val imagePosition = (obj["imagePosition"]?.let { vectors.fromJsonTree(it) } ?: Vector2f.ZERO) / PIXELS_IN_STARBOUND_UNITf diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt index 20a2b997..6db1a7fe 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/json/builder/EnumAdapter.kt @@ -25,7 +25,9 @@ interface IStringSerializable { val jsonName: String fun match(name: String): Boolean { - return name == jsonName + // there are more inconsistencies than consistencies + // where original engine ignores casing and where it does not + return name.lowercase() == jsonName.lowercase() } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt index 9319b690..97169f24 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/EntityBindings.kt @@ -15,7 +15,7 @@ fun provideEntityBindings(self: AbstractEntity, lua: LuaEnvironment) { table["id"] = luaFunction { returnBuffer.setTo(self.entityID) } table["position"] = luaFunction { returnBuffer.setTo(from(self.position)) } table["entityType"] = luaFunction { returnBuffer.setTo(self.type.jsonName) } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().orNull()) } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) } table["persistent"] = luaFunction { returnBuffer.setTo(self.isPersistent) } table["entityInSight"] = luaFunction { TODO() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt index d58d9644..0307bf5e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/bindings/WorldObjectBindings.kt @@ -52,8 +52,8 @@ fun provideWorldObjectBindings(self: WorldObject, lua: LuaEnvironment) { table["direction"] = luaFunction { returnBuffer.setTo(self.direction.luaValue) } table["position"] = luaFunction { returnBuffer.setTo(from(self.tilePosition)) } table["setInteractive"] = luaFunction { interactive: Boolean -> self.isInteractive = interactive } - table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get().orNull()) } - table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(KOptional.ofNullable(id?.decode())) } + table["uniqueId"] = luaFunction { returnBuffer.setTo(self.uniqueID.get()) } + table["setUniqueId"] = luaFunction { id: ByteString? -> self.uniqueID.accept(id?.decode()) } table["boundBox"] = luaFunction { returnBuffer.setTo(from(self.metaBoundingBox)) } // original engine parity, it returns occupied spaces in local coordinates diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt index 05c7956b..1f201087 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/Connection.kt @@ -188,7 +188,7 @@ abstract class Connection(val side: ConnectionSide, val type: ConnectionType) : var orbitalWarpAction by server2clientGroup.upstream.add(networkedData(KOptional(), warpActionCodec, legacyWarpActionCodec)) var worldID by server2clientGroup.upstream.add(networkedData(WorldID.Limbo, WorldID.CODEC, WorldID.LEGACY_CODEC)) var isAdmin by server2clientGroup.upstream.add(networkedBoolean()) - var team by server2clientGroup.upstream.add(networkedData(EntityDamageTeam(), EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)) + var team by server2clientGroup.upstream.add(networkedData(EntityDamageTeam.FRIENDLY, EntityDamageTeam.CODEC, EntityDamageTeam.LEGACY_CODEC)) var shipUpgrades by server2clientGroup.upstream.add(networkedData(ShipUpgrades(), ShipUpgrades.CODEC, ShipUpgrades.LEGACY_CODEC)) var shipCoordinate by server2clientGroup.upstream.add(networkedData(UniversePos(), UniversePos.CODEC, UniversePos.LEGACY_CODEC)) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt index 374a7b45..52da6942 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerChunk.kt @@ -463,7 +463,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk System.nanoTime()) + if (scheduledQueue.isEmpty() || !scheduledQueue.peek()!!.shouldExecuteNow()) next = eventQueue.poll() else next = null @@ -142,7 +147,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer if (scheduledQueue.isNotEmpty()) { val executed = ObjectArrayList>(4) - while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.executeAt <= System.nanoTime())) { + while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.shouldExecuteNow())) { executedAnything = true val poll = scheduledQueue.poll()!! diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt index 187d45c3..7df298d7 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/util/Directives.kt @@ -10,8 +10,8 @@ import ru.dbotthepony.kommons.gson.consumeNull import ru.dbotthepony.kstarbound.Starbound class Directives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap) { - constructor() : this(Object2ObjectAVLTreeMap()) - constructor(directives: String) : this() { + constructor() : this(EMPTY_MAP) + constructor(directives: String) : this(Object2ObjectAVLTreeMap()) { if (directives.isNotBlank()) { if ('?' !in directives) { if ('=' !in directives) { @@ -104,6 +104,8 @@ class Directives private constructor(private val directivesInternal: Object2Obje } companion object : TypeAdapter() { + private val EMPTY_MAP = Object2ObjectAVLTreeMap() + override fun write(out: JsonWriter, value: Directives?) { if (value == null) out.nullValue() diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt index 13660473..6a84a57c 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/World.kt @@ -316,6 +316,8 @@ abstract class World, ChunkType : Chunk sb.any { p.intersect(it) != null } } } + fun lineIntersectsPoly(a: Line2d, b: Poly): Boolean { + val sa = split(a) + val sb = split(b) + return sa.any { p -> sb.any { it.intersect(p) != null } } + } + fun rectIntersectsRect(a: AABB, b: AABB): Boolean { val sa = split(a).first val sb = split(b).first diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 8f4a8429..c6b48e3f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -8,6 +8,7 @@ import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream import it.unimi.dsi.fastutil.objects.ObjectArrayList import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.io.koptional +import ru.dbotthepony.kommons.io.nullable import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kommons.util.KOptional @@ -113,15 +114,12 @@ abstract class AbstractEntity : Comparable { * indexed in the stored world. Unique ids must be different across all * entities in a single world. */ - val uniqueID = networkedData(KOptional(), InternedStringCodec.koptional()) + val uniqueID = networkedData(null, InternedStringCodec.nullable()) var description = "" /** - * Whenever this entity should be removed when chunk containing it is being unloaded - * - * Returning false will also stop entity from being saved to disk, and render entity orphaned - * when chunk containing it will get unloaded + * Whenever this entity should be saved to disk when chunk containing it is being unloaded */ open val isPersistent: Boolean get() = true @@ -300,10 +298,6 @@ abstract class AbstractEntity : Comparable { * Called in narrow phase when determining which entities can be hit */ fun canBeHit(source: DamageSource, attacker: AbstractEntity? = null, inflictor: AbstractEntity? = null): Boolean { - if (!source.team.canDamage(team.get(), attacker === this)) { - return false - } - if (source.rayCheck) { val damageSource = inflictor ?: attacker ?: return true // nothing to check against @@ -414,7 +408,10 @@ abstract class AbstractEntity : Comparable { val attacker = world.entities[source.sourceEntityId] ?: this - val predicate = Predicate { it !== this && isDamageAuthoritative(it) && it.potentiallyCanBeHit(source, attacker, this) && it.canBeHit(source, attacker, this) } + val predicate = Predicate { + it !== this && isDamageAuthoritative(it) && source.team.canDamage(it.team.get(), attacker === it) && it.potentiallyCanBeHit(source, attacker, this) && it.canBeHit(source, attacker, this) + } + val hitEntities = source.damageArea.map({ world.entityIndex.query(it.aabb, predicate) }, { world.entityIndex.query(it, predicate) }) for (entity in hitEntities) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt index e98ca9fa..243adb8f 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/MovementController.kt @@ -34,7 +34,7 @@ import kotlin.math.acos import kotlin.math.cos import kotlin.math.sin -open class MovementController() { +open class MovementController { private var world0: World<*, *>? = null val world: World<*, *> get() = world0!! private var spatialEntry: EntityIndex.Entry? = null @@ -438,6 +438,10 @@ open class MovementController() { updateForceRegions() } + open fun tickRemote(delta: Double) { + + } + protected data class CollisionSeparation( var correction: Vector2d = Vector2d.ZERO, var solutionFound: Boolean = false, diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt index 8cdc21ae..fee1ab5e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/ProjectileEntity.kt @@ -1,23 +1,158 @@ package ru.dbotthepony.kstarbound.world.entities +import com.google.common.collect.ImmutableSet +import com.google.gson.JsonObject +import ru.dbotthepony.kommons.gson.contains +import ru.dbotthepony.kommons.gson.get +import ru.dbotthepony.kommons.guava.immutableMap +import ru.dbotthepony.kommons.io.readSignedVarInt +import ru.dbotthepony.kommons.io.writeBinaryString +import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kommons.util.getValue +import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Registry +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.defs.DamageType +import ru.dbotthepony.kstarbound.defs.EntityDamageTeam import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.PhysicsForceRegion +import ru.dbotthepony.kstarbound.defs.PhysicsMovingCollision +import ru.dbotthepony.kstarbound.defs.ProjectileDefinition +import ru.dbotthepony.kstarbound.fromJson +import ru.dbotthepony.kstarbound.io.readDouble +import ru.dbotthepony.kstarbound.io.writeDouble +import ru.dbotthepony.kstarbound.json.readJsonElement +import ru.dbotthepony.kstarbound.json.writeJsonElement import ru.dbotthepony.kstarbound.math.AABB +import ru.dbotthepony.kstarbound.math.vector.Vector2d +import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean +import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter +import ru.dbotthepony.kstarbound.util.AssetPathStack +import ru.dbotthepony.kstarbound.util.Directives +import ru.dbotthepony.kstarbound.util.valueOf +import java.io.DataInputStream import java.io.DataOutputStream -class ProjectileEntity() : DynamicEntity() { +class ProjectileEntity private constructor(val config: Registry.Entry, private val parameters: JsonObject) : DynamicEntity() { override val type: EntityType get() = EntityType.PROJECTILE + // TODO: make it persistent + override val isPersistent: Boolean + get() = false + + constructor(config: Registry.Entry, data: DataInputStream, isLegacy: Boolean) : this(config, data.readJsonElement() as JsonObject) { + sourceEntityId = if (isLegacy) data.readSignedVarInt() else data.readInt() + trackSourceEntity = data.readBoolean() + + initialSpeed = data.readDouble(isLegacy) + powerMultiplier = data.readDouble(isLegacy) + + team.accept(EntityDamageTeam(data, isLegacy)) + } + override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { - TODO("Not yet implemented") + stream.writeBinaryString(config.key) + stream.writeJsonElement(parameters) + if (isLegacy) stream.writeSignedVarInt(sourceEntityId) else stream.writeInt(sourceEntityId) + stream.writeBoolean(trackSourceEntity) + stream.writeDouble(initialSpeed, isLegacy) + stream.writeDouble(powerMultiplier, isLegacy) + team.get().write(stream, isLegacy) } override val metaBoundingBox: AABB - get() = TODO("Not yet implemented") - override val movement: MovementController - get() = TODO("Not yet implemented") + get() = config.value.boundBox + position - private fun setup() { + override val movement: MovementController = MovementController() + init { + if ("uniqueId" in parameters) { + this.uniqueID.accept(parameters["uniqueId"].asString) + } + } + + var acceleration = parameters.get("acceleration", config.value.acceleration) + var power = parameters.get("power", config.value.power) + var powerMultiplier = parameters.get("powerMultiplier", config.value.power) + var imageDirectives = Directives(parameters.get("processing", "")) + var persistentAudio = AssetPathStack.relativeTo(config.file?.computeDirectory(true) ?: "", parameters.get("persistentAudio", config.value.persistentAudio.fullPath)) + var damageKind = parameters.get("damageKind", config.value.damageKind) + var damageType = DamageType.entries.valueOf(parameters.get("damageType", config.value.damageType)) + var rayCheckToSource = parameters.get("rayCheckToSource", config.value.rayCheckToSource) + var damageRepeatGroup = parameters["damageRepeatGroup"]?.asString ?: config.value.damageRepeatGroup + var damageRepeatTimeout = parameters["damageRepeatTimeout"]?.asDouble ?: config.value.damageRepeatTimeout + var falldown = parameters["falldown"]?.asBoolean ?: config.value.falldown + var hydrophobic = parameters["hydrophobic"]?.asBoolean ?: config.value.hydrophobic + var onlyHitTerrain = parameters["onlyHitTerrain"]?.asBoolean ?: config.value.onlyHitTerrain + + init { + if ("damageTeam" in parameters) { + team.accept(Starbound.gson.fromJson(parameters["damageTeam"])!!) + } else if (config.value.damageTeam != null) { + team.accept(config.value.damageTeam!!) + } + + var movementParams = config.value.actualMovementSettings.get() + + if ("movementSettings" in parameters) { + movementParams = movementParams.merge(Starbound.gson.fromJson(parameters["movementSettings"])!!) + } + + if (movementParams.physicsEffectCategories == null) { + movementParams = movementParams.copy(physicsEffectCategories = ImmutableSet.of("projectile")) + } + + movement.applyParameters(movementParams) + } + + var initialSpeed = parameters["speed"]?.asDouble ?: config.value.speed + + private var lastSourceEntityPosition: Vector2d? = null + var sourceEntityId: Int = 0 + + var sourceEntity: AbstractEntity? + get() = if (isInWorld) world.entities[sourceEntityId] else null + set(value) { + sourceEntityId = value?.entityID ?: 0 + } + + var trackSourceEntity = false + var bounces = parameters["bounces"]?.asInt ?: config.value.bounces + var frame = 0 + var animationTimer = 0.0 + var animationCycle = parameters["animationCycle"]?.asDouble ?: config.value.animationCycle + var collision = false + + inner class ForceRegion(val region: PhysicsForceRegion) { + var enabled by networkedBoolean(region.enabled).also { networkGroup.upstream.add(it) } + } + + inner class MovingCollision(val collision: PhysicsMovingCollision) { + var enabled by networkedBoolean(collision.enabled).also { networkGroup.upstream.add(it) } + } + + val physicsForces = immutableMap { + for (k in config.value.physicsForces.keys.sorted()) { + put(k, ForceRegion(config.value.physicsForces[k]!!)) + } + } + + val physicsCollisions = immutableMap { + for (k in config.value.physicsCollisions.keys.sorted()) { + put(k, MovingCollision(config.value.physicsCollisions[k]!!)) + } + } + + val collisionEvent = networkedEventCounter().also { networkGroup.upstream.add(it) } + + init { + networkGroup.upstream.add(movement.networkGroup) + } + + val emitter = EffectEmitter(this).also { networkGroup.upstream.add(it.networkGroup) } + + override fun tick(delta: Double) { + super.tick(delta) } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt index fcc66b1c..e0e7710e 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/player/PlayerEntity.kt @@ -2,20 +2,21 @@ package ru.dbotthepony.kstarbound.world.entities.player import com.google.gson.JsonObject import ru.dbotthepony.kommons.io.writeBinaryString -import ru.dbotthepony.kstarbound.math.AABB -import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue -import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.Globals +import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityType +import ru.dbotthepony.kstarbound.defs.HitType import ru.dbotthepony.kstarbound.defs.actor.Gender import ru.dbotthepony.kstarbound.defs.actor.HumanoidData import ru.dbotthepony.kstarbound.defs.actor.HumanoidEmote import ru.dbotthepony.kstarbound.defs.actor.player.PlayerGamemode import ru.dbotthepony.kstarbound.io.readInternedString import ru.dbotthepony.kstarbound.json.builder.IStringSerializable +import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.math.Interpolator +import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean import ru.dbotthepony.kstarbound.network.syncher.networkedData import ru.dbotthepony.kstarbound.network.syncher.networkedEnum @@ -23,12 +24,14 @@ import ru.dbotthepony.kstarbound.network.syncher.networkedEnumExtraStupid import ru.dbotthepony.kstarbound.network.syncher.networkedEventCounter import ru.dbotthepony.kstarbound.network.syncher.networkedFixedPoint import ru.dbotthepony.kstarbound.network.syncher.networkedString +import ru.dbotthepony.kstarbound.world.entities.AbstractEntity import ru.dbotthepony.kstarbound.world.entities.Animator import ru.dbotthepony.kstarbound.world.entities.HumanoidActorEntity import ru.dbotthepony.kstarbound.world.entities.StatusController +import ru.dbotthepony.kstarbound.world.physics.Poly import java.io.DataInputStream import java.io.DataOutputStream -import java.util.UUID +import java.util.* import kotlin.properties.Delegates class PlayerEntity() : HumanoidActorEntity() { @@ -47,7 +50,7 @@ class PlayerEntity() : HumanoidActorEntity() { } constructor(data: DataInputStream, isLegacy: Boolean) : this() { - uniqueID.accept(KOptional(data.readInternedString())) + uniqueID.accept(data.readInternedString()) description = data.readInternedString() gamemode = PlayerGamemode.entries[if (isLegacy) data.readInt() else data.readUnsignedByte()] humanoidData = HumanoidData.read(data, isLegacy) @@ -63,12 +66,7 @@ class PlayerEntity() : HumanoidActorEntity() { var gamemode = PlayerGamemode.CASUAL override fun writeNetwork(stream: DataOutputStream, isLegacy: Boolean) { - uniqueID.get().ifPresent { - stream.writeBinaryString(it) - }.ifNotPresent { - stream.writeBinaryString("") - } - + stream.writeBinaryString(uniqueID.get() ?: "") stream.writeBinaryString(description) if (isLegacy) stream.writeInt(gamemode.ordinal) else stream.writeByte(gamemode.ordinal) humanoidData.write(stream, isLegacy) @@ -131,6 +129,32 @@ class PlayerEntity() : HumanoidActorEntity() { override val isPersistent: Boolean get() = false + var isAdmin = false + + val isDead: Boolean + get() = health <= 0.0 + + val isTeleporting: Boolean + get() = state == State.TELEPORT_IN || state == State.TELEPORT_OUT + + override val damageHitbox: List + get() = movement.computeLocalHitboxes() + + override fun potentiallyCanBeHit( + source: DamageSource, + attacker: AbstractEntity?, + inflictor: AbstractEntity? + ): Boolean { + return !isAdmin && !isDead && !isTeleporting && (statusController.resources["invulnerable"]?.value ?: 0.0) <= 0.0 + } + + override fun queryHit(source: DamageSource, attacker: AbstractEntity?, inflictor: AbstractEntity?): HitType? { + if (source.intersect(world.geometry, movement.computeLocalHitboxes())) + return HitType.HIT + + return null + } + var uuid: UUID by Delegates.notNull() private set } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt index 89699f2a..b0594d19 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/PlantEntity.kt @@ -34,6 +34,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.isEmptyTile import ru.dbotthepony.kstarbound.defs.tile.isMetaTile import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile +import ru.dbotthepony.kstarbound.defs.tile.isNullTile import ru.dbotthepony.kstarbound.defs.world.BushVariant import ru.dbotthepony.kstarbound.defs.world.GrassVariant import ru.dbotthepony.kstarbound.defs.world.TreeVariant @@ -649,7 +650,10 @@ class PlantEntity() : TileEntity() { remove(RemovalReason.REMOVED) } else if (roots.isNotEmpty()) { for (root in roots) { - if (world.getCell(root).foreground.material.isEmptyTile) { + val cell = world.getCell(root) + + // avoid timber on edges of chunks which are being unloaded / loaded + if (cell.foreground.material.isEmptyTile && !cell.foreground.material.isNullTile) { if (fallsWhenDead) { breakAtPosition(tilePosition, position) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 386731cc..6c729eeb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -110,12 +110,12 @@ open class WorldObject(val config: Registry.Entry) : TileEntit loadParameters(data.get("parameters") { JsonObject() }) if ("uniqueId" in data) - uniqueID.accept(KOptional.ofNullable(data["uniqueId"]?.asStringOrNull)) + uniqueID.accept(data["uniqueId"]?.asStringOrNull) } open fun loadParameters(parameters: JsonObject) { if ("uniqueId" in parameters) { - uniqueID.accept(KOptional(parameters["uniqueId"]!!.asString)) + uniqueID.accept(parameters["uniqueId"]!!.asString) } for ((k, v) in parameters.entrySet()) { @@ -136,7 +136,7 @@ open class WorldObject(val config: Registry.Entry) : TileEntit data["scriptStorage"] = scriptStorage.toJson(true) } - uniqueID.get().ifPresent { + uniqueID.get()?.let { data["uniqueId"] = it } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt index 619a70f1..fc7d4b2d 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/physics/Poly.kt @@ -38,7 +38,7 @@ import kotlin.math.cos import kotlin.math.sin private fun calculateEdges(points: List): Pair, ImmutableList> { - require(points.size >= 2) { "Provided poly is invalid (only ${points.size} points are defined)" } + require(points.size >= 2) { "Provided poly is degenerate (only ${points.size} points are defined)" } if (points.size == 2) { // line... @@ -444,8 +444,14 @@ class Poly private constructor(val edges: ImmutableList, val vertices: I override fun read(`in`: JsonReader): Poly? { if (`in`.consumeNull()) return null - else - return Poly(list.read(`in`)) + else { + val list = list.read(`in`) + + if (list.isEmpty()) + return EMPTY + + return Poly(list) + } } } as TypeAdapter }