Skip to content

Instantly share code, notes, and snippets.

@CyberShadow
Last active May 29, 2024 20:20
Show Gist options
  • Save CyberShadow/bc6ac011016b841c050fdf88816e202f to your computer and use it in GitHub Desktop.
Save CyberShadow/bc6ac011016b841c050fdf88816e202f to your computer and use it in GitHub Desktop.
Tunnet switch builder
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
);
}
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;
}
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