mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-12 19:44:05 +09:00
wire sim refactor
This commit is contained in:
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="999" />
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
|
|||||||
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/Terrarum.wiki" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -97,7 +97,8 @@ class WireActor : ActorWithBody, NoSerialise, InternalActor {
|
|||||||
|
|
||||||
// signal wires?
|
// signal wires?
|
||||||
if (WireCodex.wireProps[wireID]?.accepts == "digital_bit") {
|
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
|
// draw base (unlit) sprite
|
||||||
batch.color = Color.WHITE
|
batch.color = Color.WHITE
|
||||||
|
|||||||
@@ -122,6 +122,13 @@ open class GameWorld(
|
|||||||
public val wirings = HashedWirings()
|
public val wirings = HashedWirings()
|
||||||
private val wiringGraph = HashedWiringGraph()
|
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_POS_MAP = intArrayOf(1,2,4,8)
|
||||||
@Transient private val WIRE_ANTIPOS_MAP = intArrayOf(4,8,1,2)
|
@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
|
// scratch-that-i'll-figure-it-out wire placement
|
||||||
setWireGraphOfUnsafe(blockAddr, tile, connection)
|
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) {
|
fun setTileOnLayerUnsafe(layer: Int, x: Int, y: Int, tile: Int) {
|
||||||
@@ -569,6 +580,9 @@ open class GameWorld(
|
|||||||
// remove wire from this tile
|
// remove wire from this tile
|
||||||
wiringGraph[blockAddr]!!.remove(tile)
|
wiringGraph[blockAddr]!!.remove(tile)
|
||||||
wirings[blockAddr]!!.ws.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
|
// remove wire from this tile
|
||||||
wiringGraph[blockAddr]!!.remove(tile)
|
wiringGraph[blockAddr]!!.remove(tile)
|
||||||
wirings[blockAddr]!!.ws.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
|
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>? {
|
fun getWireReceptionStateOf(x: Int, y: Int, itemID: ItemID): ArrayList<WireReceptionState>? {
|
||||||
val (x, y) = coerceXY(x, y)
|
val (x, y) = coerceXY(x, y)
|
||||||
val blockAddr = LandUtil.getBlockAddr(this, 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.gameitems.ItemID
|
||||||
import net.torvald.terrarum.gameworld.GameWorld
|
import net.torvald.terrarum.gameworld.GameWorld
|
||||||
import net.torvald.terrarum.gameworld.GameWorld.Companion.FLUID
|
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.TerrarumIngame.Companion.inUpdateRange
|
||||||
import net.torvald.terrarum.modulebasegame.gameactors.*
|
import net.torvald.terrarum.modulebasegame.gameactors.*
|
||||||
import net.torvald.terrarum.modulebasegame.gameitems.AxeCore
|
import net.torvald.terrarum.modulebasegame.gameitems.AxeCore
|
||||||
@@ -525,37 +527,155 @@ object WorldSimulator {
|
|||||||
it.inUpdateRange(world) && it.wireEmitterTypes.isNotEmpty()
|
it.inUpdateRange(world) && it.wireEmitterTypes.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep for backwards compatibility - used by oldTraversedNodes cleanup
|
||||||
private val wireSimMarked = HashSet<Long>()
|
private val wireSimMarked = HashSet<Long>()
|
||||||
private val wireSimPoints = Queue<WireGraphCursor>()
|
private val wireSimPoints = Queue<WireGraphCursor>()
|
||||||
private val oldTraversedNodes = ArrayList<WireGraphCursor>()
|
private val oldTraversedNodes = ArrayList<WireGraphCursor>()
|
||||||
private val fixtureCache = HashMap<Point2i, Pair<Electric, WireEmissionType>>() // also instance of Electric
|
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) {
|
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) ->
|
oldTraversedNodes.forEach { (x, y, _, _, wire) ->
|
||||||
world.getAllWiringGraph(x, y)?.get(wire)?.emt?.set(0.0, 0.0)
|
world.getAllWiringGraph(x, y)?.get(wire)?.emt?.set(0.0, 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
oldTraversedNodes.clear()
|
oldTraversedNodes.clear()
|
||||||
fixtureCache.clear()
|
fixtureCache.clear()
|
||||||
|
|
||||||
wiresimGetSourceBlocks().let { sources ->
|
// Collect all wire types that have active emitters
|
||||||
// signal-emitting fixtures must set emitState of its own tiles via update()
|
val activeWireTypes = HashSet<ItemID>()
|
||||||
sources.forEach {
|
wiresimGetSourceBlocks().forEach { fixture ->
|
||||||
it.wireEmitterTypes.forEach { (bbi, wireType) ->
|
fixture.wireEmitterTypes.forEach { (bbi, wireEmissionType) ->
|
||||||
|
val pos = fixture.worldBlockPos!! + fixture.blockBoxIndexToPoint2i(bbi)
|
||||||
val startingPoint = it.worldBlockPos!! + it.blockBoxIndexToPoint2i(bbi)
|
world.getAllWiringGraph(pos.x, pos.y)?.keys?.forEach { wireType ->
|
||||||
val signal = it.wireEmission[bbi] ?: Vector2(0.0, 0.0)
|
if (WireCodex[wireType].accepts == wireEmissionType) {
|
||||||
|
activeWireTypes.add(wireType)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
private fun calculateDecay(signal: Vector2, dist: Int, wire: ItemID, signalType: WireEmissionType): Vector2 {
|
||||||
@@ -563,7 +683,9 @@ object WorldSimulator {
|
|||||||
return signal * d.pow(dist.toDouble())
|
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
|
val emissionType = WireCodex[wire].accepts
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,30 @@ open class Electric : FixtureBase {
|
|||||||
const val ELECTRIC_EPSILON_GENERIC = 1.0 / 1024.0
|
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(blockBoxIndex: BlockBoxIndex) = this.wireEmitterTypes[blockBoxIndex]
|
||||||
fun getWireEmitterAt(point: Point2i) = this.wireEmitterTypes[pointToBlockBoxIndex(point)]
|
fun getWireEmitterAt(point: Point2i) = this.wireEmitterTypes[pointToBlockBoxIndex(point)]
|
||||||
fun getWireEmitterAt(x: Int, y: Int) = this.wireEmitterTypes[pointToBlockBoxIndex(x, y)]
|
fun getWireEmitterAt(x: Int, y: Int) = this.wireEmitterTypes[pointToBlockBoxIndex(x, y)]
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ object BlockBase {
|
|||||||
oldTileX = mtx
|
oldTileX = mtx
|
||||||
oldTileY = mty
|
oldTileY = mty
|
||||||
|
|
||||||
|
// Rebuild logical wire graph after connectivity is fully established
|
||||||
|
if (ret >= 0) {
|
||||||
|
ingame.world.logicalWireGraph.rebuild(itemID)
|
||||||
|
}
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
200
work_files/latency_simulator_storage_device.kts
Normal file
200
work_files/latency_simulator_storage_device.kts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
|
||||||
|
object Random {
|
||||||
|
fun uniformRand(low: Int, high: Int) = (Math.random() * (high + 1)).toInt()
|
||||||
|
|
||||||
|
fun triangularRand(low: Float, high: Float): Float {
|
||||||
|
val a = (Math.random() + Math.random()) / 2.0
|
||||||
|
return ((high - low) * a + low).toFloat()
|
||||||
|
}
|
||||||
|
fun gaussianRand(avg: Float, stddev: Float): Float {
|
||||||
|
// Box-Muller transform to generate random numbers with standard normal distribution
|
||||||
|
// This implementation uses the polar form for better efficiency
|
||||||
|
|
||||||
|
// We need two uniform random values between 0 and 1
|
||||||
|
val random = kotlin.random.Random
|
||||||
|
|
||||||
|
// Using the polar form of the Box-Muller transformation
|
||||||
|
var u: Double
|
||||||
|
var v: Double
|
||||||
|
var s: Double
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Generate two uniform random numbers between -1 and 1
|
||||||
|
u = Math.random() * 2 - 1
|
||||||
|
v = Math.random() * 2 - 1
|
||||||
|
|
||||||
|
// Calculate sum of squares
|
||||||
|
s = u * u + v * v
|
||||||
|
} while (s >= 1 || s == 0.0)
|
||||||
|
|
||||||
|
// Calculate polar transformation
|
||||||
|
val multiplier = kotlin.math.sqrt(-2.0 * kotlin.math.ln(s) / s)
|
||||||
|
|
||||||
|
// Transform to the desired mean and standard deviation
|
||||||
|
// We only use one of the two generated values here
|
||||||
|
return (avg + stddev * u * multiplier).toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SeekSimulator {
|
||||||
|
|
||||||
|
abstract fun computeSeekTime(currentSector: Int, targetSector: Int): Float
|
||||||
|
|
||||||
|
class Tape(
|
||||||
|
val totalSectors: Int,
|
||||||
|
val tapeLengthMeters: Float = 200f,
|
||||||
|
val baseSeekTime: Float = 0.5f, // seconds base inertia
|
||||||
|
val tapeSpeedMetersPerSec: Float = 2.0f, // normal speed
|
||||||
|
) : SeekSimulator() {
|
||||||
|
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||||
|
val posCurrent = (currentSector.toFloat() / totalSectors) * tapeLengthMeters
|
||||||
|
val posTarget = (targetSector.toFloat() / totalSectors) * tapeLengthMeters
|
||||||
|
val distance = kotlin.math.abs(posTarget - posCurrent)
|
||||||
|
|
||||||
|
// Inject random tape jitter
|
||||||
|
val effectiveSpeed = tapeSpeedMetersPerSec * Random.triangularRand(0.9f, 1.1f)
|
||||||
|
|
||||||
|
return baseSeekTime + (distance / effectiveSpeed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Disc(
|
||||||
|
val totalTracks: Int,
|
||||||
|
val armSeekBaseTime: Float = 0.005f, // fast seek, seconds
|
||||||
|
val armSeekMultiplier: Float = 0.002f, // slower for bigger jumps
|
||||||
|
val rotationLatencyAvg: Float = 0.008f, // seconds (half-rotation average)
|
||||||
|
) : SeekSimulator() {
|
||||||
|
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||||
|
val cylCurrent = sectorToTrack(currentSector)
|
||||||
|
val cylTarget = sectorToTrack(targetSector)
|
||||||
|
val deltaTracks = kotlin.math.abs(cylTarget - cylCurrent)
|
||||||
|
|
||||||
|
val armSeek = armSeekBaseTime + (armSeekMultiplier * kotlin.math.sqrt(deltaTracks.toFloat()))
|
||||||
|
val rotationLatency = Random.gaussianRand(rotationLatencyAvg, rotationLatencyAvg * 0.2f)
|
||||||
|
|
||||||
|
return armSeek + rotationLatency
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sectorToTrack(sector: Int): Int {
|
||||||
|
// Simplistic assumption: sector layout maps 1:1 to track at this level
|
||||||
|
return sector % totalTracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Drum(
|
||||||
|
val rpm: Float = 3000f
|
||||||
|
) : SeekSimulator() {
|
||||||
|
override fun computeSeekTime(currentSector: Int, targetSector: Int): Float {
|
||||||
|
val degreesPerSector = 360.0f / 10000.0f // Assume 10k sectors per drum circumference
|
||||||
|
val angleCurrent = currentSector * degreesPerSector
|
||||||
|
val angleTarget = targetSector * degreesPerSector
|
||||||
|
val deltaAngle = kotlin.math.abs(angleTarget - angleCurrent) % 360f
|
||||||
|
|
||||||
|
val rotationLatencySeconds = (deltaAngle / 360f) * (60f / rpm)
|
||||||
|
|
||||||
|
// Add a little mechanical jitter
|
||||||
|
val jitteredLatency = rotationLatencySeconds * Random.triangularRand(0.95f, 1.05f)
|
||||||
|
|
||||||
|
return jitteredLatency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SeekLatencySampler(
|
||||||
|
val simulator: SeekSimulator,
|
||||||
|
val totalSectors: Int,
|
||||||
|
val sampleCount: Int = 10000
|
||||||
|
) {
|
||||||
|
data class Sample(val fromSector: Int, val toSector: Int, val latency: Float)
|
||||||
|
|
||||||
|
val samples = mutableListOf<Sample>()
|
||||||
|
|
||||||
|
fun runSampling() {
|
||||||
|
samples.clear()
|
||||||
|
var lastSector = Random.uniformRand(0, totalSectors - 1)
|
||||||
|
|
||||||
|
repeat(sampleCount) {
|
||||||
|
val nextSector = Random.uniformRand(0, totalSectors - 1)
|
||||||
|
val latency = simulator.computeSeekTime(lastSector, nextSector)
|
||||||
|
samples.add(Sample(lastSector, nextSector, latency))
|
||||||
|
lastSector = nextSector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun analyzeAndPrint() {
|
||||||
|
if (samples.isEmpty()) {
|
||||||
|
println("No samples generated. Run runSampling() first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val latencies = samples.map { it.latency }
|
||||||
|
val minLatency = latencies.minOrNull() ?: 0f
|
||||||
|
val maxLatency = latencies.maxOrNull() ?: 0f
|
||||||
|
val avgLatency = latencies.average().toFloat()
|
||||||
|
val stddevLatency = kotlin.math.sqrt(latencies.map { (it - avgLatency).let { diff -> diff * diff } }.average()).toFloat()
|
||||||
|
|
||||||
|
println("=== Seek Latency Stats ===")
|
||||||
|
println("Samples: $sampleCount")
|
||||||
|
println("Min: ${"%.4f".format(minLatency)} s")
|
||||||
|
println("Max: ${"%.4f".format(maxLatency)} s")
|
||||||
|
println("Avg: ${"%.4f".format(avgLatency)} s")
|
||||||
|
println("Stddev: ${"%.4f".format(stddevLatency)} s")
|
||||||
|
|
||||||
|
printSimpleHistogram(latencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun printSimpleHistogram(latencies: List<Float>, bins: Int = 30) {
|
||||||
|
val min = latencies.minOrNull() ?: return
|
||||||
|
val max = latencies.maxOrNull() ?: return
|
||||||
|
val binSize = (max - min) / bins
|
||||||
|
|
||||||
|
val histogram = IntArray(bins) { 0 }
|
||||||
|
|
||||||
|
latencies.forEach { latency ->
|
||||||
|
val bin = kotlin.math.min(((latency - min) / binSize).toInt(), bins - 1)
|
||||||
|
histogram[bin]++
|
||||||
|
}
|
||||||
|
|
||||||
|
println("--- Latency Distribution ---")
|
||||||
|
histogram.forEachIndexed { index, count ->
|
||||||
|
val lower = min + binSize * index
|
||||||
|
val upper = lower + binSize
|
||||||
|
val bar = "#".repeat(count / (sampleCount / 200)) // Scale bar length
|
||||||
|
println("${"%.4f".format(lower)} - ${"%.4f".format(upper)} s: $bar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val tapeSimulator = SeekSimulator.Tape(
|
||||||
|
totalSectors = 100000,
|
||||||
|
tapeLengthMeters = 200f,
|
||||||
|
baseSeekTime = 0.2f,
|
||||||
|
tapeSpeedMetersPerSec = 5.0f
|
||||||
|
)
|
||||||
|
|
||||||
|
val discSimulator = SeekSimulator.Disc(
|
||||||
|
totalTracks = 3810,
|
||||||
|
armSeekBaseTime = 0.005f,
|
||||||
|
armSeekMultiplier = 0.002f,
|
||||||
|
rotationLatencyAvg = 0.008f
|
||||||
|
)
|
||||||
|
|
||||||
|
val drumSimulator = SeekSimulator.Drum(
|
||||||
|
rpm = 3000f
|
||||||
|
)
|
||||||
|
|
||||||
|
listOf(tapeSimulator, discSimulator, drumSimulator).forEach { sim ->
|
||||||
|
SeekLatencySampler(
|
||||||
|
simulator = sim,
|
||||||
|
totalSectors = 100000,
|
||||||
|
sampleCount = 5000
|
||||||
|
).also {
|
||||||
|
it.runSampling()
|
||||||
|
it.analyzeAndPrint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user