wired/tools/LDtkImport.zig

691 lines
29 KiB
Zig
Raw Normal View History

2022-08-03 16:23:59 -06:00
//! Uses zig-ldtk to convert a ldtk file into a binary format for wired
const std = @import("std");
const LDtk = @import("../deps/zig-ldtk/src/LDtk.zig");
2022-08-03 22:06:17 -06:00
const world = @import("../src/world.zig");
2022-08-03 16:23:59 -06:00
const KB = 1024;
const MB = 1024 * KB;
const LDtkImport = @This();
step: std.build.Step,
builder: *std.build.Builder,
source_path: std.build.FileSource,
output_name: []const u8,
2022-08-04 01:59:32 -06:00
world_data: std.build.GeneratedFile,
2022-08-03 16:23:59 -06:00
pub fn create(b: *std.build.Builder, opt: struct {
source_path: std.build.FileSource,
output_name: []const u8,
}) *@This() {
var result = b.allocator.create(LDtkImport) catch @panic("memory");
result.* = LDtkImport{
.step = std.build.Step.init(.custom, "convert and embed a ldtk map file", b.allocator, make),
.builder = b,
.source_path = opt.source_path,
.output_name = opt.output_name,
2022-08-04 01:59:32 -06:00
.world_data = undefined,
2022-08-03 16:23:59 -06:00
};
2022-08-04 01:59:32 -06:00
result.*.world_data = std.build.GeneratedFile{ .step = &result.*.step };
2022-08-03 16:23:59 -06:00
return result;
}
fn make(step: *std.build.Step) !void {
const this = @fieldParentPtr(LDtkImport, "step", step);
const allocator = this.builder.allocator;
const cwd = std.fs.cwd();
// Get path to source and output
const source_src = this.source_path.getPath(this.builder);
const output = this.builder.getInstallPath(.lib, this.output_name);
// Open ldtk file and read all of it into `source`
const source_file = try cwd.openFile(source_src, .{});
defer source_file.close();
const source = try source_file.readToEndAlloc(allocator, 10 * MB);
defer allocator.free(source);
var ldtk_parser = try LDtk.parse(allocator, source);
defer ldtk_parser.deinit();
const ldtk = ldtk_parser.root;
2022-08-03 16:23:59 -06:00
2022-08-06 01:15:38 -06:00
// Store levels
var levels = std.ArrayList(world.Level).init(allocator);
defer levels.deinit();
2022-08-06 01:15:38 -06:00
for (ldtk.levels) |level| {
var entity_array = std.ArrayList(world.Entity).init(allocator);
defer entity_array.deinit();
2022-08-03 23:50:50 -06:00
const parsed_level = try parseLevel(.{
.allocator = allocator,
.ldtk = ldtk,
.level = level,
.entity_array = &entity_array,
});
2022-08-03 23:50:50 -06:00
2022-08-06 01:15:38 -06:00
try levels.append(parsed_level);
}
defer for (levels.items) |level| {
allocator.free(level.tiles.?);
allocator.free(level.entities.?);
};
2022-08-07 00:16:00 -06:00
var circuit = try buildCircuit(allocator, levels.items);
defer circuit.deinit();
// TODO
for (circuit.items) |node, i| {
std.log.warn("{:0>2}: {}", .{ i, node });
2022-08-07 00:16:00 -06:00
}
2022-08-06 01:15:38 -06:00
// Calculate the offset of each level and store it in the headers.
// Offset is relative to the beginning of level.data
var level_headers = std.ArrayList(world.LevelHeader).init(allocator);
defer level_headers.deinit();
for (levels.items) |level, i| {
if (level_headers.items.len == 0) {
try level_headers.append(.{
.x = level.world_x,
.y = level.world_y,
.offset = 0,
});
continue;
}
const last_offset = level_headers.items[i - 1].offset;
const last_size = try levels.items[i - 1].calculateSize();
const offset = @intCast(u16, last_offset + last_size);
try level_headers.append(.{
.x = level.world_x,
.y = level.world_y,
.offset = offset,
});
}
// Create array to write data to
var data = std.ArrayList(u8).init(allocator);
defer data.deinit();
const writer = data.writer();
2022-08-07 15:56:26 -06:00
try world.write(writer, level_headers.items, circuit.items, levels.items);
2022-08-06 01:15:38 -06:00
// Open output file and write data into it
2022-08-03 16:23:59 -06:00
cwd.makePath(this.builder.getInstallPath(.lib, "")) catch |e| switch (e) {
error.PathAlreadyExists => {},
else => return e,
};
try cwd.writeFile(output, data.items);
2022-08-04 01:59:32 -06:00
this.world_data.path = output;
2022-08-03 16:23:59 -06:00
}
2022-08-06 01:15:38 -06:00
/// Returns parsed level. User owns level.tiles
fn parseLevel(opt: struct {
allocator: std.mem.Allocator,
ldtk: LDtk.Root,
level: LDtk.Level,
entity_array: *std.ArrayList(world.Entity),
}) !world.Level {
const ldtk = opt.ldtk;
const level = opt.level;
const entity_array = opt.entity_array;
const allocator = opt.allocator;
const layers = level.layerInstances orelse return error.NoLayers;
2022-08-06 01:15:38 -06:00
const world_x: i8 = @intCast(i8, @divExact(level.worldX, (ldtk.worldGridWidth orelse 160)));
const world_y: i8 = @intCast(i8, @divExact(level.worldY, (ldtk.worldGridHeight orelse 160)));
var circuit_layer: ?LDtk.LayerInstance = null;
var collision_layer: ?LDtk.LayerInstance = null;
for (layers) |layer| {
if (std.mem.eql(u8, layer.__identifier, "Entities")) {
// Entities
std.debug.assert(layer.__type == .Entities);
for (layer.entityInstances) |entity| {
var kind_opt: ?world.EntityKind = null;
if (std.mem.eql(u8, entity.__identifier, "Player")) {
kind_opt = .Player;
} else if (std.mem.eql(u8, entity.__identifier, "Wire")) {
kind_opt = .WireNode;
} else if (std.mem.eql(u8, entity.__identifier, "Coin")) {
kind_opt = .Coin;
} else if (std.mem.eql(u8, entity.__identifier, "Door")) {
kind_opt = .Door;
} else if (std.mem.eql(u8, entity.__identifier, "Trapdoor")) {
kind_opt = .Trapdoor;
}
2022-08-07 00:16:00 -06:00
// Parsing code for wire entities. They're a little more complex
// than the rest
if (kind_opt) |kind| {
if (kind != .WireNode) {
const world_entity = world.Entity{
.kind = kind,
.x = @intCast(i16, entity.__grid[0]),
.y = @intCast(i16, entity.__grid[1]),
};
try entity_array.append(world_entity);
} else {
const p1_x: i16 = @intCast(i16, entity.__grid[0]);
const p1_y: i16 = @intCast(i16, entity.__grid[1]);
var anchor1 = false;
var anchor2 = false;
var p2_x: i16 = p1_x;
var p2_y: i16 = p1_y;
for (entity.fieldInstances) |field| {
if (std.mem.eql(u8, field.__identifier, "Anchor")) {
const anchors = field.__value.Array.items;
anchor1 = anchors[0].Bool;
anchor2 = anchors[1].Bool;
} else if (std.mem.eql(u8, field.__identifier, "Point")) {
const end = field.__value.Array.items.len - 1;
const endpoint = field.__value.Array.items[end];
const x = endpoint.Object.get("cx").?;
const y = endpoint.Object.get("cy").?;
p2_x = @intCast(i16, x.Integer);
p2_y = @intCast(i16, y.Integer);
}
}
const wire_begin = world.Entity{
.kind = if (anchor1) .WireAnchor else .WireNode,
.x = p1_x,
.y = p1_y,
};
try entity_array.append(wire_begin);
const wire_end = world.Entity{
.kind = if (anchor2) .WireEndAnchor else .WireEndNode,
.x = p2_x,
.y = p2_y,
};
try entity_array.append(wire_end);
}
}
}
} else if (std.mem.eql(u8, layer.__identifier, "Circuit")) {
// Circuit
std.debug.assert(layer.__type == .IntGrid);
circuit_layer = layer;
} else if (std.mem.eql(u8, layer.__identifier, "Collision")) {
// Collision
std.debug.assert(layer.__type == .IntGrid);
collision_layer = layer;
} else {
// Unknown
std.log.warn("{s}: {}", .{ layer.__identifier, layer.__type });
}
}
if (circuit_layer == null) return error.MissingCircuitLayer;
if (collision_layer == null) return error.MissingCollisionLayer;
const circuit = circuit_layer.?;
const collision = collision_layer.?;
std.debug.assert(circuit.__cWid == collision.__cWid);
std.debug.assert(circuit.__cHei == collision.__cHei);
const width = @intCast(u16, circuit.__cWid);
const size = @intCast(u16, width * circuit.__cHei);
var parsed_level = world.Level{
.world_x = world_x,
.world_y = world_y,
.width = @intCast(u16, width),
.size = @intCast(u16, size),
.entity_count = @intCast(u16, entity_array.items.len),
2022-08-06 01:15:38 -06:00
.tiles = try allocator.alloc(world.TileData, size),
.entities = try allocator.dupe(world.Entity, entity_array.items),
};
const tiles = parsed_level.tiles.?;
// Add unchanged tile data
for (collision.autoLayerTiles) |autotile| {
const x = @divExact(autotile.px[0], collision.__gridSize);
const y = @divExact(autotile.px[1], collision.__gridSize);
const i = @intCast(usize, x + y * width);
const sx = @divExact(autotile.src[0], collision.__gridSize);
const sy = @divExact(autotile.src[1], collision.__gridSize);
const t = sx + sy * 16;
tiles[i] = world.TileData{ .tile = @intCast(u7, t) };
}
// Add circuit tiles
for (circuit.intGridCsv) |cir64, i| {
const cir = @intCast(u4, cir64);
const col = collision.intGridCsv[i];
if (col == 0 or col == 1) {
tiles[i] = world.TileData{ .flags = .{
.solid = col == 1,
2022-08-07 00:16:00 -06:00
.circuit = @intToEnum(world.CircuitType, cir),
} };
}
}
return parsed_level;
}
2022-08-07 00:16:00 -06:00
pub fn buildCircuit(alloc: std.mem.Allocator, levels: []world.Level) !std.ArrayList(world.CircuitNode) {
const Coord = world.Coordinate;
2022-08-07 00:16:00 -06:00
const SearchItem = struct {
coord: Coord,
last_coord: ?Coord = null,
2022-08-07 15:56:26 -06:00
last_node: world.NodeID,
2022-08-07 00:16:00 -06:00
};
const Queue = std.TailQueue(SearchItem);
const Node = Queue.Node;
var nodes = std.ArrayList(world.CircuitNode).init(alloc);
var sources = Queue{};
var sockets = Queue{};
2022-08-07 00:16:00 -06:00
var level_hashmap = std.AutoHashMap(u16, world.Level).init(alloc);
defer level_hashmap.deinit();
for (levels) |level| {
const id: u16 = @bitCast(u8, level.world_x) | @intCast(u16, @bitCast(u8, level.world_y)) << 8;
// So we can quickly find levels
try level_hashmap.put(id, level);
// Use a global coordinate system for our algorithm
const global_x = @intCast(i16, level.world_x) * 20;
const global_y = @intCast(i16, level.world_y) * 20;
2022-08-07 00:16:00 -06:00
for (level.tiles orelse continue) |tileData, i| {
const x = global_x + @intCast(i16, @mod(i, level.width));
const y = global_y + @intCast(i16, @divTrunc(i, level.width));
2022-08-07 00:16:00 -06:00
const coordinate = try alloc.create(Node);
coordinate.* = .{ .data = .{
2022-08-07 15:56:26 -06:00
.last_node = @intCast(world.NodeID, nodes.items.len),
.coord = Coord.init(.{ x, y }),
} };
2022-08-07 00:16:00 -06:00
switch (tileData) {
.tile => |_| {
// Do nothing
},
.flags => |flags| {
switch (flags.circuit) {
.Source => {
try nodes.append(.{ .kind = .Source, .coord = Coord.init(.{ x, y }) });
2022-08-07 00:16:00 -06:00
sources.append(coordinate);
},
.Socket => {
// try nodes.append(.{ .kind = .{ .Plug = null } });
2022-08-07 15:56:26 -06:00
coordinate.data.last_node = std.math.maxInt(world.NodeID);
sockets.append(coordinate);
},
2022-08-07 00:16:00 -06:00
else => {
// Do nothing
},
}
},
}
}
}
var visited = std.AutoHashMap(Coord, void).init(alloc);
2022-08-07 12:47:44 -06:00
defer visited.deinit();
var multi_input = std.AutoHashMap(Coord, usize).init(alloc);
2022-08-07 12:47:44 -06:00
defer multi_input.deinit();
2022-08-07 00:16:00 -06:00
var bfs_queue = Queue{};
var run: usize = 0;
while (run < 2) : (run += 1) {
if (run == 0) bfs_queue.concatByMoving(&sources);
if (run == 1) bfs_queue.concatByMoving(&sockets);
2022-08-07 00:16:00 -06:00
// bfs_queue.concatByMoving(&outlets);
while (bfs_queue.popFirst()) |node| {
// Make sure we clean up the node's memory
defer alloc.destroy(node);
const coord = node.data.coord;
if (visited.contains(coord)) continue;
try visited.put(coord, .{});
// TODO remove magic numbers
const worldc = coord.toWorld();
const id: u16 = @bitCast(u8, worldc[0]) | @intCast(u16, @bitCast(u8, worldc[1])) << 8;
2022-08-07 00:16:00 -06:00
// const level_opt: ?world.Level = level_hashmap.get(.{ world_x, world_y });
if (level_hashmap.getPtr(id) != null) {
const level = level_hashmap.getPtr(id);
const last_node = node.data.last_node;
var next_node = last_node;
const tile = level.?.getTile(coord).?;
2022-08-07 00:16:00 -06:00
if (tile != .flags) continue;
const flags = tile.flags;
switch (flags.circuit) {
2022-08-07 15:56:26 -06:00
.Source => {}, // Do nothing, but add everything around the source
.Conduit => {
// Collects from two other nodes. Intersections will need to be stored so when
// we find out we have to outputs, we can add the conduit and possible rewrite
// previous nodes to point to the conduit
2022-08-07 00:16:00 -06:00
// TODO
},
.Socket => {
next_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .Socket = null },
.coord = coord,
});
},
.Plug => {
// Plugs by their nature end a conduit path, so don't add
// surrounding tiles.
try nodes.append(.{
.kind = .{ .Plug = last_node },
.coord = coord,
});
continue;
2022-08-07 00:16:00 -06:00
},
.Outlet => {
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .Outlet = last_node },
.coord = coord,
});
2022-08-07 00:16:00 -06:00
},
.Switch_Off, .Switch_On => {
// Identify input side
const last_coord = node.data.last_coord.?;
const Dir = enum { North, West, East, South };
const input_dir: Dir = dir: {
if (last_coord.eq(coord.add(.{ 0, -1 }))) {
break :dir .North;
} else if (last_coord.eq(coord.add(.{ -1, 0 }))) {
break :dir .West;
} else if (last_coord.eq(coord.add(.{ 1, 0 }))) {
break :dir .East;
} else {
break :dir .South;
}
};
// Find outlets
const north_opt = level.?.getTile(coord.add(.{ 0, -1 })).?.getCircuit();
const west_opt = level.?.getTile(coord.add(.{ -1, 0 })).?.getCircuit();
const east_opt = level.?.getTile(coord.add(.{ 1, 0 })).?.getCircuit();
const south_opt = level.?.getTile(coord.add(.{ 0, 1 })).?.getCircuit();
const north = (north_opt orelse world.CircuitType.None) != .None;
const west = (west_opt orelse world.CircuitType.None) != .None;
const east = (east_opt orelse world.CircuitType.None) != .None;
const south = (south_opt orelse world.CircuitType.None) != .None;
// We don't have four way switches, don't allow them
std.debug.assert(west != true or east != true);
// We only have vertically oriented switches ATM
std.debug.assert(north == true and south == true);
// Determine initial state of switch
const state: u8 = state: {
// Vertical switch
if (!west and !east) {
if (flags.circuit == .Switch_Off) break :state 0;
break :state 1;
}
if (east and !west) {
if (flags.circuit == .Switch_Off) break :state 0;
break :state 1;
}
if (west and !east) {
if (flags.circuit == .Switch_Off) break :state 0;
break :state 1;
}
return error.ImpossibleSwitchState;
};
// Add switch
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .Switch = .{
.source = last_node,
.state = state,
} },
.coord = coord,
});
// Add switch outlets
if (input_dir != .West and west) {
const out_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .SwitchOutlet = .{
.source = next_node,
.which = 0,
} },
.coord = coord,
});
const right = try alloc.create(Node);
right.* = Node{ .data = .{
.last_node = out_node,
.coord = coord.add(.{ 1, 0 }),
.last_coord = coord,
} };
bfs_queue.append(right);
}
if (input_dir != .East and east) {
const out_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .SwitchOutlet = .{
.source = next_node,
.which = 0,
} },
.coord = coord,
});
const left = try alloc.create(Node);
left.* = Node{ .data = .{
.last_node = out_node,
.coord = coord.add(.{ -1, 0 }),
.last_coord = coord,
} };
bfs_queue.append(left);
}
if (input_dir != .South and south) {
const out_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .SwitchOutlet = .{
.source = next_node,
.which = 1,
} },
.coord = coord,
});
const down = try alloc.create(Node);
down.* = Node{ .data = .{
.last_node = out_node,
.coord = coord.add(.{ 0, 1 }),
.last_coord = coord,
} };
bfs_queue.append(down);
}
if (input_dir != .North and north) {
const out_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .SwitchOutlet = .{
.source = next_node,
.which = 1,
} },
.coord = coord,
});
const up = try alloc.create(Node);
up.* = Node{ .data = .{
.last_node = out_node,
.coord = coord.add(.{ 0, -1 }),
.last_coord = coord,
} };
bfs_queue.append(up);
}
continue;
2022-08-07 00:16:00 -06:00
},
.Join => {
const last_coord = node.data.last_coord.?;
if (last_coord.toLevelTopLeft().eq(coord.toLevelTopLeft())) {
std.log.warn("Join first side", .{});
} else {
std.log.warn("Join second side", .{});
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
try nodes.append(.{
.kind = .{ .Join = last_node },
.coord = coord,
});
}
2022-08-07 00:16:00 -06:00
},
.And => {
// TODO: verify And gate is properly connected. A source node
// should never feed directly into an And gate output. Inputs
// should be to the left and right.
2022-08-07 12:47:44 -06:00
const last_coord = node.data.last_coord.?;
const Side = enum { O, L, R };
const side: Side =
if (last_coord.val[0] == coord.val[0] - 1)
2022-08-07 12:47:44 -06:00
Side.L
else if (last_coord.val[0] == coord.val[0] + 1)
2022-08-07 12:47:44 -06:00
Side.R
else
Side.O;
// std.log.warn("{any}: {}", .{ coord, side });
if (multi_input.get(coord)) |a| {
switch (side) {
.L => {
// std.log.warn("Filling left", .{});
nodes.items[a].kind.And[0] = last_node;
},
.R => {
// std.log.warn("Filling right", .{});
nodes.items[a].kind.And[1] = last_node;
},
else => {}, // reverse connection
2022-08-07 12:47:44 -06:00
}
} else {
_ = visited.remove(coord);
if (side == .O) {
// TODO: reverse the path, since the search path
// may have come from a plug
2022-08-07 12:47:44 -06:00
return error.OutputToSource;
} else if (side == .L) {
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
2022-08-07 12:47:44 -06:00
try nodes.append(.{
2022-08-07 15:56:26 -06:00
.kind = .{ .And = .{ last_node, std.math.maxInt(world.NodeID) } },
2022-08-07 12:47:44 -06:00
.coord = coord,
});
} else if (side == .R) {
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
2022-08-07 12:47:44 -06:00
try nodes.append(.{
2022-08-07 15:56:26 -06:00
.kind = .{ .And = .{ std.math.maxInt(world.NodeID), last_node } },
2022-08-07 12:47:44 -06:00
.coord = coord,
});
}
try multi_input.put(coord, next_node);
}
2022-08-07 00:16:00 -06:00
},
.Xor => {
// TODO: verify Xor gate is properly connected
2022-08-07 12:51:20 -06:00
const last_coord = node.data.last_coord.?;
const Side = enum { O, L, R };
const side: Side =
if (last_coord.val[0] == coord.val[0] - 1)
2022-08-07 12:51:20 -06:00
Side.L
else if (last_coord.val[0] == coord.val[0] + 1)
2022-08-07 12:51:20 -06:00
Side.R
else
Side.O;
// std.log.warn("{any}: {}", .{ coord, side });
if (multi_input.get(coord)) |a| {
switch (side) {
.L => {
// std.log.warn("Filling left", .{});
nodes.items[a].kind.Xor[0] = last_node;
},
.R => {
// std.log.warn("Filling right", .{});
nodes.items[a].kind.Xor[1] = last_node;
},
else => {}, // reverse connection
2022-08-07 12:51:20 -06:00
}
} else {
_ = visited.remove(coord);
if (side == .O) {
// TODO: reverse the path, since the search path
// may have come from a plug
2022-08-07 12:51:20 -06:00
return error.OutputToSource;
} else if (side == .L) {
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
2022-08-07 12:51:20 -06:00
try nodes.append(.{
2022-08-07 15:56:26 -06:00
.kind = .{ .Xor = .{ last_node, std.math.maxInt(world.NodeID) } },
2022-08-07 12:51:20 -06:00
.coord = coord,
});
} else if (side == .R) {
2022-08-07 15:56:26 -06:00
next_node = @intCast(world.NodeID, nodes.items.len);
2022-08-07 12:51:20 -06:00
try nodes.append(.{
2022-08-07 15:56:26 -06:00
.kind = .{ .Xor = .{ std.math.maxInt(world.NodeID), last_node } },
2022-08-07 12:51:20 -06:00
.coord = coord,
});
}
try multi_input.put(coord, next_node);
}
2022-08-07 00:16:00 -06:00
},
.None => continue,
2022-08-07 00:16:00 -06:00
}
const right = try alloc.create(Node);
const left = try alloc.create(Node);
const down = try alloc.create(Node);
const up = try alloc.create(Node);
right.* = Node{ .data = .{
.last_node = next_node,
.coord = coord.add(.{ 1, 0 }),
2022-08-07 12:47:44 -06:00
.last_coord = coord,
2022-08-07 00:16:00 -06:00
} };
left.* = Node{ .data = .{
.last_node = next_node,
.coord = coord.add(.{ -1, 0 }),
2022-08-07 12:47:44 -06:00
.last_coord = coord,
2022-08-07 00:16:00 -06:00
} };
down.* = Node{ .data = .{
.last_node = next_node,
.coord = coord.add(.{ 0, 1 }),
2022-08-07 12:47:44 -06:00
.last_coord = coord,
2022-08-07 00:16:00 -06:00
} };
up.* = Node{ .data = .{
.last_node = next_node,
.coord = coord.add(.{ 0, -1 }),
2022-08-07 12:47:44 -06:00
.last_coord = coord,
2022-08-07 00:16:00 -06:00
} };
bfs_queue.append(right);
bfs_queue.append(left);
bfs_queue.append(down);
bfs_queue.append(up);
}
}
}
2022-08-07 12:47:44 -06:00
var i: usize = 0;
while (i < nodes.items.len) : (i += 1) {
switch (nodes.items[i].kind) {
// .Source => {
// },
.And => {},
.Xor => {},
.Conduit => {},
.Plug => {},
.Switch => {},
.Join => {},
.Outlet => {},
else => {},
}
}
2022-08-07 00:16:00 -06:00
return nodes;
}