From f89afb80bb01ef73102f4b0dfcb1e183a200c5e5 Mon Sep 17 00:00:00 2001 From: DBotThePony Date: Sat, 20 Apr 2024 21:31:26 +0700 Subject: [PATCH] Working wiring --- ADDITIONS.md | 6 + .../kstarbound/defs/UniverseServerConfig.kt | 2 + .../kstarbound/defs/dungeon/DungeonWorld.kt | 64 ++++++- .../dbotthepony/kstarbound/lua/Functions.kt | 10 ++ .../kstarbound/network/PacketRegistry.kt | 3 +- .../packets/serverbound/ConnectWirePacket.kt | 12 +- .../serverbound/DisconnectAllWiresPacket.kt | 29 +++ .../network/syncher/NetworkedList.kt | 4 +- .../server/world/LegacyWireProcessor.kt | 166 ++++++++++++++++++ .../kstarbound/server/world/ServerWorld.kt | 6 + .../world/entities/AbstractEntity.kt | 7 + .../world/entities/tile/ContainerObject.kt | 2 + .../entities/{wire => tile}/WireConnection.kt | 6 +- .../world/entities/tile/WireNode.kt | 24 +++ .../world/entities/tile/WorldObject.kt | 108 ++++++++++-- 15 files changed, 430 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt rename src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/{wire => tile}/WireConnection.kt (85%) create mode 100644 src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireNode.kt diff --git a/ADDITIONS.md b/ADDITIONS.md index b3ea88ee..9a734721 100644 --- a/ADDITIONS.md +++ b/ADDITIONS.md @@ -134,6 +134,12 @@ val color: TileColor = TileColor.DEFAULT --------------- +### universe_server.config + * Added `useNewWireProcessing`, which defaults to `true` + * New wire updating system is insanely fast (because wiring is updated along entity ticking, and doesn't involve intense entity map lookups) + * However, it is not a complete replacement for legacy system, because some mods might rely on fact that in legacy system when wired entities update, they load all other endpoints into memory (basically, chunkload all connected entities). In new system if wired entity references unloaded entities it simply does not update its state. + * If specified as `false`, original behavior will be restored, but beware of performance degradation! If you are a modder, **PLEASE** consider other ways around instead of enabling the old behavior, because performance cost of using old system is almost always gonna outweight "benefits" of chunkloaded wiring systems. + ### Worldgen * Major dungeon placement on planets is now deterministic * Container item population in dungeons is now deterministic and is based on dungeon seed diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt index 4477d219..68373311 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/UniverseServerConfig.kt @@ -13,6 +13,8 @@ data class UniverseServerConfig( val clockUpdatePacketInterval: Long = 500L, val findStarterWorldParameters: StarterWorld, val queuedFlightWaitTime: Double = 0.0, + + val useNewWireProcessing: Boolean = true, ) { @JsonFactory data class WorldPredicate( diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt index 12558727..81b12605 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/dungeon/DungeonWorld.kt @@ -18,6 +18,7 @@ import ru.dbotthepony.kstarbound.defs.tile.TileDefinition import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition import ru.dbotthepony.kstarbound.defs.tile.isNotEmptyTile import ru.dbotthepony.kstarbound.defs.world.FloatingDungeonWorldParameters +import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kstarbound.server.world.ServerChunk import ru.dbotthepony.kstarbound.server.world.ServerWorld import ru.dbotthepony.kstarbound.world.ChunkPos @@ -27,6 +28,7 @@ import ru.dbotthepony.kstarbound.world.api.AbstractLiquidState import ru.dbotthepony.kstarbound.world.api.MutableTileState import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.entities.tile.TileEntity +import ru.dbotthepony.kstarbound.world.entities.tile.WireConnection import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject import ru.dbotthepony.kstarbound.world.physics.Poly import java.util.Collections @@ -151,7 +153,7 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar private val openLocalWires = LinkedHashMap>() private val globalWires = LinkedHashMap>() - private val localWires = ArrayList>() + private val localWires = ArrayList>() private val placedObjects = LinkedHashMap() @@ -387,6 +389,49 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar chunk.setCell(pos - chunk.pos.tile, cell) } + private fun placeWires(group: Collection) { + val inbounds = HashSet>() + val outbounds = HashSet>() + + for (wirePos in group) { + var any = false + + parent.entityIndex.iterate(AABB.withSide(wirePos.toDoubleVector(), 16.0), { + if (it is WorldObject) { + for ((i, node) in it.inputNodes.withIndex()) { + if (node.position + it.tilePosition == wirePos) { + inbounds.add(it to WireConnection(it.tilePosition, i)) + any = true + } + } + + for ((i, node) in it.outputNodes.withIndex()) { + if (node.position + it.tilePosition == wirePos) { + outbounds.add(it to WireConnection(it.tilePosition, i)) + any = true + } + } + } + }) + + if (!any) { + LOGGER.warn("Dungeon wire endpoint not found for wire at $wirePos (wires: $group)") + } + } + + if (inbounds.isEmpty() || outbounds.isEmpty()) { + LOGGER.error("Incomplete dungeon wiring group: $group") + return + } + + for ((source, outbound) in outbounds) { + for ((target, inbound) in inbounds) { + source.outputNodes[outbound.index].addConnection(inbound) + target.inputNodes[inbound.index].addConnection(outbound) + } + } + } + suspend fun commit() { val tickets = ArrayList() @@ -508,6 +553,23 @@ class DungeonWorld(val parent: ServerWorld, val random: RandomGenerator, val mar LOGGER.error("Exception while putting dungeon object $obj at ${obj!!.tilePosition}", err) } } + + // objects are placed, now place wiring + for (wiring in localWires) { + try { + placeWires(wiring) + } catch (err: Throwable) { + LOGGER.error("Exception while applying dungeon wiring group", err) + } + } + + for (wiring in globalWires.values) { + try { + placeWires(wiring) + } catch (err: Throwable) { + LOGGER.error("Exception while applying dungeon wiring group", err) + } + } }.await() if (targetChunkState != ChunkState.FULL) { diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt index dc6c0193..c5715008 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/lua/Functions.kt @@ -156,6 +156,16 @@ fun TableFactory.tableOf(vararg values: Any?): Table { return table } +fun TableFactory.tableMapOf(vararg values: Pair): Table { + val table = newTable(0, values.size) + + for ((k, v) in values) { + table[k] = v + } + + return table +} + fun TableFactory.tableOf(): Table { return newTable() } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt index 42932eb1..e51c7625 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/PacketRegistry.kt @@ -62,6 +62,7 @@ import ru.dbotthepony.kstarbound.network.packets.serverbound.ChatSendPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ClientDisconnectRequestPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.ConnectWirePacket import ru.dbotthepony.kstarbound.network.packets.serverbound.DamageTileGroupPacket +import ru.dbotthepony.kstarbound.network.packets.serverbound.DisconnectAllWiresPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.EntityInteractPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FindUniqueEntityPacket import ru.dbotthepony.kstarbound.network.packets.serverbound.FlyShipPacket @@ -464,7 +465,7 @@ class PacketRegistry(val isLegacy: Boolean) { LEGACY.add(::RequestDropPacket) LEGACY.add(::SpawnEntityPacket) LEGACY.add(::ConnectWirePacket) - LEGACY.skip("DisconnectAllWires") + LEGACY.add(::DisconnectAllWiresPacket) LEGACY.add(::WorldClientStateUpdatePacket) LEGACY.add(::FindUniqueEntityPacket) LEGACY.add(WorldStartAcknowledgePacket::read) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt index c646f826..58adcbc0 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/ConnectWirePacket.kt @@ -3,7 +3,7 @@ package ru.dbotthepony.kstarbound.network.packets.serverbound import ru.dbotthepony.kstarbound.network.IServerPacket import ru.dbotthepony.kstarbound.server.ServerConnection import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject -import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection +import ru.dbotthepony.kstarbound.world.entities.tile.WireConnection import java.io.DataInputStream import java.io.DataOutputStream @@ -23,8 +23,14 @@ class ConnectWirePacket(val target: WireConnection, val source: WireConnection) val targetNode = target.outputNodes.getOrNull(this@ConnectWirePacket.target.index) ?: return@enqueue val sourceNode = source.inputNodes.getOrNull(this@ConnectWirePacket.source.index) ?: return@enqueue - targetNode.addConnection(this@ConnectWirePacket.source) - sourceNode.addConnection(this@ConnectWirePacket.target) + if (this@ConnectWirePacket.source in targetNode.connections && this@ConnectWirePacket.target in sourceNode.connections) { + // disconnect + targetNode.removeConnection(this@ConnectWirePacket.source) + sourceNode.removeConnection(this@ConnectWirePacket.target) + } else { + targetNode.addConnection(this@ConnectWirePacket.source) + sourceNode.addConnection(this@ConnectWirePacket.target) + } } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt new file mode 100644 index 00000000..000bf3d2 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/packets/serverbound/DisconnectAllWiresPacket.kt @@ -0,0 +1,29 @@ +package ru.dbotthepony.kstarbound.network.packets.serverbound + +import ru.dbotthepony.kommons.io.readSignedVarInt +import ru.dbotthepony.kommons.io.writeSignedVarInt +import ru.dbotthepony.kstarbound.math.vector.Vector2i +import ru.dbotthepony.kstarbound.network.IServerPacket +import ru.dbotthepony.kstarbound.server.ServerConnection +import ru.dbotthepony.kstarbound.world.entities.tile.WireNode +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject +import java.io.DataInputStream +import java.io.DataOutputStream + +class DisconnectAllWiresPacket(val pos: Vector2i, val node: WireNode) : IServerPacket { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt()), WireNode(stream, isLegacy)) + + override fun write(stream: DataOutputStream, isLegacy: Boolean) { + stream.writeSignedVarInt(pos.x) + stream.writeSignedVarInt(pos.y) + node.write(stream, isLegacy) + } + + override fun play(connection: ServerConnection) { + connection.enqueue { + val target = entityIndex.tileEntityAt(pos) as? WorldObject ?: return@enqueue + val node = if (node.isInput) target.inputNodes.getOrNull(node.index) else target.outputNodes.getOrNull(node.index) + node?.removeAllConnections() + } + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt index ab1d6ae6..ffed82db 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/network/syncher/NetworkedList.kt @@ -28,7 +28,7 @@ class NetworkedList( private data class Entry(val type: Type, val index: Int, val value: KOptional) { constructor(index: Int) : this(Type.REMOVE, index, KOptional()) - constructor(index: Int, value: E) : this(Type.REMOVE, index, KOptional(value)) + constructor(index: Int, value: E) : this(Type.ADD, index, KOptional(value)) fun apply(list: MutableList) { when (type) { @@ -181,7 +181,7 @@ class NetworkedList( } override fun hasChangedSince(version: Long): Boolean { - return backlog.isNotEmpty() && backlog.first().first >= version + return backlog.isNotEmpty() && backlog.last().first >= version } override val size: Int diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt new file mode 100644 index 00000000..c6f08534 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/LegacyWireProcessor.kt @@ -0,0 +1,166 @@ +package ru.dbotthepony.kstarbound.server.world + +import it.unimi.dsi.fastutil.objects.ObjectArrayList +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.future.await +import kotlinx.coroutines.launch +import org.apache.logging.log4j.LogManager +import org.classdump.luna.ByteString +import ru.dbotthepony.kstarbound.lua.tableMapOf +import ru.dbotthepony.kstarbound.math.vector.Vector2i +import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject + +/** + * Wiring network processor from original engine + * + * This processor has several downsides, such as performance degrading as more + * entities are present in world (because this wire processor considers all entities in world on + * each tick), and it also goes for "all or nothing" strategy the hard way; + * + * meaning, it won't finish a tick until all wired entities are loaded/available + * + * This processor is kept in new engine to allow mods to get original behavior of wiring network, + * should new behavior be undesirable. + */ +class LegacyWireProcessor(val world: ServerWorld) { + private val entities = HashMap(1024, 0.5f) + private val entitySet = HashSet(1024, 0.5f) + private var unopenNodes = HashMap>(1024, 0.5f) + + private var isTicking = false + + // TODO: this keeps chunks loaded for every wire network indefinitely + // need to implement concept of hierarchical tickets to solve this (so entities in network do not prolong liveliness of chunks they reside in) + private suspend fun tick0() { + val tickets = ObjectArrayList() + + try { + world.entities.values.forEach { + if (it is WorldObject) { + populateWorking(it) + + val ticket = world.permanentChunkTicket(world.geometry.chunkFromCell(it.tilePosition)).await() + + if (ticket != null) + tickets.add(ticket) + } + } + + while (unopenNodes.isNotEmpty()) { + val copy = unopenNodes + unopenNodes = HashMap(1024, 0.5f) + + val newTickets = ObjectArrayList>, ServerChunk.ITicket>>() + + for (entry in copy) { + val (pos, dependants) = entry + if (dependants.isEmpty()) continue + val ticket = world.permanentChunkTicket(world.geometry.chunkFromCell(pos)).await() ?: continue + newTickets.add(entry to ticket) + tickets.add(ticket) + } + + coroutineScope { + for ((entry, ticket) in newTickets) { + val (pos, dependants) = entry + + launch { + ticket.chunk.await() + + val findEntity = world.entityIndex.tileEntityAt(pos) + + if (findEntity is WorldObject) { + // if entity exists, add it to working entities and find more not loaded entities + populateWorking(findEntity) + } else { + // if entity does not exist - break connections + for (dep in dependants) { + for (node in dep.inputNodes) { + node.removeConnectionsTo(pos) + } + + for (node in dep.outputNodes) { + node.removeConnectionsTo(pos) + } + } + } + } + } + } + } + + // finally, update the network + for (entity in entitySet) { + for ((i, node) in entity.inputNodes.withIndex()) { + val newState = node.connections.any { (pos, index) -> + entities[pos]?.outputNodes?.getOrNull(index)?.state == true + } + + if (newState != node.state) { + try { + node.state = newState + entity.lua.invokeGlobal("onInputNodeChange", entity.lua.tableMapOf(NODE_KEY to i.toLong(), LEVEL_KEY to newState)) + } catch (err: Throwable) { + LOGGER.error("Exception while updating wire state of $entity at ${entity.tilePosition} (input node index $i)", err) + } + } + } + } + } finally { + tickets.forEach { it.cancel() } + entities.clear() + entitySet.clear() + unopenNodes = HashMap() + } + } + + fun tick(): Boolean { + if (isTicking) + return false + + isTicking = true + + world.eventLoop.scope.launch { + try { + tick0() + } catch (err: Throwable) { + LOGGER.error("Exception while updating wiring network", err) + } finally { + isTicking = false + } + } + + return true + } + + private fun populateWorking(root: WorldObject) { + if (entitySet.add(root)) { + unopenNodes.remove(root.tilePosition) + entities[root.tilePosition] = root + + for (node in root.inputNodes) { + for (i in node.connections.indices) { + val connection = node.connections[i] + + if (connection.entityLocation !in entities) + unopenNodes.computeIfAbsent(connection.entityLocation) { ObjectArrayList(32) }.add(root) + } + } + + for (node in root.outputNodes) { + for (i in node.connections.indices) { + val connection = node.connections[i] + + if (connection.entityLocation !in entities) + unopenNodes.computeIfAbsent(connection.entityLocation) { ObjectArrayList(32) }.add(root) + } + } + } + } + + companion object { + val NODE_KEY: ByteString = ByteString.of("node") + val LEVEL_KEY: ByteString = ByteString.of("level") + private val LOGGER = LogManager.getLogger() + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt index 6229e987..c9a2c358 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/server/world/ServerWorld.kt @@ -154,6 +154,8 @@ class ServerWorld private constructor( private var idleTicks = 0 private var isBusy = 0 + private val wireProcessor = LegacyWireProcessor(this) + override val isClient: Boolean get() = false @@ -262,6 +264,10 @@ class ServerWorld private constructor( return } + if (!Globals.universeServer.useNewWireProcessing) { + wireProcessor.tick() + } + super.tick(delta) val packet = StepUpdatePacket(ticks) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt index 6f0b469c..f03091f1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/AbstractEntity.kt @@ -137,10 +137,15 @@ abstract class AbstractEntity : Comparable { return null } + var removalReason: RemovalReason? = null + private set + fun joinWorld(world: World<*, *>) { if (innerWorld != null) throw IllegalStateException("Already spawned (in world $innerWorld)") + removalReason = null + if (entityID == 0) { if (world is ClientWorld) { entityID = world.client.activeConnection?.nextEntityID() ?: world.nextEntityID.incrementAndGet() @@ -173,6 +178,8 @@ abstract class AbstractEntity : Comparable { val world = innerWorld ?: throw IllegalStateException("Not in world") world.eventLoop.ensureSameThread() + removalReason = reason + mailbox.shutdownNow() check(world.entities.remove(entityID) == this) { "Tried to remove $this from $world, but removed something else!" } world.entityList.remove(this) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt index 71e3c7bc..bea994eb 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/ContainerObject.kt @@ -110,6 +110,8 @@ class ContainerObject(config: Registry.Entry) : WorldObject(co } private fun randomizeContents(random: RandomGenerator, threatLevel: Double) { + if (isInitialized) return + isInitialized = true var level = threatLevel level = lookupProperty("level") { JsonPrimitive(level) }.asDouble level += lookupProperty("levelAdjustment") { JsonPrimitive(0.0) }.asDouble diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireConnection.kt similarity index 85% rename from src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt rename to src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireConnection.kt index 8bf2c063..af4d2930 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/wire/WireConnection.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireConnection.kt @@ -1,4 +1,4 @@ -package ru.dbotthepony.kstarbound.world.entities.wire +package ru.dbotthepony.kstarbound.world.entities.tile import ru.dbotthepony.kommons.io.StreamCodec import ru.dbotthepony.kommons.io.readSignedVarInt @@ -6,13 +6,15 @@ import ru.dbotthepony.kommons.io.readVarInt import ru.dbotthepony.kommons.io.writeSignedVarInt import ru.dbotthepony.kommons.io.writeVarInt import ru.dbotthepony.kstarbound.math.vector.Vector2i -import ru.dbotthepony.kstarbound.network.syncher.SizeTCodec import java.io.DataInputStream import java.io.DataOutputStream data class WireConnection(val entityLocation: Vector2i, val index: Int = 0) { constructor(stream: DataInputStream) : this(Vector2i(stream.readSignedVarInt(), stream.readSignedVarInt()), stream.readVarInt()) + // ephemeral property for use inside WorldObjects + var otherEntity: WorldObject? = null + fun write(stream: DataOutputStream) { stream.writeSignedVarInt(entityLocation.x) stream.writeSignedVarInt(entityLocation.y) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireNode.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireNode.kt new file mode 100644 index 00000000..292d5010 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WireNode.kt @@ -0,0 +1,24 @@ +package ru.dbotthepony.kstarbound.world.entities.tile + +import ru.dbotthepony.kommons.io.readVarInt +import ru.dbotthepony.kommons.io.writeVarInt +import ru.dbotthepony.kstarbound.network.syncher.legacyCodec +import ru.dbotthepony.kstarbound.network.syncher.nativeCodec +import java.io.DataInputStream +import java.io.DataOutputStream + +data class WireNode(val isInput: Boolean, val index: Int = 0) { + constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readInt() == 0 else stream.readBoolean(), stream.readVarInt()) + + fun write(stream: DataOutputStream, isLegacy: Boolean) { + if (isLegacy) { + stream.writeInt(if (isInput) 0 else 1) + stream.writeVarInt(index) + } + } + + companion object { + val CODEC = nativeCodec(::WireNode, WireNode::write) + val LEGACY_CODEC = legacyCodec(::WireNode, WireNode::write) + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt index 8cc51729..244869ab 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/world/entities/tile/WorldObject.kt @@ -19,9 +19,6 @@ import ru.dbotthepony.kstarbound.math.vector.Vector2i import ru.dbotthepony.kstarbound.Registries import ru.dbotthepony.kstarbound.Registry import ru.dbotthepony.kstarbound.Starbound -import ru.dbotthepony.kstarbound.client.StarboundClient -import ru.dbotthepony.kstarbound.client.render.LayeredRenderer -import ru.dbotthepony.kstarbound.defs.Drawable import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition import ru.dbotthepony.kstarbound.defs.`object`.ObjectOrientation import ru.dbotthepony.kommons.gson.get @@ -35,6 +32,7 @@ import ru.dbotthepony.kstarbound.math.AABB import ru.dbotthepony.kommons.util.KOptional import ru.dbotthepony.kommons.util.getValue import ru.dbotthepony.kommons.util.setValue +import ru.dbotthepony.kstarbound.Globals import ru.dbotthepony.kstarbound.math.vector.Vector2d import ru.dbotthepony.kstarbound.defs.DamageSource import ru.dbotthepony.kstarbound.defs.EntityType @@ -72,20 +70,19 @@ import ru.dbotthepony.kstarbound.lua.bindings.provideWorldObjectBindings import ru.dbotthepony.kstarbound.lua.from import ru.dbotthepony.kstarbound.lua.get import ru.dbotthepony.kstarbound.lua.set +import ru.dbotthepony.kstarbound.lua.tableMapOf import ru.dbotthepony.kstarbound.lua.tableOf import ru.dbotthepony.kstarbound.lua.toJson import ru.dbotthepony.kstarbound.lua.toJsonFromLua +import ru.dbotthepony.kstarbound.server.world.LegacyWireProcessor import ru.dbotthepony.kstarbound.util.ManualLazy import ru.dbotthepony.kstarbound.util.asStringOrNull import ru.dbotthepony.kstarbound.world.Direction -import ru.dbotthepony.kstarbound.world.LightCalculator -import ru.dbotthepony.kstarbound.world.PIXELS_IN_STARBOUND_UNITf import ru.dbotthepony.kstarbound.world.TileHealth import ru.dbotthepony.kstarbound.world.World -import ru.dbotthepony.kstarbound.world.api.TileColor import ru.dbotthepony.kstarbound.world.entities.Animator -import ru.dbotthepony.kstarbound.world.entities.wire.WireConnection import java.io.DataOutputStream +import java.util.Collections import java.util.HashMap import java.util.random.RandomGenerator @@ -240,13 +237,56 @@ open class WorldObject(val config: Registry.Entry) : TileEntit val chatPortrait by networkedString().also { networkGroup.upstream.add(it) } val chatConfig by networkedJsonElement().also { networkGroup.upstream.add(it) } + @Suppress("deprecation") inner class WireNode(val position: Vector2i, val isInput: Boolean) { - val connections = NetworkedList(WireConnection.CODEC).also { networkGroup.upstream.add(it) } + @Deprecated("Internal property, do not use directly", replaceWith = ReplaceWith("this.connections")) + val connectionsInternal = NetworkedList(WireConnection.CODEC).also { networkGroup.upstream.add(it) } + val connections: List = Collections.unmodifiableList(connectionsInternal) var state by networkedBoolean().also { networkGroup.upstream.add(it) } + val index by lazy { + if (isInput) + inputNodes.indexOf(this) + else + outputNodes.indexOf(this) + } + fun addConnection(connection: WireConnection) { - if (connection !in connections) { - connections.add(connection) + if (connection !in connectionsInternal) { + connectionsInternal.add(connection.copy()) + lua.invokeGlobal("onNodeConnectionChange") + } + } + + fun removeConnection(connection: WireConnection) { + if (connectionsInternal.remove(connection)) { + lua.invokeGlobal("onNodeConnectionChange") + } + } + + fun removeAllConnections() { + if (connectionsInternal.isNotEmpty()) { + // ensure that we disconnect both ends + val any = connectionsInternal.removeIf { + val otherEntity = world.entityIndex.tileEntityAt(it.entityLocation) as? WorldObject + val otherConnections = if (isInput) otherEntity?.outputNodes else otherEntity?.inputNodes + val any = otherConnections?.getOrNull(it.index)?.connectionsInternal?.removeIf { it.entityLocation == tilePosition && it.index == index } + + if (any == true) { + otherEntity!!.lua.invokeGlobal("onNodeConnectionChange") + } + + any == true + } + + if (any) + lua.invokeGlobal("onNodeConnectionChange") + } + } + + fun removeConnectionsTo(pos: Vector2i) { + if (connectionsInternal.removeIf { it.entityLocation == pos }) { + lua.invokeGlobal("onNodeConnectionChange") } } } @@ -500,6 +540,54 @@ open class WorldObject(val config: Registry.Entry) : TileEntit } } + if (world.isServer && Globals.universeServer.useNewWireProcessing) { + for ((i, node) in inputNodes.withIndex()) { + var newState: Boolean? = false + val itr = node.connectionsInternal.listIterator() + + for (connection in itr) { + connection.otherEntity = connection.otherEntity ?: world.entityIndex.tileEntityAt(connection.entityLocation) as? WorldObject + + if (connection.otherEntity?.isInWorld == false) { + // break connection if other entity got removed + if (connection.otherEntity?.removalReason?.removal == true) { + itr.remove() + lua.invokeGlobal("onNodeConnectionChange") + continue + } + + connection.otherEntity = null + } + + val otherEntity = connection.otherEntity + + // if entity is loaded, update our status + if (otherEntity != null) { + val otherNode = otherEntity.outputNodes.getOrNull(connection.index) + + // break connection if we point at invalid node + if (otherNode == null) { + itr.remove() + lua.invokeGlobal("onNodeConnectionChange") + } else { + newState = newState!! || otherNode.state + } + } else { + // if entity is not loaded, then consider we can't update our status + newState = null + break + } + } + + // if all entities we are connected to are loaded, then update our node state + // otherwise, keep current node state + if (newState != null && node.state != newState) { + node.state = newState + lua.invokeGlobal("onInputNodeChange", lua.tableMapOf(LegacyWireProcessor.NODE_KEY to i.toLong(), LegacyWireProcessor.LEVEL_KEY to newState)) + } + } + } + if (world.isServer && !unbreakable) { var shouldBreak = false