mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
new: world update ahchoring
This commit is contained in:
@@ -121,6 +121,46 @@ emph {
|
||||
|
||||
val uptime = App.getTIME_T() - App.startupTime
|
||||
|
||||
|
||||
|
||||
// print out the error
|
||||
printStream.println("<h3>The Error Info</h3>")
|
||||
System.err.println("== The Error Info ==")
|
||||
|
||||
printStream.println("<pre>")
|
||||
e.printStackTrace(printStream)
|
||||
printStream.println("</pre>")
|
||||
e.printStackTrace(System.err)
|
||||
|
||||
|
||||
|
||||
printStream.println("<h3>Module Info</h3>")
|
||||
printStream.println("<h4>Load Order</h4>")
|
||||
printStream.println("<ol>${ModMgr.loadOrder.joinToString(separator = "") { "<li>" +
|
||||
"$it <small>(" +
|
||||
"${moduleMetaToText(ModMgr.moduleInfo[it] ?: ModMgr.moduleInfoErrored[it])}" +
|
||||
")</small></li>" }
|
||||
}</ol>")
|
||||
|
||||
|
||||
|
||||
// print out loaded modules
|
||||
ModMgr.errorLogs.let {
|
||||
if (it.size > 0) {
|
||||
printStream.println("<h4>Module Errors</h4>")
|
||||
System.err.println("== Module Errors ==")
|
||||
it.forEach {
|
||||
printStream.println("<p>From Module <strong>${it.moduleName}</strong> (${it.type.toHTML()}):</p>")
|
||||
printStream.println("<pre>")
|
||||
it.cause?.printStackTrace(printStream)
|
||||
printStream.println("</pre>")
|
||||
it.cause?.printStackTrace(System.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// print out device info
|
||||
printStream.println("<h3>System Info</h3>")
|
||||
printStream.println("<ul>")
|
||||
@@ -146,39 +186,6 @@ emph {
|
||||
printStream.println("<p><emph>GL not initialised</emph></p>")
|
||||
}
|
||||
|
||||
printStream.println("<h3>Module Info</h3>")
|
||||
printStream.println("<h4>Load Order</h4>")
|
||||
printStream.println("<ol>${ModMgr.loadOrder.joinToString(separator = "") { "<li>" +
|
||||
"$it <small>(" +
|
||||
"${moduleMetaToText(ModMgr.moduleInfo[it] ?: ModMgr.moduleInfoErrored[it])}" +
|
||||
")</small></li>" }
|
||||
}</ol>")
|
||||
|
||||
|
||||
ModMgr.errorLogs.let {
|
||||
if (it.size > 0) {
|
||||
printStream.println("<h4>Module Errors</h4>")
|
||||
System.err.println("== Module Errors ==")
|
||||
it.forEach {
|
||||
printStream.println("<p>From Module <strong>${it.moduleName}</strong> (${it.type.toHTML()}):</p>")
|
||||
printStream.println("<pre>")
|
||||
it.cause?.printStackTrace(printStream)
|
||||
printStream.println("</pre>")
|
||||
it.cause?.printStackTrace(System.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printStream.println("<h3>The Error Info</h3>")
|
||||
System.err.println("== The Error Info ==")
|
||||
|
||||
printStream.println("<pre>")
|
||||
e.printStackTrace(printStream)
|
||||
printStream.println("</pre>")
|
||||
e.printStackTrace(System.err)
|
||||
|
||||
|
||||
|
||||
textArea.text = "<html><style type=\"text/css\">$css</style><body>$htmlSB</body></html>"
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.torvald.terrarum.gameactors.ActorID
|
||||
import net.torvald.terrarum.gameactors.ActorWithBody
|
||||
import net.torvald.terrarum.gameactors.ActorWithBody.Companion.PHYS_EPSILON_DIST
|
||||
import net.torvald.terrarum.gameactors.BlockMarkerActor
|
||||
import net.torvald.terrarum.gameactors.WorldUpdater
|
||||
import net.torvald.terrarum.gamecontroller.KeyToggler
|
||||
import net.torvald.terrarum.gamecontroller.TerrarumKeyboardEvent
|
||||
import net.torvald.terrarum.gameitems.ItemID
|
||||
@@ -147,6 +148,13 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo
|
||||
val actorAdditionQueue = ArrayList<Triple<Actor, Throwable, (Actor) -> Unit>>() // actor, stacktrace object, onSpawn
|
||||
val actorRemovalQueue = ArrayList<Triple<Actor, Throwable, (Actor) -> Unit>>() // actor, stacktrace object, onDespawn
|
||||
|
||||
/**
|
||||
* Registry of actors that implement [WorldUpdater].
|
||||
* World simulation (fluids, wires, tile updates) is active around these actors.
|
||||
* Automatically maintained when actors are added/removed.
|
||||
*/
|
||||
val worldUpdaters: MutableSet<ActorWithBody> = HashSet()
|
||||
|
||||
/**
|
||||
* ## BIG NOTE: Calculated actor distance is the **Euclidean distance SQUARED**
|
||||
*
|
||||
@@ -378,6 +386,10 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo
|
||||
actorContainer.removeAt(indexToDelete)
|
||||
}
|
||||
}
|
||||
// Unregister from worldUpdaters if applicable
|
||||
if (actor is WorldUpdater && actor is ActorWithBody) {
|
||||
worldUpdaters.remove(actor)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun forceAddActor(actor: Actor?, caller: Throwable = StackTraceRecorder()) {
|
||||
@@ -388,6 +400,10 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo
|
||||
}
|
||||
else {
|
||||
actorContainerActive.add(actor)
|
||||
// Register to worldUpdaters if applicable
|
||||
if (actor is WorldUpdater && actor is ActorWithBody) {
|
||||
worldUpdaters.add(actor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
src/net/torvald/terrarum/gameactors/WorldUpdateAnchor.kt
Normal file
47
src/net/torvald/terrarum/gameactors/WorldUpdateAnchor.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
package net.torvald.terrarum.gameactors
|
||||
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch
|
||||
|
||||
/**
|
||||
* Minimal anchor actor for world updates.
|
||||
*
|
||||
* This actor has no visible representation and does not participate in physics.
|
||||
* It serves purely as a point around which world simulation (fluids, wires, tile updates)
|
||||
* remains active.
|
||||
*
|
||||
* Created by minjaesong on 2026-01-19.
|
||||
*/
|
||||
class WorldUpdateAnchor : ActorWithBody, WorldUpdater, NoSerialise {
|
||||
|
||||
constructor() : super(RenderOrder.MIDDLE, PhysProperties.IMMOBILE()) {
|
||||
isVisible = false
|
||||
chunkAnchoring = true
|
||||
setHitboxDimension(1, 1, 0, 0)
|
||||
}
|
||||
|
||||
constructor(id: ActorID) : super(RenderOrder.MIDDLE, PhysProperties.IMMOBILE(), id) {
|
||||
isVisible = false
|
||||
chunkAnchoring = true
|
||||
setHitboxDimension(1, 1, 0, 0)
|
||||
}
|
||||
|
||||
override fun updateImpl(delta: Float) {
|
||||
// No-op; this actor exists solely as a world update anchor
|
||||
}
|
||||
|
||||
override fun drawBody(frameDelta: Float, batch: SpriteBatch) {
|
||||
// No-op; this actor is invisible
|
||||
}
|
||||
|
||||
override fun drawGlow(frameDelta: Float, batch: SpriteBatch) {
|
||||
// No-op; this actor is invisible
|
||||
}
|
||||
|
||||
override fun drawEmissive(frameDelta: Float, batch: SpriteBatch) {
|
||||
// No-op; this actor is invisible
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
// Nothing to dispose
|
||||
}
|
||||
}
|
||||
13
src/net/torvald/terrarum/gameactors/WorldUpdater.kt
Normal file
13
src/net/torvald/terrarum/gameactors/WorldUpdater.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package net.torvald.terrarum.gameactors
|
||||
|
||||
/**
|
||||
* Marker interface for actors that serve as "world update anchors".
|
||||
*
|
||||
* Actors implementing this interface cause world simulation (fluids, wires, tile updates)
|
||||
* to be active in their vicinity, regardless of camera position.
|
||||
*
|
||||
* All implementations must also extend [ActorWithBody].
|
||||
*
|
||||
* Created by minjaesong on 2026-01-19.
|
||||
*/
|
||||
interface WorldUpdater
|
||||
@@ -150,8 +150,23 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {
|
||||
(a.hitbox.centeredY - WorldCamera.yCentre).sqr()
|
||||
)
|
||||
|
||||
/** whether the actor is within update range */
|
||||
fun ActorWithBody.inUpdateRange(world: GameWorld) = distToCameraSqr(world, this) <= ACTOR_UPDATE_RANGE.sqr()
|
||||
/**
|
||||
* Returns the minimum squared distance from the actor to any living WorldUpdater.
|
||||
* Falls back to camera distance if no WorldUpdaters exist.
|
||||
*/
|
||||
fun distToNearestUpdaterSqr(world: GameWorld, actor: ActorWithBody): Double {
|
||||
val worldUpdaters = Terrarum.ingame?.worldUpdaters
|
||||
if (worldUpdaters.isNullOrEmpty()) {
|
||||
return distToCameraSqr(world, actor)
|
||||
}
|
||||
return worldUpdaters.minOf { updater ->
|
||||
if (updater.flagDespawn || updater.despawned) Double.MAX_VALUE
|
||||
else distToActorSqr(world, actor, updater)
|
||||
}
|
||||
}
|
||||
|
||||
/** whether the actor is within update range of any WorldUpdater */
|
||||
fun ActorWithBody.inUpdateRange(world: GameWorld) = distToNearestUpdaterSqr(world, this) <= ACTOR_UPDATE_RANGE.sqr()
|
||||
|
||||
/** whether the actor is within screen */
|
||||
fun ActorWithBody.inScreen(world: GameWorld) =
|
||||
|
||||
@@ -23,6 +23,7 @@ import net.torvald.terrarum.realestate.LandUtil.CHUNK_H
|
||||
import net.torvald.terrarum.realestate.LandUtil.CHUNK_W
|
||||
import org.dyn4j.geometry.Vector2
|
||||
import kotlin.math.cosh
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
@@ -50,6 +51,9 @@ object WorldSimulator {
|
||||
private val fluidNewMap = Array(DOUBLE_RADIUS) { FloatArray(DOUBLE_RADIUS) }
|
||||
private val fluidNewTypeMap = Array(DOUBLE_RADIUS) { Array(DOUBLE_RADIUS) { Fluid.NULL } }
|
||||
|
||||
// Mask to track tiles that have already been fluid-simulated this frame (for overlap deduplication)
|
||||
private val processedFluidTiles = HashSet<Long>()
|
||||
|
||||
const val FLUID_MAX_MASS = 1f // The normal, un-pressurized mass of a full water cell
|
||||
const val FLUID_MAX_COMP = 0.01f // How much excess water a cell can store, compared to the cell above it. A tile of fluid can contain more than MaxMass water.
|
||||
// const val FLUID_MIN_MASS = net.torvald.terrarum.gameworld.FLUID_MIN_MASS //Ignore cells that are almost dry (smaller than epsilon of float16)
|
||||
@@ -66,6 +70,97 @@ object WorldSimulator {
|
||||
/** Bottom-right point */
|
||||
var updateYTo = 0
|
||||
|
||||
/**
|
||||
* Represents a rectangular region for world updates.
|
||||
*/
|
||||
data class UpdateRegion(val xFrom: Int, val yFrom: Int, val xTo: Int, val yTo: Int) {
|
||||
fun overlaps(other: UpdateRegion): Boolean {
|
||||
return xFrom <= other.xTo && xTo >= other.xFrom &&
|
||||
yFrom <= other.yTo && yTo >= other.yFrom
|
||||
}
|
||||
|
||||
fun merge(other: UpdateRegion): UpdateRegion {
|
||||
return UpdateRegion(
|
||||
min(xFrom, other.xFrom),
|
||||
min(yFrom, other.yFrom),
|
||||
max(xTo, other.xTo),
|
||||
max(yTo, other.yTo)
|
||||
)
|
||||
}
|
||||
|
||||
val width get() = xTo - xFrom
|
||||
val height get() = yTo - yFrom
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges overlapping regions into non-overlapping ones.
|
||||
* Uses iterative merging until no more merges are possible.
|
||||
*/
|
||||
private fun mergeOverlappingRegions(regions: List<UpdateRegion>): List<UpdateRegion> {
|
||||
if (regions.size <= 1) return regions
|
||||
|
||||
val result = regions.toMutableList()
|
||||
var merged = true
|
||||
while (merged) {
|
||||
merged = false
|
||||
outer@ for (i in result.indices) {
|
||||
for (j in i + 1 until result.size) {
|
||||
if (result[i].overlaps(result[j])) {
|
||||
val mergedRegion = result[i].merge(result[j])
|
||||
result.removeAt(j)
|
||||
result[i] = mergedRegion
|
||||
merged = true
|
||||
break@outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes update regions from all living WorldUpdaters.
|
||||
* Returns a list of non-overlapping merged regions.
|
||||
*/
|
||||
private fun computeUpdateRegions(): List<UpdateRegion> {
|
||||
val worldUpdaters = ingame.worldUpdaters.filter { !it.flagDespawn && !it.despawned }
|
||||
if (worldUpdaters.isEmpty()) return emptyList()
|
||||
|
||||
val regions = worldUpdaters.map { updater ->
|
||||
val cx = updater.hitbox.centeredX.div(TILE_SIZE).roundToInt()
|
||||
val cy = updater.hitbox.centeredY.div(TILE_SIZE).roundToInt()
|
||||
UpdateRegion(
|
||||
cx - FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cy - FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cx + FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cy + FLUID_UPDATING_SQUARE_RADIUS
|
||||
)
|
||||
}
|
||||
|
||||
return mergeOverlappingRegions(regions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes individual (unmerged) fluid regions for each WorldUpdater.
|
||||
* Each region is exactly DOUBLE_RADIUS × DOUBLE_RADIUS to fit the fixed fluid arrays.
|
||||
* Overlap deduplication is handled by processedFluidTiles mask during fluidmapToWorld().
|
||||
*/
|
||||
private fun computeIndividualFluidRegions(): List<UpdateRegion> {
|
||||
val worldUpdaters = ingame.worldUpdaters.filter { !it.flagDespawn && !it.despawned }
|
||||
if (worldUpdaters.isEmpty()) return emptyList()
|
||||
|
||||
return worldUpdaters.map { updater ->
|
||||
val cx = updater.hitbox.centeredX.div(TILE_SIZE).roundToInt()
|
||||
val cy = updater.hitbox.centeredY.div(TILE_SIZE).roundToInt()
|
||||
UpdateRegion(
|
||||
cx - FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cy - FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cx + FLUID_UPDATING_SQUARE_RADIUS,
|
||||
cy + FLUID_UPDATING_SQUARE_RADIUS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val ingame: TerrarumIngame
|
||||
get() = Terrarum.ingame!! as TerrarumIngame
|
||||
private val world: GameWorld
|
||||
@@ -82,21 +177,66 @@ object WorldSimulator {
|
||||
|
||||
//printdbg(this, "============================")
|
||||
|
||||
if (player != null) {
|
||||
updateXFrom = player.hitbox.centeredX.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
updateYFrom = player.hitbox.centeredY.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
updateXTo = updateXFrom + DOUBLE_RADIUS
|
||||
updateYTo = updateYFrom + DOUBLE_RADIUS
|
||||
// Compute non-overlapping merged regions for general simulations
|
||||
val mergedRegions = computeUpdateRegions()
|
||||
|
||||
// Compute individual (unmerged) regions for fluid simulation
|
||||
// Each WorldUpdater gets its own fluid region; overlap is handled by processedFluidTiles mask
|
||||
val fluidRegions = computeIndividualFluidRegions()
|
||||
|
||||
// Fallback to player-based region if no WorldUpdaters exist
|
||||
val regionsToProcess = if (mergedRegions.isNotEmpty()) {
|
||||
mergedRegions
|
||||
} else if (player != null) {
|
||||
val px = player.hitbox.centeredX.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
val py = player.hitbox.centeredY.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
listOf(UpdateRegion(px, py, px + DOUBLE_RADIUS, py + DOUBLE_RADIUS))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
if (ingame.terrainChangeQueue.isNotEmpty()) { App.measureDebugTime("WorldSimulator.degrass") { buryGrassImmediately() } }
|
||||
val fluidRegionsToProcess = if (fluidRegions.isNotEmpty()) {
|
||||
fluidRegions
|
||||
} else if (player != null) {
|
||||
val px = player.hitbox.centeredX.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
val py = player.hitbox.centeredY.div(TILE_SIZE).minus(FLUID_UPDATING_SQUARE_RADIUS).roundToInt()
|
||||
listOf(UpdateRegion(px, py, px + DOUBLE_RADIUS, py + DOUBLE_RADIUS))
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
// buryGrassImmediately doesn't depend on update region
|
||||
if (ingame.terrainChangeQueue.isNotEmpty()) {
|
||||
App.measureDebugTime("WorldSimulator.degrass") { buryGrassImmediately() }
|
||||
}
|
||||
|
||||
// Process fluids for each WorldUpdater individually, using mask for overlap deduplication
|
||||
processedFluidTiles.clear()
|
||||
App.measureDebugTime("WorldSimulator.fluids") {
|
||||
for (region in fluidRegionsToProcess) {
|
||||
updateXFrom = region.xFrom
|
||||
updateYFrom = region.yFrom
|
||||
updateXTo = region.xTo
|
||||
updateYTo = region.yTo
|
||||
moveFluids(delta)
|
||||
}
|
||||
}
|
||||
|
||||
// Process other simulations using merged non-overlapping regions
|
||||
for (region in regionsToProcess) {
|
||||
updateXFrom = region.xFrom
|
||||
updateYFrom = region.yFrom
|
||||
updateXTo = region.xTo
|
||||
updateYTo = region.yTo
|
||||
|
||||
App.measureDebugTime("WorldSimulator.growGrass") { growOrKillGrass() }
|
||||
App.measureDebugTime("WorldSimulator.fluids") { moveFluids(delta) }
|
||||
App.measureDebugTime("WorldSimulator.fallables") { displaceFallables(delta) }
|
||||
App.measureDebugTime("WorldSimulator.wires") { simulateWires(delta) }
|
||||
App.measureDebugTime("WorldSimulator.collisionDroppedItem") { collideDroppedItems() }
|
||||
App.measureDebugTime("WorldSimulator.dropTreeLeaves") { dropTreeLeaves() }
|
||||
}
|
||||
|
||||
// collideDroppedItems doesn't depend on update region (uses actor list)
|
||||
App.measureDebugTime("WorldSimulator.collisionDroppedItem") { collideDroppedItems() }
|
||||
|
||||
//printdbg(this, "============================")
|
||||
}
|
||||
@@ -502,10 +642,21 @@ object WorldSimulator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Packs world coordinates into a single Long for use as a HashSet key */
|
||||
private fun packFluidCoords(worldX: Int, worldY: Int): Long =
|
||||
(worldX.toLong() shl 32) or (worldY.toLong() and 0xFFFFFFFFL)
|
||||
|
||||
private fun fluidmapToWorld() {
|
||||
for (y in fluidMap.indices) {
|
||||
for (x in fluidMap[0].indices) {
|
||||
world.setFluid(x + updateXFrom, y + updateYFrom, fluidNewTypeMap[y][x], fluidNewMap[y][x])
|
||||
val worldX = x + updateXFrom
|
||||
val worldY = y + updateYFrom
|
||||
val key = packFluidCoords(worldX, worldY)
|
||||
// Only write if this tile hasn't been processed yet (deduplication for overlapping regions)
|
||||
if (key !in processedFluidTiles) {
|
||||
world.setFluid(worldX, worldY, fluidNewTypeMap[y][x], fluidNewMap[y][x])
|
||||
processedFluidTiles.add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import net.torvald.terrarum.App
|
||||
import net.torvald.terrarum.Terrarum
|
||||
import net.torvald.terrarum.gameactors.AVKey
|
||||
import net.torvald.terrarum.gameactors.NoSerialise
|
||||
import net.torvald.terrarum.gameactors.WorldUpdater
|
||||
import net.torvald.terrarum.itemproperties.ItemRemapTable
|
||||
import net.torvald.terrarum.itemproperties.ItemTable
|
||||
import net.torvald.terrarum.spriteassembler.ADProperties
|
||||
@@ -20,7 +21,7 @@ import java.util.*
|
||||
* Created by minjaesong on 2015-12-31.
|
||||
*/
|
||||
|
||||
class IngamePlayer : ActorHumanoid, HasAssembledSprite, NoSerialise {
|
||||
class IngamePlayer : ActorHumanoid, HasAssembledSprite, NoSerialise, WorldUpdater {
|
||||
|
||||
val creationTime = App.getTIME_T()
|
||||
var lastPlayTime = App.getTIME_T() // cumulative value for the savegame
|
||||
|
||||
Reference in New Issue
Block a user