mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 12:21:52 +09:00
wire sim refactor
This commit is contained in:
@@ -97,7 +97,8 @@ class WireActor : ActorWithBody, NoSerialise, InternalActor {
|
||||
|
||||
// signal wires?
|
||||
if (WireCodex.wireProps[wireID]?.accepts == "digital_bit") {
|
||||
val strength = world?.getWireEmitStateOf(worldX, worldY, wireID)?.x ?: 0.0
|
||||
// Use parametric brightness from logical wire graph for efficiency
|
||||
val strength = world?.getWireBrightness(worldX, worldY, wireID) ?: 0.0
|
||||
|
||||
// draw base (unlit) sprite
|
||||
batch.color = Color.WHITE
|
||||
|
||||
@@ -122,6 +122,13 @@ open class GameWorld(
|
||||
public val wirings = HashedWirings()
|
||||
private val wiringGraph = HashedWiringGraph()
|
||||
|
||||
/**
|
||||
* Logical wire graph for efficient signal propagation.
|
||||
* Contains coarse graph with logical nodes (fixtures, junctions) and segments.
|
||||
* Rebuilt when wires are placed/removed; used for signal simulation and brightness lookup.
|
||||
*/
|
||||
val logicalWireGraph: LogicalWireGraph by lazy { LogicalWireGraph(this) }
|
||||
|
||||
@Transient private val WIRE_POS_MAP = intArrayOf(1,2,4,8)
|
||||
@Transient private val WIRE_ANTIPOS_MAP = intArrayOf(4,8,1,2)
|
||||
|
||||
@@ -540,6 +547,10 @@ open class GameWorld(
|
||||
|
||||
// scratch-that-i'll-figure-it-out wire placement
|
||||
setWireGraphOfUnsafe(blockAddr, tile, connection)
|
||||
|
||||
// Note: Do NOT rebuild logical wire graph here - connectivity is set up
|
||||
// separately by BlockBase.setConnectivity() AFTER this call.
|
||||
// The graph is rebuilt at the end of wire placement in BlockBase.kt
|
||||
}
|
||||
|
||||
fun setTileOnLayerUnsafe(layer: Int, x: Int, y: Int, tile: Int) {
|
||||
@@ -569,6 +580,9 @@ open class GameWorld(
|
||||
// remove wire from this tile
|
||||
wiringGraph[blockAddr]!!.remove(tile)
|
||||
wirings[blockAddr]!!.ws.remove(tile)
|
||||
|
||||
// Rebuild logical wire graph for this wire type
|
||||
logicalWireGraph.rebuild(tile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,6 +600,9 @@ open class GameWorld(
|
||||
// remove wire from this tile
|
||||
wiringGraph[blockAddr]!!.remove(tile)
|
||||
wirings[blockAddr]!!.ws.remove(tile)
|
||||
|
||||
// Rebuild logical wire graph for this wire type
|
||||
logicalWireGraph.rebuild(tile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,6 +626,27 @@ open class GameWorld(
|
||||
return wiringGraph[blockAddr]?.get(itemID)?.emt
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wire brightness using parametric evaluation from the logical wire graph.
|
||||
* This is more efficient than per-tile signal storage as it derives brightness
|
||||
* from segment properties rather than storing it per-tile.
|
||||
*
|
||||
* @param x World tile X coordinate
|
||||
* @param y World tile Y coordinate
|
||||
* @param wireType The wire item ID
|
||||
* @return Signal strength at this position (0.0 to 1.0+), or 0.0 if no wire/signal
|
||||
*/
|
||||
fun getWireBrightness(x: Int, y: Int, wireType: ItemID): Double {
|
||||
val (cx, cy) = coerceXY(x, y)
|
||||
val blockAddr = LandUtil.getBlockAddr(this, cx, cy)
|
||||
|
||||
val segment = logicalWireGraph.getGraph(wireType)
|
||||
?.positionToSegment?.get(blockAddr) ?: return 0.0
|
||||
|
||||
val offset = segment.getOffsetForPosition(Point2i(cx, cy)) ?: return 0.0
|
||||
return segment.getBrightnessAtOffset(offset)
|
||||
}
|
||||
|
||||
fun getWireReceptionStateOf(x: Int, y: Int, itemID: ItemID): ArrayList<WireReceptionState>? {
|
||||
val (x, y) = coerceXY(x, y)
|
||||
val blockAddr = LandUtil.getBlockAddr(this, x, y)
|
||||
|
||||
491
src/net/torvald/terrarum/gameworld/LogicalWireGraph.kt
Normal file
491
src/net/torvald/terrarum/gameworld/LogicalWireGraph.kt
Normal file
@@ -0,0 +1,491 @@
|
||||
package net.torvald.terrarum.gameworld
|
||||
|
||||
import net.torvald.terrarum.INGAME
|
||||
import net.torvald.terrarum.Point2i
|
||||
import net.torvald.terrarum.WireCodex
|
||||
import net.torvald.terrarum.gameitems.ItemID
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame.Companion.inUpdateRange
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.Electric
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.WireEmissionType
|
||||
import net.torvald.terrarum.realestate.LandUtil
|
||||
import org.dyn4j.geometry.Vector2
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Two-layer wire simulation model: Logical graph for signal propagation.
|
||||
*
|
||||
* The logical graph separates "what changes per tile" (visual brightness) from
|
||||
* "what doesn't" (connectivity topology, signal evaluation). This reduces
|
||||
* simulation complexity from O(wire_tiles) to O(logical_nodes).
|
||||
*
|
||||
* Logical nodes are:
|
||||
* - Signal sources (fixtures with wireEmitterTypes)
|
||||
* - Signal sinks (fixtures with wireSinkTypes)
|
||||
* - Junctions (wire tiles with 3+ connections)
|
||||
*
|
||||
* Wire segments connect logical nodes. Signal propagates along segments with
|
||||
* decay calculated per-segment, not per-tile.
|
||||
*
|
||||
* Created for Terrarum wire simulation refactoring.
|
||||
*
|
||||
* Created by minjaesong and Claude on 2026-01-08.
|
||||
*/
|
||||
|
||||
typealias BlockBoxIndex = Int
|
||||
|
||||
/**
|
||||
* Represents a node in the logical wire graph.
|
||||
* Nodes are: signal sources, signal sinks, and junctions (3+ connections).
|
||||
*/
|
||||
sealed class LogicalWireNode {
|
||||
abstract val position: Point2i
|
||||
abstract val wireType: ItemID
|
||||
abstract var signalStrength: Vector2
|
||||
|
||||
/** Connected segments (populated during graph building) */
|
||||
@Transient
|
||||
val connectedSegments: MutableList<WireSegment> = mutableListOf()
|
||||
|
||||
/**
|
||||
* Fixture-backed node (source or sink).
|
||||
* References the Electric fixture that emits or receives signals.
|
||||
*/
|
||||
data class FixtureNode(
|
||||
override val position: Point2i,
|
||||
override val wireType: ItemID,
|
||||
val fixtureRef: Electric,
|
||||
val blockBoxIndex: BlockBoxIndex,
|
||||
val isEmitter: Boolean,
|
||||
val emissionType: WireEmissionType
|
||||
) : LogicalWireNode() {
|
||||
override var signalStrength: Vector2 = Vector2(0.0, 0.0)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is FixtureNode) return false
|
||||
return position == other.position && wireType == other.wireType &&
|
||||
blockBoxIndex == other.blockBoxIndex && isEmitter == other.isEmitter
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = position.hashCode()
|
||||
result = 31 * result + wireType.hashCode()
|
||||
result = 31 * result + blockBoxIndex
|
||||
result = 31 * result + isEmitter.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Junction node where 3+ wire segments meet.
|
||||
* Acts as a signal relay point with potential splitting/merging.
|
||||
*/
|
||||
data class JunctionNode(
|
||||
override val position: Point2i,
|
||||
override val wireType: ItemID,
|
||||
val connectionCount: Int // 3 for T-junction, 4 for cross
|
||||
) : LogicalWireNode() {
|
||||
override var signalStrength: Vector2 = Vector2(0.0, 0.0)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is JunctionNode) return false
|
||||
return position == other.position && wireType == other.wireType
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = position.hashCode()
|
||||
result = 31 * result + wireType.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a wire segment connecting two logical nodes.
|
||||
* Signal decays along the segment based on length.
|
||||
*
|
||||
* Replaces N tile nodes with a single edge, enabling O(1) signal propagation
|
||||
* across an entire wire run.
|
||||
*/
|
||||
data class WireSegment(
|
||||
val wireType: ItemID,
|
||||
var startNode: LogicalWireNode,
|
||||
var endNode: LogicalWireNode,
|
||||
val length: Int,
|
||||
val tilePositions: List<Point2i>,
|
||||
var startStrength: Double = 0.0,
|
||||
var endStrength: Double = 0.0,
|
||||
val decayConstant: Double = 1.0
|
||||
) {
|
||||
/**
|
||||
* Parametric brightness evaluation - O(1) per tile.
|
||||
* @param offset Distance from start node (0 to length)
|
||||
* @return Signal strength at that offset
|
||||
*/
|
||||
fun getBrightnessAtOffset(offset: Int): Double {
|
||||
if (offset < 0 || offset > length) return 0.0
|
||||
// Calculate from whichever end has the stronger signal
|
||||
val fromStart = startStrength * decayConstant.pow(offset.toDouble())
|
||||
val fromEnd = endStrength * decayConstant.pow((length - offset).toDouble())
|
||||
return maxOf(fromStart, fromEnd).coerceAtLeast(0.0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the offset of a tile position within this segment.
|
||||
* @return Offset index, or null if position not in segment
|
||||
*/
|
||||
fun getOffsetForPosition(pos: Point2i): Int? {
|
||||
val idx = tilePositions.indexOfFirst { it.x == pos.x && it.y == pos.y }
|
||||
return if (idx >= 0) idx else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node at the other end of this segment.
|
||||
*/
|
||||
fun getOtherEnd(node: LogicalWireNode): LogicalWireNode {
|
||||
return if (node === startNode || node == startNode) endNode else startNode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete logical wire graph for a world.
|
||||
* Stored per wire-type for efficiency.
|
||||
*/
|
||||
class LogicalWireGraph(private val world: GameWorld) {
|
||||
|
||||
companion object {
|
||||
private const val RIGHT = 1
|
||||
private const val DOWN = 2
|
||||
private const val LEFT = 4
|
||||
private const val UP = 8
|
||||
|
||||
private val DIRECTIONS = intArrayOf(RIGHT, DOWN, LEFT, UP)
|
||||
private val DIRECTION_OFFSETS = mapOf(
|
||||
RIGHT to Point2i(1, 0),
|
||||
DOWN to Point2i(0, 1),
|
||||
LEFT to Point2i(-1, 0),
|
||||
UP to Point2i(0, -1)
|
||||
)
|
||||
|
||||
private fun Int.wireNodeMirror(): Int = when (this) {
|
||||
RIGHT -> LEFT
|
||||
DOWN -> UP
|
||||
LEFT -> RIGHT
|
||||
UP -> DOWN
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graph data for a single wire type.
|
||||
*/
|
||||
data class WireTypeGraph(
|
||||
val nodes: MutableList<LogicalWireNode> = mutableListOf(),
|
||||
val segments: MutableList<WireSegment> = mutableListOf(),
|
||||
val positionToSegment: HashMap<BlockAddress, WireSegment> = HashMap(),
|
||||
val positionToNode: HashMap<BlockAddress, LogicalWireNode> = HashMap(),
|
||||
var dirty: Boolean = true,
|
||||
var structureDirty: Boolean = true // Set when fixtures are added/removed; requires full rebuild
|
||||
) {
|
||||
fun clear() {
|
||||
nodes.forEach { it.connectedSegments.clear() }
|
||||
nodes.clear()
|
||||
segments.clear()
|
||||
positionToSegment.clear()
|
||||
positionToNode.clear()
|
||||
dirty = true
|
||||
structureDirty = false // We're about to rebuild, so structure will be fresh
|
||||
}
|
||||
}
|
||||
|
||||
private val graphs = HashMap<ItemID, WireTypeGraph>()
|
||||
|
||||
fun getGraph(wireType: ItemID): WireTypeGraph? = graphs[wireType]
|
||||
|
||||
fun getOrCreateGraph(wireType: ItemID): WireTypeGraph {
|
||||
return graphs.getOrPut(wireType) { WireTypeGraph() }
|
||||
}
|
||||
|
||||
fun markDirty(wireType: ItemID) {
|
||||
graphs[wireType]?.dirty = true
|
||||
}
|
||||
|
||||
fun markAllDirty() {
|
||||
graphs.values.forEach { it.dirty = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all graphs as needing structural rebuild.
|
||||
* Called when Electric fixtures are spawned or despawned.
|
||||
*/
|
||||
fun markAllStructureDirty() {
|
||||
graphs.values.forEach { it.structureDirty = true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the logical graph for a specific wire type.
|
||||
* Scans all wire tiles and fixtures to construct nodes and segments.
|
||||
*/
|
||||
fun rebuild(wireType: ItemID) {
|
||||
val graph = getOrCreateGraph(wireType)
|
||||
graph.clear()
|
||||
|
||||
val emissionType = WireCodex[wireType].accepts
|
||||
val decayConstant = WireCodex.wireDecays[wireType] ?: 1.0
|
||||
|
||||
// Step 1: Find all logical nodes
|
||||
|
||||
// 1a: Fixture nodes (emitters and sinks)
|
||||
INGAME.actorContainerActive.filterIsInstance<Electric>().forEach { fixture ->
|
||||
if (!fixture.inUpdateRange(world)) return@forEach
|
||||
|
||||
// Emitter ports
|
||||
fixture.wireEmitterTypes.forEach { (bbi, wireEmissionType) ->
|
||||
if (wireEmissionType == emissionType) {
|
||||
val pos = fixture.worldBlockPos!! + fixture.blockBoxIndexToPoint2i(bbi)
|
||||
// Only add if there's actually a wire at this position
|
||||
if (hasWireAt(pos.x, pos.y, wireType)) {
|
||||
val node = LogicalWireNode.FixtureNode(
|
||||
position = pos,
|
||||
wireType = wireType,
|
||||
fixtureRef = fixture,
|
||||
blockBoxIndex = bbi,
|
||||
isEmitter = true,
|
||||
emissionType = wireEmissionType
|
||||
)
|
||||
graph.nodes.add(node)
|
||||
graph.positionToNode[LandUtil.getBlockAddr(world, pos.x, pos.y)] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sink ports
|
||||
fixture.wireSinkTypes.forEach { (bbi, wireSinkType) ->
|
||||
if (wireSinkType == emissionType) {
|
||||
val pos = fixture.worldBlockPos!! + fixture.blockBoxIndexToPoint2i(bbi)
|
||||
if (hasWireAt(pos.x, pos.y, wireType)) {
|
||||
val blockAddr = LandUtil.getBlockAddr(world, pos.x, pos.y)
|
||||
// Don't add duplicate node if already an emitter at same position
|
||||
if (!graph.positionToNode.containsKey(blockAddr)) {
|
||||
val node = LogicalWireNode.FixtureNode(
|
||||
position = pos,
|
||||
wireType = wireType,
|
||||
fixtureRef = fixture,
|
||||
blockBoxIndex = bbi,
|
||||
isEmitter = false,
|
||||
emissionType = wireSinkType
|
||||
)
|
||||
graph.nodes.add(node)
|
||||
graph.positionToNode[blockAddr] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1b: Junction nodes (tiles with 3+ connections)
|
||||
world.wirings.forEach { (blockAddr, wiringNode) ->
|
||||
if (wiringNode.ws.contains(wireType)) {
|
||||
val cnx = world.getWireGraphUnsafe(blockAddr, wireType) ?: 0
|
||||
val connectionCount = Integer.bitCount(cnx)
|
||||
|
||||
if (connectionCount >= 3) {
|
||||
val x = (blockAddr % world.width).toInt()
|
||||
val y = (blockAddr / world.width).toInt()
|
||||
|
||||
// Don't create junction if there's already a fixture node here
|
||||
if (!graph.positionToNode.containsKey(blockAddr)) {
|
||||
val node = LogicalWireNode.JunctionNode(
|
||||
position = Point2i(x, y),
|
||||
wireType = wireType,
|
||||
connectionCount = connectionCount
|
||||
)
|
||||
graph.nodes.add(node)
|
||||
graph.positionToNode[blockAddr] = node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Trace segments between nodes
|
||||
val visitedEdges = HashSet<Pair<BlockAddress, Int>>() // (position, direction)
|
||||
|
||||
graph.nodes.forEach { startNode ->
|
||||
val startAddr = LandUtil.getBlockAddr(world, startNode.position.x, startNode.position.y)
|
||||
val cnx = world.getWireGraphUnsafe(startAddr, wireType) ?: 0
|
||||
|
||||
for (dir in DIRECTIONS) {
|
||||
if (cnx and dir != 0) {
|
||||
val edgeKey = startAddr to dir
|
||||
if (visitedEdges.contains(edgeKey)) continue
|
||||
|
||||
// Trace path in this direction
|
||||
val segment = tracePath(startNode, dir, wireType, decayConstant, graph.positionToNode)
|
||||
|
||||
if (segment != null) {
|
||||
graph.segments.add(segment)
|
||||
startNode.connectedSegments.add(segment)
|
||||
segment.endNode.connectedSegments.add(segment)
|
||||
|
||||
// Mark all tiles in segment
|
||||
segment.tilePositions.forEach { pos ->
|
||||
val addr = LandUtil.getBlockAddr(world, pos.x, pos.y)
|
||||
graph.positionToSegment[addr] = segment
|
||||
}
|
||||
|
||||
// Mark both directions as visited
|
||||
val endAddr = LandUtil.getBlockAddr(world, segment.endNode.position.x, segment.endNode.position.y)
|
||||
visitedEdges.add(startAddr to dir)
|
||||
visitedEdges.add(endAddr to dir.wireNodeMirror())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle orphan wire tiles (connected to nothing) - create single-tile segments
|
||||
world.wirings.forEach { (blockAddr, wiringNode) ->
|
||||
if (wiringNode.ws.contains(wireType) && !graph.positionToSegment.containsKey(blockAddr)) {
|
||||
val x = (blockAddr % world.width).toInt()
|
||||
val y = (blockAddr / world.width).toInt()
|
||||
val pos = Point2i(x, y)
|
||||
|
||||
// Create a degenerate segment for orphan tiles
|
||||
val orphanNode = LogicalWireNode.JunctionNode(pos, wireType, 0)
|
||||
val segment = WireSegment(
|
||||
wireType = wireType,
|
||||
startNode = orphanNode,
|
||||
endNode = orphanNode,
|
||||
length = 0,
|
||||
tilePositions = listOf(pos),
|
||||
decayConstant = decayConstant
|
||||
)
|
||||
graph.segments.add(segment)
|
||||
graph.positionToSegment[blockAddr] = segment
|
||||
}
|
||||
}
|
||||
|
||||
graph.dirty = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Trace a wire path from a node in a given direction until hitting another node.
|
||||
*/
|
||||
private fun tracePath(
|
||||
startNode: LogicalWireNode,
|
||||
initialDirection: Int,
|
||||
wireType: ItemID,
|
||||
decayConstant: Double,
|
||||
positionToNode: HashMap<BlockAddress, LogicalWireNode>
|
||||
): WireSegment? {
|
||||
val path = mutableListOf<Point2i>()
|
||||
path.add(startNode.position)
|
||||
|
||||
var currentPos = startNode.position
|
||||
var direction = initialDirection
|
||||
|
||||
// Move to first tile in the direction
|
||||
val offset = DIRECTION_OFFSETS[direction] ?: return null
|
||||
currentPos = Point2i(currentPos.x + offset.x, currentPos.y + offset.y)
|
||||
|
||||
val maxIterations = 100000 // Safety limit
|
||||
var iterations = 0
|
||||
|
||||
while (iterations++ < maxIterations) {
|
||||
val blockAddr = LandUtil.getBlockAddr(world, currentPos.x, currentPos.y)
|
||||
|
||||
// Check if we've reached another logical node
|
||||
val endNode = positionToNode[blockAddr]
|
||||
if (endNode != null && endNode !== startNode) {
|
||||
path.add(currentPos)
|
||||
return WireSegment(
|
||||
wireType = wireType,
|
||||
startNode = startNode,
|
||||
endNode = endNode,
|
||||
length = path.size - 1, // Length is number of edges, not vertices
|
||||
tilePositions = path.toList(),
|
||||
decayConstant = decayConstant
|
||||
)
|
||||
}
|
||||
|
||||
// Check if wire exists here
|
||||
val cnx = world.getWireGraphUnsafe(blockAddr, wireType)
|
||||
if (cnx == null || cnx == 0) {
|
||||
// Dead end - create endpoint node
|
||||
val deadEndNode = LogicalWireNode.JunctionNode(
|
||||
position = Point2i(currentPos.x, currentPos.y),
|
||||
wireType = wireType,
|
||||
connectionCount = 1
|
||||
)
|
||||
// Note: we don't add dead end nodes to the main node list
|
||||
return WireSegment(
|
||||
wireType = wireType,
|
||||
startNode = startNode,
|
||||
endNode = deadEndNode,
|
||||
length = path.size,
|
||||
tilePositions = path.toList() + listOf(currentPos),
|
||||
decayConstant = decayConstant
|
||||
)
|
||||
}
|
||||
|
||||
path.add(currentPos)
|
||||
|
||||
// Find the exit direction (not the direction we came from)
|
||||
val entryDir = direction.wireNodeMirror()
|
||||
var exitDir: Int? = null
|
||||
|
||||
for (dir in DIRECTIONS) {
|
||||
if (dir != entryDir && (cnx and dir) != 0) {
|
||||
// Check if target is connected back
|
||||
val nextOffset = DIRECTION_OFFSETS[dir] ?: continue
|
||||
val nextPos = Point2i(currentPos.x + nextOffset.x, currentPos.y + nextOffset.y)
|
||||
val nextCnx = world.getWireGraphOf(nextPos.x, nextPos.y, wireType) ?: 0
|
||||
|
||||
if ((nextCnx and dir.wireNodeMirror()) != 0) {
|
||||
exitDir = dir
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exitDir == null) {
|
||||
// Dead end
|
||||
return WireSegment(
|
||||
wireType = wireType,
|
||||
startNode = startNode,
|
||||
endNode = LogicalWireNode.JunctionNode(currentPos, wireType, 1),
|
||||
length = path.size - 1,
|
||||
tilePositions = path.toList(),
|
||||
decayConstant = decayConstant
|
||||
)
|
||||
}
|
||||
|
||||
// Move to next tile
|
||||
val nextOffset = DIRECTION_OFFSETS[exitDir]!!
|
||||
currentPos = Point2i(currentPos.x + nextOffset.x, currentPos.y + nextOffset.y)
|
||||
direction = exitDir
|
||||
}
|
||||
|
||||
return null // Safety: exceeded max iterations
|
||||
}
|
||||
|
||||
private fun hasWireAt(x: Int, y: Int, wireType: ItemID): Boolean {
|
||||
val blockAddr = LandUtil.getBlockAddr(world, x, y)
|
||||
return world.wirings[blockAddr]?.ws?.contains(wireType) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild graphs for all wire types present in the world.
|
||||
*/
|
||||
fun rebuildAll() {
|
||||
// Collect all wire types
|
||||
val wireTypes = HashSet<ItemID>()
|
||||
world.wirings.values.forEach { wiringNode ->
|
||||
wireTypes.addAll(wiringNode.ws)
|
||||
}
|
||||
|
||||
wireTypes.forEach { wireType ->
|
||||
rebuild(wireType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import net.torvald.terrarum.gameactors.Controllable
|
||||
import net.torvald.terrarum.gameitems.ItemID
|
||||
import net.torvald.terrarum.gameworld.GameWorld
|
||||
import net.torvald.terrarum.gameworld.GameWorld.Companion.FLUID
|
||||
import net.torvald.terrarum.gameworld.LogicalWireNode
|
||||
import net.torvald.terrarum.gameworld.WireSegment
|
||||
import net.torvald.terrarum.modulebasegame.TerrarumIngame.Companion.inUpdateRange
|
||||
import net.torvald.terrarum.modulebasegame.gameactors.*
|
||||
import net.torvald.terrarum.modulebasegame.gameitems.AxeCore
|
||||
@@ -525,37 +527,155 @@ object WorldSimulator {
|
||||
it.inUpdateRange(world) && it.wireEmitterTypes.isNotEmpty()
|
||||
}
|
||||
|
||||
// Keep for backwards compatibility - used by oldTraversedNodes cleanup
|
||||
private val wireSimMarked = HashSet<Long>()
|
||||
private val wireSimPoints = Queue<WireGraphCursor>()
|
||||
private val oldTraversedNodes = ArrayList<WireGraphCursor>()
|
||||
private val fixtureCache = HashMap<Point2i, Pair<Electric, WireEmissionType>>() // also instance of Electric
|
||||
|
||||
/**
|
||||
* Simulates wire signal propagation using the two-layer logical wire graph.
|
||||
*
|
||||
* This implementation operates on logical nodes (fixtures, junctions) and segments
|
||||
* rather than individual tiles, reducing complexity from O(wire_tiles) to O(logical_nodes).
|
||||
*
|
||||
* Signal strength is calculated per-segment and stored for parametric brightness evaluation.
|
||||
* Per-tile emission states are also updated for backwards compatibility with existing code.
|
||||
*/
|
||||
private fun simulateWires(delta: Float) {
|
||||
// unset old wires before we begin
|
||||
// Clear old per-tile emission states for backwards compatibility
|
||||
oldTraversedNodes.forEach { (x, y, _, _, wire) ->
|
||||
world.getAllWiringGraph(x, y)?.get(wire)?.emt?.set(0.0, 0.0)
|
||||
}
|
||||
|
||||
oldTraversedNodes.clear()
|
||||
fixtureCache.clear()
|
||||
|
||||
wiresimGetSourceBlocks().let { sources ->
|
||||
// signal-emitting fixtures must set emitState of its own tiles via update()
|
||||
sources.forEach {
|
||||
it.wireEmitterTypes.forEach { (bbi, wireType) ->
|
||||
|
||||
val startingPoint = it.worldBlockPos!! + it.blockBoxIndexToPoint2i(bbi)
|
||||
val signal = it.wireEmission[bbi] ?: Vector2(0.0, 0.0)
|
||||
|
||||
world.getAllWiringGraph(startingPoint.x, startingPoint.y)?.keys?.filter { WireCodex[it].accepts == wireType }?.forEach { wire ->
|
||||
val simStartingPoint = WireGraphCursor(startingPoint, wire)
|
||||
wireSimMarked.clear()
|
||||
wireSimPoints.clear()
|
||||
traverseWireGraph(world, wire, simStartingPoint, signal, wireType)
|
||||
// Collect all wire types that have active emitters
|
||||
val activeWireTypes = HashSet<ItemID>()
|
||||
wiresimGetSourceBlocks().forEach { fixture ->
|
||||
fixture.wireEmitterTypes.forEach { (bbi, wireEmissionType) ->
|
||||
val pos = fixture.worldBlockPos!! + fixture.blockBoxIndexToPoint2i(bbi)
|
||||
world.getAllWiringGraph(pos.x, pos.y)?.keys?.forEach { wireType ->
|
||||
if (WireCodex[wireType].accepts == wireEmissionType) {
|
||||
activeWireTypes.add(wireType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process each wire type
|
||||
activeWireTypes.forEach { wireType ->
|
||||
simulateWireType(wireType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate signal propagation for a specific wire type using the logical graph.
|
||||
*/
|
||||
private fun simulateWireType(wireType: ItemID) {
|
||||
val graph = world.logicalWireGraph.getGraph(wireType)
|
||||
|
||||
// Rebuild graph if it doesn't exist, is empty, or structure is dirty (fixtures added/removed)
|
||||
if (graph == null || graph.nodes.isEmpty() || graph.structureDirty) {
|
||||
world.logicalWireGraph.rebuild(wireType)
|
||||
}
|
||||
|
||||
val currentGraph = world.logicalWireGraph.getGraph(wireType) ?: return
|
||||
if (currentGraph.nodes.isEmpty()) return
|
||||
|
||||
val decayConstant = WireCodex.wireDecays[wireType] ?: 1.0
|
||||
val emissionType = WireCodex[wireType].accepts
|
||||
|
||||
// Step 1: Reset all node signal strengths and segment strengths
|
||||
currentGraph.nodes.forEach { node ->
|
||||
node.signalStrength = Vector2(0.0, 0.0)
|
||||
}
|
||||
currentGraph.segments.forEach { segment ->
|
||||
segment.startStrength = 0.0
|
||||
segment.endStrength = 0.0
|
||||
}
|
||||
|
||||
// Step 2: Get signals from emitter fixture nodes
|
||||
val emitterNodes = currentGraph.nodes.filterIsInstance<LogicalWireNode.FixtureNode>()
|
||||
.filter { it.isEmitter && it.fixtureRef.inUpdateRange(world) }
|
||||
|
||||
emitterNodes.forEach { emitterNode ->
|
||||
val signal = emitterNode.fixtureRef.wireEmission[emitterNode.blockBoxIndex] ?: Vector2(0.0, 0.0)
|
||||
emitterNode.signalStrength = signal.copy()
|
||||
}
|
||||
|
||||
// Step 3: BFS propagation on logical graph
|
||||
val visited = HashSet<LogicalWireNode>()
|
||||
val queue = ArrayDeque<LogicalWireNode>()
|
||||
|
||||
// Start from all emitter nodes
|
||||
emitterNodes.forEach { node ->
|
||||
if (node.signalStrength.x > 0.0 || node.signalStrength.y > 0.0) {
|
||||
queue.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.isNotEmpty()) {
|
||||
val node = queue.removeFirst()
|
||||
if (visited.contains(node)) continue
|
||||
visited.add(node)
|
||||
|
||||
// Propagate through connected segments
|
||||
node.connectedSegments.forEach { segment ->
|
||||
val otherNode = segment.getOtherEnd(node)
|
||||
|
||||
// Calculate decayed signal at the other end
|
||||
val decayedSignal = node.signalStrength.copy()
|
||||
decayedSignal.x *= decayConstant.pow(segment.length.toDouble())
|
||||
decayedSignal.y *= decayConstant.pow(segment.length.toDouble())
|
||||
|
||||
// Update segment strengths for brightness calculation
|
||||
if (node === segment.startNode || node == segment.startNode) {
|
||||
segment.startStrength = maxOf(segment.startStrength, node.signalStrength.x)
|
||||
} else {
|
||||
segment.endStrength = maxOf(segment.endStrength, node.signalStrength.x)
|
||||
}
|
||||
|
||||
// Update other node with max signal (handles multiple paths)
|
||||
if (decayedSignal.x > otherNode.signalStrength.x) {
|
||||
otherNode.signalStrength.x = decayedSignal.x
|
||||
}
|
||||
if (decayedSignal.y > otherNode.signalStrength.y) {
|
||||
otherNode.signalStrength.y = decayedSignal.y
|
||||
}
|
||||
|
||||
// Add to queue for further propagation
|
||||
if (!visited.contains(otherNode)) {
|
||||
queue.add(otherNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Update per-tile emission states for backwards compatibility
|
||||
// This allows getWireEmitStateOf() to work alongside the new getWireBrightness()
|
||||
currentGraph.segments.forEach { segment ->
|
||||
segment.tilePositions.forEachIndexed { index, pos ->
|
||||
val brightness = segment.getBrightnessAtOffset(index)
|
||||
val emitState = Vector2(brightness, 0.0)
|
||||
world.setWireEmitStateOf(pos.x, pos.y, wireType, emitState)
|
||||
|
||||
// Track for cleanup on next tick
|
||||
oldTraversedNodes.add(WireGraphCursor(pos, wireType))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Notify sink fixtures
|
||||
currentGraph.nodes.filterIsInstance<LogicalWireNode.FixtureNode>()
|
||||
.filter { !it.isEmitter && it.fixtureRef.inUpdateRange(world) }
|
||||
.forEach { sinkNode ->
|
||||
if (sinkNode.signalStrength.x > 0.0 || sinkNode.signalStrength.y > 0.0) {
|
||||
val offsetX = sinkNode.blockBoxIndex % sinkNode.fixtureRef.blockBox.width
|
||||
val offsetY = sinkNode.blockBoxIndex / sinkNode.fixtureRef.blockBox.width
|
||||
sinkNode.fixtureRef.updateOnWireGraphTraversal(offsetX, offsetY, sinkNode.emissionType)
|
||||
}
|
||||
}
|
||||
|
||||
currentGraph.dirty = false
|
||||
}
|
||||
|
||||
private fun calculateDecay(signal: Vector2, dist: Int, wire: ItemID, signalType: WireEmissionType): Vector2 {
|
||||
@@ -563,7 +683,9 @@ object WorldSimulator {
|
||||
return signal * d.pow(dist.toDouble())
|
||||
}
|
||||
|
||||
private fun traverseWireGraph(world: GameWorld, wire: ItemID, startingPoint: WireGraphCursor, signal: Vector2, signalType: WireEmissionType) {
|
||||
// Keep old traversal method for reference/fallback (can be removed later)
|
||||
@Deprecated("Use simulateWireType() with logical graph instead")
|
||||
private fun traverseWireGraphLegacy(world: GameWorld, wire: ItemID, startingPoint: WireGraphCursor, signal: Vector2, signalType: WireEmissionType) {
|
||||
|
||||
val emissionType = WireCodex[wire].accepts
|
||||
|
||||
|
||||
@@ -57,6 +57,30 @@ open class Electric : FixtureBase {
|
||||
const val ELECTRIC_EPSILON_GENERIC = 1.0 / 1024.0
|
||||
}
|
||||
|
||||
/**
|
||||
* When an Electric fixture is spawned, mark all wire graphs as structurally dirty
|
||||
* so they rebuild to include this fixture's emitter/sink nodes.
|
||||
*/
|
||||
override fun onSpawn(tx: Int, ty: Int) {
|
||||
super.onSpawn(tx, ty)
|
||||
// Mark wire graphs as needing structural rebuild to include this fixture
|
||||
if (wireEmitterTypes.isNotEmpty() || wireSinkTypes.isNotEmpty()) {
|
||||
INGAME.world.logicalWireGraph.markAllStructureDirty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When an Electric fixture is despawned, mark all wire graphs as structurally dirty
|
||||
* so they rebuild without this fixture's nodes.
|
||||
*/
|
||||
override fun despawn() {
|
||||
// Mark wire graphs as needing structural rebuild before this fixture is removed
|
||||
if (wireEmitterTypes.isNotEmpty() || wireSinkTypes.isNotEmpty()) {
|
||||
INGAME.world.logicalWireGraph.markAllStructureDirty()
|
||||
}
|
||||
super.despawn()
|
||||
}
|
||||
|
||||
fun getWireEmitterAt(blockBoxIndex: BlockBoxIndex) = this.wireEmitterTypes[blockBoxIndex]
|
||||
fun getWireEmitterAt(point: Point2i) = this.wireEmitterTypes[pointToBlockBoxIndex(point)]
|
||||
fun getWireEmitterAt(x: Int, y: Int) = this.wireEmitterTypes[pointToBlockBoxIndex(x, y)]
|
||||
|
||||
@@ -236,6 +236,11 @@ object BlockBase {
|
||||
oldTileX = mtx
|
||||
oldTileY = mty
|
||||
|
||||
// Rebuild logical wire graph after connectivity is fully established
|
||||
if (ret >= 0) {
|
||||
ingame.world.logicalWireGraph.rebuild(itemID)
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user