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.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 std.typecons;
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;
// 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 totalSize =
terminatorSize +
cellSize * numHosts +
portLinkSize +
portLaneSize * numHosts +
minPortFilterSize;
auto totalChunks = cast(int)ceil(totalSize / chunkSize);
totalSize = totalChunks * 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 + totalChunks)
foreach (x; startChunk.x .. startChunk.x + totalChunks)
chunkPlan[ChunkCoord(x, startChunk.y, z)] = ChunkPlan(ChunkGeometry.flat, false);
foreach (x; startChunk.x .. startChunk.x + totalChunks)
chunkPlan[ChunkCoord(x, startChunk.y, startChunk.z + totalChunks)] = ChunkPlan(ChunkGeometry.flat, true);
// Bus
auto busChunkZ = startChunk.z + totalChunks + 1;
foreach (x; startChunk.x .. startChunk.x + totalChunks)
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 &&
node.pos.toChunk in chunkPlan &&
chunkPlan[node.pos.toChunk].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 = 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)
{
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);
}
}
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 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.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 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(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);
}
}
// Switch edge port to endpoint
NodePort[numHosts] peerPorts;
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));
addRelays(cellPorts[Side.source][host], NodePort(inputLoopFilter, 1));
addRelays(cellPorts[Side.target][host], NodePort(outputRelay, 0));
peerPorts[host] = NodePort(inputFilter, 0);
}
// Reconnect peers
foreach (HostIndex host; 0 .. numHosts)
reconnect(peerPorts[host]);
}
// The bus
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));
}
}
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
{
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;
}
import std.algorithm.iteration;
import std.array;
import std.math.rounding;
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 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;
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] = 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),
);
}
///////////////////////////////////////////////////////////////////////////////
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