Last active
May 29, 2024 20:20
-
-
Save CyberShadow/bc6ac011016b841c050fdf88816e202f to your computer and use it in GitHub Desktop.
Tunnet switch builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std.algorithm.comparison; | |
import std.algorithm.iteration; | |
import std.algorithm.mutation; | |
import std.algorithm.searching; | |
import std.algorithm.setops; | |
import std.algorithm.sorting; | |
import std.array; | |
import std.file; | |
import std.format; | |
import std.math.algebraic; | |
import std.math.constants; | |
import std.math.rounding; | |
import std.meta : AliasSeq; | |
import std.path; | |
import std.range; | |
import std.stdio : stderr; | |
import std.sumtype; | |
import std.traits; | |
import std.typecons; | |
import ae.sys.file; | |
import ae.utils.array; | |
import ae.utils.json; | |
import ae.utils.meta; | |
import config; | |
import game; | |
/////////////////////////////////////////////////////////////////////////////// | |
/////////////////////////////////////////////////////////////////////////////// | |
/// Returns a range of coordinates of chunks which fully contain or | |
/// touch (on a face, edge, or corner) the given world coordinate. | |
auto chunksTouching(WorldCoord pos) | |
{ | |
auto chunkPos = pos.toChunk; | |
return cartesianProduct( | |
iota(chunkPos.x - (pos[0] % chunkSize == 0 ? 1 : 0), chunkPos.x + 1), | |
iota(chunkPos.y - (pos[1] % chunkSize == 0 ? 1 : 0), chunkPos.y + 1), | |
iota(chunkPos.z - (pos[2] % chunkSize == 0 ? 1 : 0), chunkPos.z + 1), | |
).map!(t => ChunkCoord(t.expand)); | |
} | |
void buildThing(ref SaveGame game, Config config) | |
{ | |
const origGame = game; | |
// Configuration | |
enum numHosts = 4 ^^ 3; | |
enum terminatorSize = 4.0; | |
enum cellSize = 2.0; | |
enum numInputIngesters = 5; | |
enum inputIngesterSize = hubLength; | |
enum inputPortLinkSize = numInputIngesters * inputIngesterSize; | |
enum numAvalancheCells = 0; // Optional "avalanche" feature for getting throughput high scores | |
enum avalancheCellSize = 0.75; | |
enum outputPortLinkSize = numAvalancheCells * avalancheCellSize; | |
enum portLaneSize = 0.25; | |
enum minPortFilterSize = 4; | |
// enum maxWireLength = 4.0; | |
// Cheat to save FPS. Wires start to visually disappear at around 40. | |
enum maxWireLength = 32; | |
enum ChunkCoord startChunk = {x: 11, y: 0, z: -2}; | |
auto totalWorldXSize = | |
terminatorSize + | |
cellSize * numHosts + | |
outputPortLinkSize + | |
portLaneSize * numHosts; | |
auto totalChunkXSize = cast(int)ceil(totalWorldXSize / chunkSize); | |
auto totalWorldZSize = | |
terminatorSize + | |
cellSize * numHosts + | |
inputPortLinkSize + | |
portLaneSize * numHosts + | |
minPortFilterSize; | |
auto totalChunkZSize = cast(int)ceil(totalWorldZSize / chunkSize); | |
totalWorldZSize = totalChunkZSize * chunkSize; | |
auto startWorld = startChunk.toWorld; | |
auto objectY = startWorld[1] + 5.5; // We will place all objects at this height. | |
// Note: these target the landing pad (a chunk that we will clear on the first run only, and not populate) | |
static immutable ChunkCoord[4] busTargets = [ | |
{ 5, 0, 5}, | |
{ 8, -1, 16}, | |
{19, -1, 11}, | |
{35, -1, 7}, | |
]; | |
// Lay out the chunk geometry | |
enum ChunkGeometry { flat, empty } | |
struct ChunkPlan { ChunkGeometry geometry; bool firstRunOnly; } | |
ChunkPlan[ChunkCoord] chunkPlan; | |
// Switch | |
foreach (z; startChunk.z .. startChunk.z + totalChunkZSize) | |
foreach (x; startChunk.x .. startChunk.x + totalChunkXSize) | |
chunkPlan[ChunkCoord(x, startChunk.y, z)] = ChunkPlan(ChunkGeometry.flat, false); | |
foreach (x; startChunk.x .. startChunk.x + totalChunkXSize) | |
chunkPlan[ChunkCoord(x, startChunk.y, startChunk.z + totalChunkZSize)] = ChunkPlan(ChunkGeometry.flat, true); | |
// Bus | |
auto busChunkZ = startChunk.z + totalChunkZSize + 1; | |
foreach (x; startChunk.x .. startChunk.x + totalChunkXSize) | |
chunkPlan[ChunkCoord(x, startChunk.y, busChunkZ)] = ChunkPlan(ChunkGeometry.flat, false); | |
foreach (i, target; busTargets) | |
{ | |
{ | |
int x0 = target.x; | |
int x1 = startChunk.x; | |
if (x0 > x1) swap(x0, x1); | |
foreach (x; x0 .. x1 + 1) | |
chunkPlan[ChunkCoord(x, startChunk.y, busChunkZ)] = ChunkPlan(ChunkGeometry.flat, false); | |
} | |
{ | |
int z0 = target.z; | |
int z1 = busChunkZ; | |
if (z0 > z1) z0--; else z0++; | |
if (z0 > z1) swap(z0, z1); | |
foreach (z; z0 .. z1 + 1) | |
chunkPlan[ChunkCoord(target.x, startChunk.y, z)] = ChunkPlan(ChunkGeometry.flat, false); | |
} | |
{ | |
auto padGeom = target.y >= startChunk.y | |
? ChunkGeometry.flat | |
: ChunkGeometry.empty; | |
chunkPlan[ChunkCoord(target.x, startChunk.y, target.z)] = ChunkPlan(padGeom, true); | |
} | |
} | |
bool firstRun = !game.nodes.canFind!(node => node.pos.toChunk in chunkPlan && node.pos[1] == objectY); | |
auto chunkRunPlan = chunkPlan | |
.byKeyValue | |
.filter!(kv => !kv.value.firstRunOnly || firstRun) | |
.map!(kv => tuple(kv.key, kv.value.geometry)) | |
.assocArray; | |
static immutable nullNode = Node([0, 0, 0], [0, 0, 0], 0); | |
// Delete nodes, edges, and objects in the build zone | |
// 1. Delete the nodes themselves | |
NodeIndex[] nodeFreeList; | |
{ | |
game.nodes = game.nodes.dup; | |
foreach (NodeIndex i, ref node; game.nodes) | |
if (node.pos[1] == objectY && | |
chunksTouching(node.pos).any!(chunkPos => | |
chunkPos in chunkPlan && | |
chunkPlan[chunkPos].firstRunOnly == false | |
)) | |
{ | |
node = nullNode; | |
nodeFreeList ~= i; | |
} | |
} | |
// 2. Map deleted nodes | |
auto nodeDeleted = new bool[game.nodes.length]; | |
foreach (NodeIndex i; nodeFreeList) | |
nodeDeleted[i] = true; | |
// 3. Delete affected objects | |
{ | |
game.edges = game.edges .filter!(edge => !(nodeDeleted[edge[0][0]] || nodeDeleted[edge[1][0]])).array; | |
game.relays = game.relays .filter!(o => !nodeDeleted[o.node]).array; | |
game.filters = game.filters.filter!(o => !nodeDeleted[o.node]).array; | |
game.hubs = game.hubs .filter!(o => !nodeDeleted[o.node]).array; | |
} | |
// scope(success) enforce(nodeFreeList.length == 0, "Still have deleted nodes"); | |
// Make chunks | |
{ | |
static immutable Material[] unimportantMaterials = [ | |
Material.empty, | |
Material.dirt, | |
Material.drillGoo, | |
Material.stone, | |
Material.crackedStone, | |
]; | |
foreach (chunk; game.chunks) | |
if (chunk[0] in chunkRunPlan) | |
foreach (r; chunk[1]) | |
if (!unimportantMaterials.canFind(cast(Material)r[1])) | |
throw new Exception("Refusing to overwrite chunk at %s with important material %s".format(chunk[0], cast(Material)r[1])); | |
foreach (node; game.nodes) | |
if (node.pos[1] != objectY && node.pos.toChunk in chunkRunPlan) | |
throw new Exception("Refusing to overwrite chunk at %s with user-placed node at %s".format(node.pos.toChunk, node.pos)); | |
game.chunks = game.chunks | |
.filter!(c => c[0] !in chunkRunPlan) | |
.array; | |
foreach (chunkPos, geometry; chunkRunPlan) | |
{ | |
ChunkContents c; | |
foreach (z, ref plane; c) | |
foreach (y, ref row; plane) | |
foreach (x, ref cell; row) | |
cell = delegate Material { | |
final switch (geometry) | |
{ | |
case ChunkGeometry.flat: | |
return y >= 12 && y < 30 | |
? Material.empty | |
: Material.dirt; | |
case ChunkGeometry.empty: | |
return Material.empty; | |
} | |
}(); | |
game.chunks ~= Chunk(chunkPos, compress(c)); | |
} | |
} | |
static immutable WorldCoord upVector = [0, 1, 0]; | |
NodeIndex addNode(WorldCoord pos, WorldCoord up = upVector, double angle = 0) | |
{ | |
foreach (NodeIndex oldNodeIndex, ref oldNode; game.nodes) | |
if (oldNode.pos == pos) | |
assert(false, "Trying to create node on top of existing node"); | |
NodeIndex index; | |
if (nodeFreeList.length) | |
index = nodeFreeList.shift; | |
else | |
index = game.nodes.length++; | |
game.nodes[index] = Node(pos, up, angle); | |
return index; | |
} | |
NodeIndex addRelay(WorldCoord pos, WorldCoord up = upVector, double angle = 0, bool fixed = false, bool light = true) | |
{ | |
auto index = addNode(pos, up, angle); | |
game.relays ~= Relay(index, fixed: fixed, light: light); | |
return index; | |
} | |
NodeIndex addHub(WorldCoord pos, WorldCoord up = upVector, double angle = 0, bool fixed = false, bool dir = false) | |
{ | |
auto index = addNode(pos, up, angle); | |
game.hubs ~= Hub(index, fixed: fixed, dir: dir); | |
return index; | |
} | |
NodeIndex addFilter(WorldCoord pos, WorldCoord up = upVector, double angle = 0, Filter.Config config = Filter.Config.init, bool fixed = false) | |
{ | |
auto index = addNode(pos, up, angle); | |
game.filters ~= Filter(index, config, fixed: fixed); | |
return index; | |
} | |
size_t[NodePort] portUsed; | |
size_t numUsedPorts; | |
void usePort(NodePort port) | |
{ | |
// if (numUsedPorts == 2248) assert(false, "Here"); | |
assert(port !in portUsed, "Port used: %d".format(portUsed[port])); | |
portUsed[port] = numUsedPorts++; | |
} | |
foreach (edge; game.edges) | |
static foreach (i; 0 .. 2) | |
usePort(edge[i]); | |
void addEdge(NodePort source, NodePort target, CableColor color = 0) | |
{ | |
usePort(source); | |
usePort(target); | |
game.edges ~= Edge(source, target, color); | |
} | |
void addRelays(NodePort source, NodePort target, CableColor color = 0) | |
{ | |
while (true) | |
{ | |
auto sourceCoord = game.nodes[source[0]].pos; | |
auto targetCoord = game.nodes[target[0]].pos; | |
WorldCoord vec = [ | |
targetCoord[0] - sourceCoord[0], | |
targetCoord[1] - sourceCoord[1], | |
targetCoord[2] - sourceCoord[2], | |
]; | |
auto vecLength = sqrt(vec[0] ^^ 2 + vec[1] ^^ 2 + vec[2] ^^ 2); | |
if (vecLength <= maxWireLength) | |
return addEdge(source, target, color); | |
vec[] /= vecLength; | |
auto relayCoord = sourceCoord; | |
relayCoord[] += vec[] * maxWireLength; | |
auto relay = addRelay(relayCoord, light: false); | |
addEdge(source, NodePort(relay, 0), color); | |
source = NodePort(relay, 1); | |
} | |
} | |
void reconnect(NodePort newPort) | |
{ | |
auto prevNumEdges = game.edges.length; | |
auto newNodeIndex = newPort[0]; | |
auto newNode = game.nodes[newNodeIndex]; | |
foreach (oldNodeIndex, oldNode; origGame.nodes) | |
if (oldNode.pos == newNode.pos) | |
{ | |
auto oldPort = NodePort(oldNodeIndex, newPort[1]); | |
foreach (oldEdge; origGame.edges) | |
if (oldEdge[0] == oldPort && !nodeDeleted[oldEdge[1][0]]) | |
addEdge(newPort, oldEdge[1]); | |
else | |
if (oldEdge[1] == oldPort && !nodeDeleted[oldEdge[0][0]]) | |
addEdge(oldEdge[0], newPort); | |
} | |
auto numAddedEdges = game.edges.length - prevNumEdges; | |
assert(numAddedEdges <= 1, "Ambiguous reconnect"); | |
} | |
// Switch edge port to bus / endpoint | |
NodePort[numHosts] peerPorts; | |
// Create the switch | |
{ | |
enum Side | |
{ | |
source, | |
target, | |
} | |
NodePort[numHosts][enumLength!Side] cellPorts; | |
// Terminators | |
foreach (side; Side.init .. enumLength!Side) | |
{ | |
WorldCoord sidePos(WorldCoord pos) | |
{ | |
pos = side == Side.source | |
? pos | |
: [pos[2], pos[1], pos[0]]; | |
return [ | |
pos[0] + startWorld[0], | |
pos[1] + startWorld[1], | |
pos[2] + startWorld[2], | |
]; | |
} | |
double sideAngle(double angle) | |
{ | |
return side == Side.source | |
? angle | |
: PI/2 - angle; | |
} | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
auto x0 = 0 + terminatorSize + host * cellSize; | |
auto z0 = 0; | |
auto hubX = x0 + cellSize * 1/8; // aligned with cells' hubs | |
auto hub = addHub( | |
pos: sidePos([hubX, objectY, z0 + cellSize * 5/4]), | |
angle: sideAngle(0), | |
); | |
auto relay0 = addRelay( | |
pos: sidePos([hubX - cellSize * 1/4, objectY, z0 + cellSize * 3/4]), | |
angle: sideAngle(0), | |
); | |
auto relay1 = addRelay( | |
pos: sidePos([hubX + cellSize * 1/4, objectY, z0 + cellSize * 3/4]), | |
angle: sideAngle(0), | |
); | |
addEdge(NodePort(hub, 1), NodePort(relay0, 0)); | |
addEdge(NodePort(relay0, 1), NodePort(relay1, 0)); | |
addEdge(NodePort(relay1, 1), NodePort(hub, 2)); | |
cellPorts[side][host] = NodePort(hub, 0); | |
} | |
} | |
// NxN router cells | |
foreach (HostIndex sourceHost; 0 .. numHosts) | |
foreach (HostIndex targetHost; 0 .. numHosts) | |
{ | |
auto x0 = startWorld[0] + terminatorSize + sourceHost * cellSize; | |
auto z0 = startWorld[2] + terminatorSize + targetHost * cellSize; | |
auto active = config.canSend[sourceHost][targetHost]; | |
if (active) | |
{ | |
auto filterInputHub = addHub( | |
pos: [x0 + cellSize * 1/8, objectY, z0 + cellSize * 5/8], | |
angle: -PI/2, | |
); | |
auto targetCheckFilter = addFilter( | |
pos: [x0 + cellSize * 0.4125, objectY, z0 + cellSize * 0.725], | |
angle: 0, | |
config: Filter.Config( | |
// Sends back packets not addressed to the target. | |
port: 0, | |
mask: toAddress(targetHost, Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Differ, | |
collision: Filter.Config.Collision.SendBackOutbound, | |
), | |
); | |
auto oneWayOutputFilter = addFilter( | |
pos: [x0 + cellSize * 0.725, objectY, z0 + cellSize * 0.4125], | |
angle: PI/2, | |
config: Filter.Config( | |
// Rejects everything on the outbound port. | |
port: 1, | |
// mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
// addr: Filter.Config.Addr.Dst, | |
// action: Filter.Config.Action.SendBackPacket, | |
// op: Filter.Config.Op.Match, | |
// collision: Filter.Config.Collision.SendBackOutbound, | |
// This variant (which should always match) allows identification of the source: | |
mask: Address((){ auto a = sourceHost.toAddressElements(); a[0] = Address.Element.Three; return a; }(), Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Src, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Differ, | |
collision: numAvalancheCells == 0 | |
? Filter.Config.Collision.SendBackOutbound | |
: Filter.Config.Collision.DropInbound, | |
), | |
); | |
auto filterOutputHub = addHub( | |
pos: [x0 + cellSize * 5/8, objectY, z0 + cellSize * 1/8], | |
angle: PI, | |
); | |
addEdge(NodePort(filterInputHub, 1), NodePort(targetCheckFilter, 0)); | |
addEdge(NodePort(targetCheckFilter, 1), NodePort(oneWayOutputFilter, 0)); | |
addEdge(NodePort(oneWayOutputFilter, 1), NodePort(filterOutputHub, 1)); | |
// Link with top/right neighbor | |
addRelays(cellPorts[Side.source][sourceHost], NodePort(filterInputHub, 2)); | |
cellPorts[Side.source][sourceHost] = NodePort(filterInputHub, 0); | |
addRelays(cellPorts[Side.target][targetHost], NodePort(filterOutputHub, 0)); | |
cellPorts[Side.target][targetHost] = NodePort(filterOutputHub, 2); | |
} | |
} | |
// Input ingesters | |
// (This mechanism attempts to solve the problem of packet drops due to unfortunate timing. | |
// It works by giving packets more than one attempt to enter the input loop.) | |
NodePort[numHosts] inputIngesters; | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
auto inputX0 = startWorld[0] + terminatorSize + host * cellSize; | |
auto inputZ0 = startWorld[2] + terminatorSize + numHosts * cellSize; | |
Nullable!NodePort lastInnerPort = cellPorts[Side.source][host]; | |
Nullable!NodePort lastOuterPort; // will be created on the first iteration | |
assert(numInputIngesters > 0); // Must have at least one. One ingester is just one filter, no hubs. | |
foreach (i; 0 .. numInputIngesters) | |
{ | |
auto z = inputZ0 + inputIngesterSize * i + inputIngesterSize * 0.5; | |
auto filter = addFilter( | |
pos: [inputX0 + cellSize * 1/8 + 0.65, objectY, z], | |
angle: -PI/2, | |
config: Filter.Config( | |
port: 1, | |
mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, | |
collision: Filter.Config.Collision.SendBackOutbound, | |
), | |
); | |
if (i + 1 == numInputIngesters) | |
{ | |
// Wire directly to filter | |
addEdge(lastInnerPort.get(), NodePort(filter, 1)); | |
lastInnerPort = Nullable!NodePort(); | |
} | |
else | |
{ | |
auto innerHub = addHub( | |
pos: [inputX0 + cellSize * 1/8, objectY, z], | |
angle: -PI/2, | |
); | |
addEdge(lastInnerPort.get(), NodePort(innerHub, 0)); | |
addEdge(NodePort(innerHub, 1), NodePort(filter, 1)); | |
lastInnerPort = NodePort(innerHub, 2); | |
} | |
if (i == 0) | |
{ | |
// Wire directly from filter | |
lastOuterPort = NodePort(filter, 0); | |
} | |
else | |
{ | |
auto outerHub = addHub( | |
pos: [inputX0 + cellSize * 1/8 + 1.3, objectY, z], | |
angle: -PI/2, | |
); | |
addEdge(lastOuterPort.get(), NodePort(outerHub, 2)); | |
addEdge(NodePort(outerHub, 1), NodePort(filter, 0)); | |
lastOuterPort = NodePort(outerHub, 0); | |
} | |
} | |
// One final filter to 1) allow things to line up 2) trap packets to keep retrying | |
auto loopFilter = addFilter( | |
pos: [ | |
// The position where the last hub would be | |
inputX0 + cellSize * 1/8, | |
objectY, | |
inputZ0 + inputIngesterSize * (numInputIngesters - 1) + inputIngesterSize * 0.5 | |
], | |
angle: 0, | |
config: Filter.Config( | |
port: 1, | |
mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, | |
collision: Filter.Config.Collision.DropInbound, | |
), | |
); | |
addEdge(lastOuterPort.get(), NodePort(loopFilter, 1)); | |
inputIngesters[host] = NodePort(loopFilter, 0); | |
} | |
// Avalanche cells | |
if (numAvalancheCells) | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
auto port = cellPorts[Side.target][host]; | |
foreach (i; 0 .. numAvalancheCells) | |
{ | |
auto filter = addFilter( | |
pos: [ | |
startWorld[0] + terminatorSize + numHosts * cellSize + i * avalancheCellSize + avalancheCellSize * 0.5, | |
objectY, | |
startWorld[2] + terminatorSize + host * cellSize + cellSize * 1/8, | |
], | |
angle: 0, | |
config: i + 1 == numAvalancheCells | |
? Filter.Config( | |
// The "go" switch | |
port: 0, | |
mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, | |
collision: Filter.Config.Collision.DropInbound, | |
) | |
: Filter.Config( | |
// Avalanche cell - trap the packet | |
port: 1, | |
mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, | |
collision: Filter.Config.Collision.SendBackOutbound, | |
), | |
); | |
addRelays(port, NodePort(filter, 0)); | |
port = NodePort(filter, 1); | |
} | |
cellPorts[Side.target][host] = port; | |
} | |
// Input / output / bus integration | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
auto inputX0 = startWorld[0] + terminatorSize + host * cellSize; | |
auto inputZ0 = startWorld[2] + terminatorSize + numHosts * cellSize; | |
auto outputX0 = startWorld[0] + terminatorSize + numHosts * cellSize; | |
auto outputZ0 = startWorld[2] + terminatorSize + host * cellSize; | |
auto inputOutputSplitHub = addHub( | |
pos: [inputX0 + cellSize * 1/8, objectY, inputZ0 + inputPortLinkSize + host * portLaneSize], | |
angle: 0, | |
); | |
auto inputFilter = addFilter( | |
pos: [inputX0 + cellSize * 1/8, objectY, startWorld[2] + totalWorldZSize - 0.3], | |
angle: 0, | |
config: Filter.Config( | |
// Sends back packets not from the intended peer. Mainly for identification. | |
port: 0, | |
mask: toAddress(host, Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Src, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Differ, | |
collision: Filter.Config.Collision.DropInbound, | |
), | |
); | |
auto outputToCornerRelay = addRelay( | |
pos: [outputX0 + outputPortLinkSize + host * portLaneSize, objectY, outputZ0 + cellSize * 1/8], | |
angle: 0, | |
); | |
auto cornerRelay = addRelay( | |
pos: [outputX0 + outputPortLinkSize + host * portLaneSize, objectY, inputZ0 + inputPortLinkSize + host * portLaneSize], | |
angle: 0, | |
); | |
addRelays(NodePort(inputFilter, 1), NodePort(inputOutputSplitHub, 0)); | |
addRelays(NodePort(inputOutputSplitHub, 1), inputIngesters[host]); | |
addRelays(cellPorts[Side.target][host], NodePort(outputToCornerRelay, 0)); | |
addRelays(NodePort(outputToCornerRelay, 1), NodePort(cornerRelay, 0)); | |
addRelays(NodePort(cornerRelay, 1), NodePort(inputOutputSplitHub, 2)); | |
peerPorts[host] = NodePort(inputFilter, 0); | |
} | |
// Reconnect peers | |
foreach (HostIndex host; 0 .. numHosts) | |
reconnect(peerPorts[host]); | |
} | |
// The bus | |
NodePort[numHosts] busInnerPorts, busOuterPorts; | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
enum hostsPerBusArm = 16; | |
auto exitX = startWorld[0] + terminatorSize + host * cellSize + cellSize * 1/8; // Same as inputFilter | |
auto busLaneWidth = double(chunkSize) / numHosts; | |
auto busExitZ = busChunkZ * chunkSize; | |
auto busZ = busExitZ + host * busLaneWidth + busLaneWidth / 2; | |
auto busEnterArmWidth = double(chunkSize) / 4 * 3; | |
auto busEnterLaneWidth = busEnterArmWidth / hostsPerBusArm; // 0.75 (edges are too close to dig areas and might even be effectively buried) | |
auto enterX = | |
busTargets[host / hostsPerBusArm].x * chunkSize // bus arm position | |
+ | |
(chunkSize - busEnterArmWidth) / 2 // centering within bus arm | |
+ | |
(host % hostsPerBusArm) * busEnterLaneWidth // per-host offset | |
+ | |
busEnterLaneWidth / 2; // centering within bus lane | |
auto enterZ = busTargets[host / hostsPerBusArm].z * chunkSize + (busTargets[host / hostsPerBusArm].z > busChunkZ ? 0 : 1) * chunkSize; | |
auto busExit = addRelay([exitX, objectY, busExitZ]); | |
auto busFinish = addRelay([exitX, objectY, busZ]); | |
auto busStart = addRelay([enterX, objectY, busZ]); | |
auto busEnter = addRelay([enterX, objectY, enterZ]); | |
reconnect(NodePort(busEnter, 0)); | |
addRelays(NodePort(busEnter, 1), NodePort(busStart, 0)); | |
addRelays(NodePort(busStart, 1), NodePort(busFinish, 0)); | |
addRelays(NodePort(busFinish, 1), NodePort(busExit, 0)); | |
reconnect(NodePort(busExit, 1)); | |
busOuterPorts[host] = NodePort(busEnter, 0); | |
busInnerPorts[host] = NodePort(busExit, 1); | |
} | |
// Check connectivity | |
stderr.writefln("Host\tSwitch\tBus"); | |
foreach (HostIndex host; 0 .. numHosts) | |
{ | |
stderr.writef(host.toAddressElements().toString()); | |
foreach (port; [peerPorts[host], busOuterPorts[host]]) | |
{ | |
stderr.write("\t"); | |
auto peer = game.getFinalNodePortPeer(port); | |
if (peer.isNull()) | |
stderr.writef("Dangle"); | |
else | |
{ | |
auto peerNodeIndex = peer.get().port[0]; | |
auto obj = game.getGameObject(peerNodeIndex); | |
if (obj.isNull()) | |
stderr.writef("Unknown"); | |
else | |
obj.get.match!( | |
(ref const Endpoint endpoint) { | |
if (endpoint.address == host.toAddress(Address.Type.Endpoint)) | |
stderr.writef("OK (%d)", peer.get().numHops); | |
else | |
stderr.writef(endpoint.address.elements.toString()); | |
}, | |
(ref const other) { | |
stderr.writef(Unqual!(typeof(other)).stringof); | |
} | |
); | |
} | |
} | |
stderr.writeln(); | |
} | |
} | |
alias GameObject = SumType!( | |
Endpoint, | |
Relay, | |
Filter, | |
Hub, | |
Bridge, | |
); | |
Nullable!GameObject getGameObject(ref const SaveGame game, NodeIndex node) | |
{ | |
static foreach (getObjectsOfType; AliasSeq!( | |
() => game.endpoints, | |
() => game.relays, | |
() => game.filters, | |
() => game.hubs, | |
() => game.bridges, | |
)) | |
foreach (obj; getObjectsOfType()) | |
if (obj.node == node) | |
return typeof(return)(GameObject(obj)); | |
return typeof(return)(); | |
} | |
Nullable!NodePort getNodePortPeer(ref const SaveGame game, NodePort port) | |
{ | |
foreach (edge; game.edges) | |
if (edge[0] == port) | |
return typeof(return)(edge[1]); | |
else if (edge[1] == port) | |
return typeof(return)(edge[0]); | |
return typeof(return)(); | |
} | |
struct FinalPeer | |
{ | |
NodePort port; | |
size_t numHops; | |
} | |
Nullable!FinalPeer getFinalNodePortPeer(ref const SaveGame game, NodePort port) | |
{ | |
size_t numHops; | |
while (true) | |
{ | |
auto peer = game.getNodePortPeer(port); | |
if (peer.isNull) | |
return typeof(return)(); | |
auto peerNode = peer.get()[0]; | |
auto peerPort = peer.get()[1]; | |
auto obj = game.getGameObject(peerNode); | |
if (obj.isNull) | |
return typeof(return)(FinalPeer(peer.get(), numHops)); | |
bool done; | |
obj.get().match!( | |
(ref const Relay relay) { done = false; port = NodePort(peerNode, cast(PortIndex)(1 - peerPort)); }, | |
(_ ) { done = true; }, | |
); | |
if (done) | |
return typeof(return)(FinalPeer(peer.get(), numHops)); | |
numHops++; | |
} | |
} | |
void main() | |
{ | |
auto game = "~/steam/.local/share/tunnet/slot_0.json" | |
.expandTilde | |
.readText | |
.jsonParse!SaveGame; | |
auto config = readConfig(); | |
buildThing(game, config); | |
"~/steam/.local/share/tunnet/slot_1.json" | |
.expandTilde | |
.atomicWrite( | |
game.toJson | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std.algorithm.searching; | |
import std.file; | |
import std.string; | |
import game; | |
struct Config | |
{ | |
bool[4^^4][4^^4] canSend; | |
} | |
Config readConfig() | |
{ | |
Address.Element[4][] replierMasks; | |
Config config; | |
foreach (line; readText("rules.txt").splitLines) | |
{ | |
if (!line.length || line[0] == '#') | |
continue; | |
auto parts = line.findSplit("\t"); | |
switch (parts[2]) | |
{ | |
case "reply": | |
auto sourceMask = parseAddress(parts[0]); | |
replierMasks ~= sourceMask; | |
break; | |
default: | |
// expand letters | |
void handle(string sourceStr, string targetStr) | |
{ | |
auto sourceMask = parseAddress(sourceStr); | |
auto targetMask = parseAddress(targetStr); | |
foreach (source; hostsMatching(sourceMask)) | |
foreach (target; hostsMatching(targetMask)) | |
config.canSend[source][target] = true; | |
} | |
void expand(string sourceStr, string targetStr) | |
{ | |
foreach (letter; "abcd") | |
if (sourceStr.canFind(letter)) | |
{ | |
foreach (replacement; "0123") | |
expand( | |
sourceStr.replace(letter, replacement), | |
targetStr.replace(letter, replacement), | |
); | |
return; | |
} | |
handle(sourceStr, targetStr); | |
} | |
expand(parts[0], parts[2]); | |
break; | |
} | |
} | |
foreach (replierMask; replierMasks) | |
foreach (replier; hostsMatching(replierMask)) | |
foreach (peer; allHosts) | |
if (config.canSend[peer][replier]) | |
config.canSend[replier][peer] = true; | |
return config; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std.algorithm.iteration; | |
import std.array; | |
import std.math.rounding; | |
import std.range; | |
import std.traits; | |
import std.typecons; | |
import ae.utils.json; | |
// @ ["edges",["set"],{}] | |
// + [[411,1],[412,0],0] | |
// + [[410,1],[412,2],0] | |
// + [[293,0],[412,1],0] | |
// @ ["hubs",["set"],{}] | |
// + {"dir":false,"fixed":false,"node":412} | |
// @ ["nodes",["set"],{}] | |
// + {"angle":-2.2549667,"pos":[83.30553,5.5096917,132.92021],"up":[0,1,0]} | |
// @ ["player","credits"] | |
// - 296909 | |
// + 296901 | |
// @ ["hubs",["set"],{}] | |
// - {"dir":false,"fixed":false,"node":412} | |
// + {"dir":true,"fixed":false,"node":412} | |
// jd -set slot_{0,1}.json 51.01s user 2.62s system 272% cpu 19.667 total | |
alias WorldCoord = double[3]; | |
struct Node | |
{ | |
WorldCoord pos, up; | |
double angle; | |
} | |
alias CableColor = ubyte; | |
alias PortIndex = ubyte; | |
alias NodeIndex = size_t; | |
struct Address | |
{ | |
enum Element | |
{ | |
Zero, | |
One, | |
Two, | |
Three, | |
Wildcard, | |
} | |
Element[4] elements; | |
enum Type | |
{ | |
Endpoint, | |
Filter, | |
UnrestrictedFilter, | |
} | |
Type address_type; | |
} | |
alias NodePort = Tuple!( | |
NodeIndex, | |
PortIndex, | |
); | |
alias Edge = Tuple!( | |
NodePort, | |
NodePort, | |
CableColor, | |
); | |
enum Infection | |
{ | |
Bio, | |
StrongBio, | |
Hack, | |
} | |
struct Endpoint | |
{ | |
NodeIndex node; | |
Address address; | |
Nullable!Infection infection; | |
bool disinfection; | |
} | |
struct Relay | |
{ | |
NodeIndex node; | |
bool fixed; | |
bool light; | |
} | |
struct Filter | |
{ | |
NodeIndex node; | |
struct Config | |
{ | |
PortIndex port; | |
Address mask; | |
enum Addr { Src, Dst } | |
Addr addr = Addr.Dst; | |
enum Action { DropPacket, SendBackPacket } | |
Action action; | |
enum Op { Match, Differ } | |
Op op; | |
enum Collision { DropInbound, DropOutbound, SendBackOutbound } | |
Collision collision; | |
} | |
Config config; | |
bool fixed; | |
} | |
struct Hub | |
{ | |
NodeIndex node; | |
bool fixed; | |
bool dir; | |
} | |
struct Bridge | |
{ | |
NodeIndex node; | |
} | |
struct ChunkCoord { int x, y, z; } | |
alias ChunkRunLength = ushort; | |
enum Material : ubyte | |
{ | |
empty, | |
dirt, // diggable | |
packedDirt, | |
softGrass, // diggable; with trees | |
grass, | |
indestructibleStone, | |
steelPlates, | |
ceramicTiles, | |
flatSteelPlates, | |
stoneBricks, // looks like cobblestone from above | |
drillGoo, // diggable; user-placed | |
cobblestone, | |
mosaic, // looks like cobblestone from above; black on red | |
carpet, // looks like cobblestone from above; brown on red | |
woodPlanks, | |
crackedCeramicTiles, | |
leakingSteelPlates, | |
empty2, | |
lava, | |
empty3, | |
water, // invisible; makes splashing sounds and screen warp effect when submerged | |
water2, // ditto | |
empty4, | |
// ... lots of water variations go here ... | |
// ... TODO ... | |
stone = 43, | |
crackedStone = 44, | |
} | |
alias ChunkRun = Tuple!( | |
ChunkRunLength, | |
OriginalType!Material, | |
); | |
alias ChunkCompressedContents = ChunkRun[]; | |
alias Chunk = Tuple!( | |
ChunkCoord, | |
ChunkCompressedContents, | |
); | |
struct SaveGame | |
{ | |
JSONFragment player; | |
JSONFragment story; | |
Node[] nodes; | |
Edge[] edges; | |
Endpoint[] endpoints; | |
Relay[] relays; | |
Filter[] filters; | |
JSONFragment testers; | |
Hub[] hubs; | |
JSONFragment antennas; | |
Bridge[] bridges; | |
JSONFragment chunk_types; | |
Chunk[] chunks; | |
JSONFragment toolboxes; | |
JSONFragment pages; | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
enum chunkCells = 32; | |
alias ChunkContents = Material[chunkCells][chunkCells][chunkCells]; | |
ChunkContents decompress(const ChunkCompressedContents compressed) | |
{ | |
ChunkContents cube; | |
uint p; | |
foreach (r; compressed) | |
{ | |
auto count = r[0]; | |
auto v = r[1]; | |
foreach (i; 0 .. count) | |
{ | |
cube[p / 32 / 32][p / 32 % 32][p % 32] = cast(Material)v; | |
p++; | |
} | |
} | |
assert(p == chunkCells * chunkCells * chunkCells, "Invalid compressed chunk size"); | |
return cube; | |
} | |
ChunkCompressedContents compress(const ChunkContents cube) | |
{ | |
ChunkCompressedContents compressed; | |
foreach (z, ref plane; cube) | |
foreach (y, ref row; plane) | |
foreach (x, ref cell; row) | |
if (compressed.length > 0 && compressed[$-1][1] == cell) | |
compressed[$-1][0]++; | |
else | |
compressed ~= ChunkRun(1, cell); | |
return compressed; | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
enum chunkSize = 16; // Chunk size in world coordinates | |
WorldCoord toWorld(ChunkCoord c) | |
{ | |
return [ | |
c.x * chunkSize, | |
c.y * chunkSize, | |
c.z * chunkSize, | |
]; | |
} | |
ChunkCoord toChunk(WorldCoord c) | |
{ | |
return ChunkCoord( | |
cast(int)floor(c[0] / chunkSize), | |
cast(int)floor(c[1] / chunkSize), | |
cast(int)floor(c[2] / chunkSize), | |
); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
enum relaySize = 0.4; | |
enum hubLength = 1.0; | |
enum hubWidth = 0.5; | |
enum filterSize = 0.6; | |
/////////////////////////////////////////////////////////////////////////////// | |
Address.Element[4] parseAddress(string str) | |
{ | |
Address.Element[4] result; | |
foreach (i, part; str.split(".")) | |
result[i] = { | |
switch (part) | |
{ | |
case "*": return Address.Element.Wildcard; | |
case "0": return Address.Element.Zero; | |
case "1": return Address.Element.One; | |
case "2": return Address.Element.Two; | |
case "3": return Address.Element.Three; | |
default: throw new Exception("Invalid address element: " ~ part); | |
} | |
}(); | |
return result; | |
} | |
string toString(Address.Element element) { return "0123*"[element .. element+1]; } | |
string toString(Address.Element[4] address) { return address[].map!toString.join("."); } | |
/////////////////////////////////////////////////////////////////////////////// | |
alias HostIndex = ubyte; | |
Address.Element[4] toAddressElements(HostIndex index) | |
{ | |
return [ | |
cast(Address.Element)((index >> (3 * 2)) & 3), | |
cast(Address.Element)((index >> (2 * 2)) & 3), | |
cast(Address.Element)((index >> (1 * 2)) & 3), | |
cast(Address.Element)((index >> (0 * 2)) & 3), | |
]; | |
} | |
Address toAddress(HostIndex index, Address.Type type) | |
{ | |
return Address(toAddressElements(index), type); | |
} | |
Address wildcardAddress(Address.Type type) | |
{ | |
return Address([ | |
Address.Element.Wildcard, | |
Address.Element.Wildcard, | |
Address.Element.Wildcard, | |
Address.Element.Wildcard, | |
], type); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
bool matches(Address.Element[4] address, Address.Element[4] pattern) | |
{ | |
foreach (i, elem; pattern) | |
{ | |
assert(address[i] != Address.Element.Wildcard, "Wildcard not allowed in address"); | |
if (elem != Address.Element.Wildcard && elem != address[i]) | |
return false; | |
} | |
return true; | |
} | |
auto allHosts() | |
{ | |
return (4^^4) | |
.iota | |
.map!(hostIndex => cast(HostIndex)hostIndex); | |
} | |
auto hostsMatching(Address.Element[4] mask) | |
{ | |
return allHosts.filter!(hostIndex => hostIndex.toAddressElements.matches(mask)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment