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.searching; | |
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.path; | |
import std.stdio : stderr; | |
import ae.sys.file; | |
import ae.utils.array; | |
import ae.utils.json; | |
import ae.utils.meta; | |
import config; | |
import game; | |
/////////////////////////////////////////////////////////////////////////////// | |
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); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
void buildThing(ref SaveGame game, Config config) | |
{ | |
const origGame = game; | |
// Configuration | |
enum numHosts = 4 ^^ 3; | |
enum terminatorSize = 4.0; | |
enum cellSize = 2.0; | |
enum portLinkSize = cellSize; | |
enum portLaneSize = 0.25; | |
enum minPortFilterSize = 4; | |
enum maxWireLength = 4.0; | |
enum ChunkCoord startChunk = {x: 5, y: 0, z: 8}; | |
auto totalSize = | |
terminatorSize + | |
cellSize * numHosts + | |
portLinkSize + | |
portLaneSize * numHosts + | |
minPortFilterSize; | |
auto totalChunks = cast(uint)ceil(totalSize / chunkSize); | |
totalSize = totalChunks * chunkSize; | |
auto startWorld = startChunk.toWorld; | |
auto objectY = startWorld[1] + 5.5; // We will place all objects at this height. | |
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[0] >= startWorld[0] && node.pos[0] < startWorld[0] + totalSize && | |
node.pos[1] == objectY && | |
node.pos[2] >= startWorld[2] && node.pos[2] < startWorld[2] + totalSize) | |
{ | |
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"); | |
bool firstRun = true; | |
if (nodeFreeList.length) | |
firstRun = false; | |
// Make chunks | |
{ | |
enum chunkX0 = startChunk.x; | |
enum chunkY = startChunk.y; | |
enum chunkZ0 = startChunk.z; | |
auto chunksToMake = totalChunks + firstRun; | |
game.chunks = game.chunks | |
.filter!(c => !(c[0].x >= chunkX0 && c[0].x < chunkX0 + chunksToMake && c[0].y == chunkY && c[0].z >= chunkZ0 && c[0].z < chunkZ0 + chunksToMake)) | |
.array; | |
foreach (chunkZ; chunkZ0 .. chunkZ0 + chunksToMake) | |
foreach (chunkX; chunkX0 .. chunkX0 + chunksToMake) | |
{ | |
ChunkContents c; | |
foreach (z, ref plane; c) | |
foreach (y, ref row; plane) | |
foreach (x, ref cell; row) | |
cell = y >= 12 && y < 30 | |
? 0 | |
: 1; | |
game.chunks ~= Chunk(ChunkCoord(chunkX, chunkY, chunkZ), 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 = false) | |
{ | |
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) | |
{ | |
bool honest = false; | |
if (honest) | |
{ | |
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); | |
addEdge(source, NodePort(relay, 0), color); | |
source = NodePort(relay, 1); | |
} | |
} | |
else | |
{ | |
// Cheat to save FPS | |
addEdge(source, target, color); | |
} | |
} | |
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"); | |
} | |
// Create the router | |
{ | |
enum Side | |
{ | |
source, | |
target, | |
} | |
// Terminators | |
// Connect to (hub, 0) | |
struct Terminator | |
{ | |
NodePort hub; | |
} | |
Terminator[numHosts][enumLength!Side] 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)); | |
terminators[side][host] = Terminator( | |
hub: NodePort(hub, 0), | |
); | |
} | |
} | |
// NxN router cells | |
struct Cell | |
{ | |
NodePort inputUp, inputDown, outputLeft, outputRight; | |
} | |
Cell[numHosts][numHosts] 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.rules.any!(rule => sourceHost.toAddressElements.matches(rule.source) && targetHost.toAddressElements.matches(rule.target)); | |
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 killSwitchFilter = addFilter( | |
pos: [x0 + cellSize * 0.725, objectY, z0 + cellSize * 0.725], | |
angle: 0, | |
config: Filter.Config( | |
// By default, rejects everything, but allows easy reconfiguration to allow everything. | |
port: 0, | |
mask: wildcardAddress(Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Dst, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, // User can toggle this to allow everything. | |
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: toAddress(sourceHost, Address.Type.UnrestrictedFilter), | |
addr: Filter.Config.Addr.Src, | |
action: Filter.Config.Action.SendBackPacket, | |
op: Filter.Config.Op.Match, | |
collision: Filter.Config.Collision.SendBackOutbound, | |
), | |
); | |
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(killSwitchFilter, 0)); | |
addEdge(NodePort(killSwitchFilter, 1), NodePort(oneWayOutputFilter, 0)); | |
addEdge(NodePort(oneWayOutputFilter, 1), NodePort(filterOutputHub, 1)); | |
cells[sourceHost][targetHost] = Cell( | |
inputUp: NodePort(filterInputHub, 2), | |
inputDown: NodePort(filterInputHub, 0), | |
outputLeft: NodePort(filterOutputHub, 0), | |
outputRight: NodePort(filterOutputHub, 2), | |
); | |
} | |
else | |
{ | |
auto inputRelay = addRelay( | |
pos: [x0 + cellSize * 1/8, objectY, z0 + cellSize * 5/8], | |
angle: -PI/2, | |
); | |
auto outputRelay = addRelay( | |
pos: [x0 + cellSize * 5/8, objectY, z0 + cellSize * 1/8], | |
angle: PI, | |
); | |
cells[sourceHost][targetHost] = Cell( | |
inputUp: NodePort(inputRelay, 1), | |
inputDown: NodePort(inputRelay, 0), | |
outputLeft: NodePort(outputRelay, 0), | |
outputRight: NodePort(outputRelay, 1), | |
); | |
} | |
} | |
// Ingress/egress port | |
struct Port | |
{ | |
NodePort peer, inputChain, outputChain; | |
} | |
Port[numHosts] ports; | |
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 inputLoopFilter = addFilter( | |
pos: [inputX0 + cellSize * 1/8, objectY, inputZ0 + cellSize * 0.5], | |
angle: 0, | |
config: Filter.Config( | |
// Sends back all packets. On collision, drop the inbound (old) 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.DropInbound, | |
), | |
); | |
auto inputOutputSplitHub = addHub( | |
pos: [inputX0 + cellSize * 1/8, objectY, inputZ0 + portLinkSize + host * portLaneSize], | |
angle: 0, | |
); | |
auto inputFilter = addFilter( | |
pos: [inputX0 + cellSize * 1/8, objectY, startWorld[2] + totalSize - 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 outputRelay = addRelay( | |
pos: [outputX0 + portLinkSize * 0.5, objectY, outputZ0 + cellSize * 1/8], | |
angle: 0, | |
); | |
auto outputToCornerRelay = addRelay( | |
pos: [outputX0 + portLinkSize + host * portLaneSize, objectY, outputZ0 + cellSize * 1/8], | |
angle: 0, | |
); | |
auto cornerRelay = addRelay( | |
pos: [outputX0 + portLinkSize + host * portLaneSize, objectY, inputZ0 + portLinkSize + host * portLaneSize], | |
angle: 0, | |
); | |
addRelays(NodePort(inputFilter, 1), NodePort(inputOutputSplitHub, 0)); | |
addRelays(NodePort(inputOutputSplitHub, 1), NodePort(inputLoopFilter, 0)); | |
addRelays(NodePort(outputRelay, 1), NodePort(outputToCornerRelay, 0)); | |
addRelays(NodePort(outputToCornerRelay, 1), NodePort(cornerRelay, 0)); | |
addRelays(NodePort(cornerRelay, 1), NodePort(inputOutputSplitHub, 2)); | |
ports[host] = Port( | |
peer: NodePort(inputFilter, 0), | |
inputChain: NodePort(inputLoopFilter, 1), | |
outputChain: NodePort(outputRelay, 0), | |
); | |
} | |
// Wire everything together | |
// Input chain | |
foreach (HostIndex sourceHost; 0 .. numHosts) | |
foreach (HostIndex targetHost; 0 .. numHosts) | |
{ | |
// Connect to terminator | |
if (targetHost == 0) | |
addEdge(cells[sourceHost][targetHost].inputUp, terminators[Side.source][sourceHost].hub); | |
// Daisy-chain | |
if (targetHost > 0) | |
addEdge(cells[sourceHost][targetHost - 1].inputDown, cells[sourceHost][targetHost].inputUp); | |
// Connect to input port | |
if (targetHost + 1 == numHosts) | |
addEdge(ports[sourceHost].inputChain, cells[sourceHost][targetHost].inputDown); | |
} | |
// Output chain | |
foreach (HostIndex sourceHost; 0 .. numHosts) | |
foreach (HostIndex targetHost; 0 .. numHosts) | |
{ | |
// Connect to terminator | |
if (sourceHost == 0) | |
addEdge(cells[sourceHost][targetHost].outputLeft, terminators[Side.target][targetHost].hub); | |
// Daisy-chain | |
if (sourceHost > 0) | |
addEdge(cells[sourceHost - 1][targetHost].outputRight, cells[sourceHost][targetHost].outputLeft); | |
// Connect to output port | |
if (sourceHost + 1 == numHosts) | |
addEdge(cells[sourceHost][targetHost].outputRight, ports[targetHost].outputChain); | |
} | |
// Reconnect peers | |
foreach (HostIndex host; 0 .. numHosts) | |
reconnect(ports[host].peer); | |
} | |
} | |
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 | |
{ | |
struct Rule | |
{ | |
Address.Element[4] source, target; | |
} | |
Rule[] rules; | |
} | |
Config readConfig() | |
{ | |
Config config; | |
foreach (line; readText("rules.txt").splitLines) | |
{ | |
if (!line.length || line[0] == '#') | |
continue; | |
auto rule = line.parseRule(); | |
config.rules ~= rule; | |
} | |
return config; | |
} | |
Config.Rule parseRule(string line) | |
{ | |
auto parts = line.findSplit("\t"); | |
auto source = parseAddress(parts[0]); | |
auto target = parseAddress(parts[2]); | |
return Config.Rule(source, target); | |
} | |
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; | |
} |
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.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 ChunkCoord { int x, y, z; } | |
alias ChunkRunLength = ushort; | |
alias Material = ubyte; | |
alias ChunkRun = Tuple!( | |
ChunkRunLength, | |
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; | |
JSONFragment 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] = 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), | |
); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
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("."); } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment