Minimal projectile net state code

This commit is contained in:
DBotThePony 2024-04-28 16:37:48 +07:00
parent ac55422c3b
commit d141bb64b0
Signed by: DBot
GPG Key ID: DCC23B5715498507
29 changed files with 471 additions and 122 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String> = ImmutableSet.of()) : Predicate<String> {
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<String>): 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<String>? = null,
val categoryBlacklist: ImmutableSet<String>? = null,
)
class Adapter(gson: Gson) : TypeAdapter<PhysicsCategoryFilter>() {
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()
}
}

View File

@ -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<String>) : Predicate<String> {
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<String>): 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<out PhysicsForceRegion>) : 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)

View File

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

View File

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

View File

@ -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<EphemeralStatusEffect> = ImmutableList.of(),
@ -68,10 +78,32 @@ data class ProjectileDefinition(
val clientEntityMode: ClientEntityMode = ClientEntityMode.CLIENT_MASTER_ALLOWED,
val masterOnly: Boolean = false,
val scripts: ImmutableList<AssetPath> = ImmutableList.of(),
val physicsForces: JsonObject = JsonObject(),
val physicsCollisions: JsonObject = JsonObject(),
val physicsForces: ImmutableMap<String, PhysicsForceRegion> = ImmutableMap.of(),
val physicsCollisions: ImmutableMap<String, PhysicsMovingCollision> = ImmutableMap.of(),
val persistentStatusEffects: ImmutableList<Either<String, StatModifier>> = 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<MovementParameters> = 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()
}
}

View File

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

View File

@ -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<String, RGBAColor> = 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<String, RGBAColor> = 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<ParticleEmissionEntry>()
@ -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) {

View File

@ -154,14 +154,24 @@ data class ObjectOrientation(
private val spaces = gson.setAdapter<Vector2i>()
private val materialSpaces = gson.getAdapter(TypeToken.getParameterized(ImmutableList::class.java, TypeToken.getParameterized(Pair::class.java, Vector2i::class.java, String::class.java).type)) as TypeAdapter<ImmutableList<Pair<Vector2i, String>>>
suspend fun read(obj: JsonObject): ObjectOrientation {
suspend fun read(obj: JsonObject, folder: String): ObjectOrientation {
val drawables = ArrayList<Drawable>()
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

View File

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

View File

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

View File

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

View File

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

View File

@ -463,7 +463,7 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
entity.joinWorld(world)
}
if (isBackground && cell.foreground.material.isEmptyTile) {
if (mCell.foreground.material.isEmptyTile && mCell.background.material.isEmptyTile) {
val info = world.template.cellInfo(pos + this.pos.tile)
if (info.oceanLiquid.isNotEmptyLiquid && !info.encloseLiquids && pos.y < info.oceanLiquidLevel) {
@ -553,10 +553,10 @@ class ServerChunk(world: ServerWorld, pos: ChunkPos) : Chunk<ServerWorld, Server
val unloadable = world.entityIndex
.query(
aabbd,
filter = Predicate { it.isPersistent && !it.isRemote && aabbd.isInside(it.position) })
filter = Predicate { !it.isRemote && aabbd.isInside(it.position) })
world.storage.saveCells(pos, copyCells(), state, now)
world.storage.saveEntities(pos, unloadable, now)
world.storage.saveEntities(pos, unloadable.filter { it.isPersistent }, now)
return unloadable
}

View File

@ -21,6 +21,7 @@ import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.LockSupport
import java.util.function.Supplier
import kotlin.math.absoluteValue
// I tried to make use of Netty's event loops, but they seem to be a bit overcomplicated
// if you try to use them by yourself :(
@ -35,6 +36,10 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
return executeAt - System.nanoTime()
}
fun shouldExecuteNow(): Boolean {
return executeAt <= System.nanoTime()
}
fun shouldEnqueue(isShutdown: Boolean): Boolean {
if (isShutdown || executeAt <= System.nanoTime())
return perform(isShutdown)
@ -89,7 +94,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
val scope = CoroutineScope(coroutines + SupervisorJob())
init {
priority = 7
priority = MAX_PRIORITY
}
private fun nextDeadline(): Long {
@ -133,7 +138,7 @@ open class BlockableEventLoop(name: String) : Thread(name), ScheduledExecutorSer
}
// keep executing queued tasks until we hit scheduled task deadline
if (scheduledQueue.isEmpty() || scheduledQueue.peek()!!.executeAt > 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<ScheduledTask<*>>(4)
while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.executeAt <= System.nanoTime())) {
while (scheduledQueue.isNotEmpty() && (isShutdown || scheduledQueue.peek()!!.shouldExecuteNow())) {
executedAnything = true
val poll = scheduledQueue.poll()!!

View File

@ -10,8 +10,8 @@ import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kstarbound.Starbound
class Directives private constructor(private val directivesInternal: Object2ObjectAVLTreeMap<String, String>) {
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<Directives>() {
private val EMPTY_MAP = Object2ObjectAVLTreeMap<String, String>()
override fun write(out: JsonWriter, value: Directives?) {
if (value == null)
out.nullValue()

View File

@ -316,6 +316,8 @@ abstract class World<This : World<This, ChunkType>, ChunkType : Chunk<This, Chun
dynamicEntities.forEach {
if (!it.isRemote) {
it.movement.move(delta)
} else {
it.movement.tickRemote(delta)
}
}
} else {

View File

@ -389,6 +389,12 @@ data class WorldGeometry(val size: Vector2i, val loopX: Boolean = true, val loop
return sa.any { p -> 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

View File

@ -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<AbstractEntity> {
* 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<AbstractEntity> {
* 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<AbstractEntity> {
val attacker = world.entities[source.sourceEntityId] ?: this
val predicate = Predicate<AbstractEntity> { it !== this && isDamageAuthoritative(it) && it.potentiallyCanBeHit(source, attacker, this) && it.canBeHit(source, attacker, this) }
val predicate = Predicate<AbstractEntity> {
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) {

View File

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

View File

@ -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<ProjectileDefinition>, 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<ProjectileDefinition>, 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<String, ForceRegion> {
for (k in config.value.physicsForces.keys.sorted()) {
put(k, ForceRegion(config.value.physicsForces[k]!!))
}
}
val physicsCollisions = immutableMap<String, MovingCollision> {
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)
}
}

View File

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

View File

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

View File

@ -110,12 +110,12 @@ open class WorldObject(val config: Registry.Entry<ObjectDefinition>) : 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<ObjectDefinition>) : TileEntit
data["scriptStorage"] = scriptStorage.toJson(true)
}
uniqueID.get().ifPresent {
uniqueID.get()?.let {
data["uniqueId"] = it
}

View File

@ -38,7 +38,7 @@ import kotlin.math.cos
import kotlin.math.sin
private fun calculateEdges(points: List<Vector2d>): Pair<ImmutableList<Line2d>, ImmutableList<Vector2d>> {
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<Line2d>, 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<T>
}