add support for parsing chunked maps

This commit is contained in:
Rokas Puzonas 2026-09-21 02:16:36 +03:00
parent e0d0f479be
commit 96cfcd4626
11 changed files with 799 additions and 725 deletions

View File

@ -1,57 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="0" nextlayerid="6" nextobjectid="4"> <map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="1" nextlayerid="8" nextobjectid="15">
<tileset firstgid="1" source="tileset.tsx"/> <tileset firstgid="1" source="tileset.tsx"/>
<layer id="2" name="Ground" width="30" height="20"> <layer id="2" name="Ground" width="30" height="20">
<data encoding="csv"> <data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, <chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, </chunk>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, </data>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer> </layer>
<layer id="1" name="Foreground" width="30" height="20"> <layer id="1" name="Foreground" width="30" height="20">
<data encoding="csv"> <data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, <chunk x="0" y="0" width="16" height="16">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,98,98,101,98,98,98,98,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,98,0,0,98,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,98,101,98,98,98,98,98,0,0,
0,0,0,0,0,0,98,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,0,0,98,0,0,0,98,0,0,
0,0,0,0,0,0,98,0,7,0,0,26,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,0,0,0,0,0,0,98,0,0,
0,0,0,0,0,0,98,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,0,7,0,0,26,0,98,0,0,
0,0,0,0,0,0,98,0,0,98,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,0,0,0,0,0,0,101,0,0,
0,0,0,0,0,0,98,98,98,98,98,101,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,0,0,98,0,0,0,101,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,98,98,98,98,98,101,101,101,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, </chunk>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, </data>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer> </layer>
<objectgroup id="5" name="Markers"> <objectgroup id="5" name="Markers">
<object id="1" name="spawn" x="484" y="555"> <object id="1" name="spawn" x="484" y="555">
<point/> <point/>
</object> </object>
</objectgroup> </objectgroup>
<objectgroup id="7" name="Level bounds"/>
</map> </map>

View File

@ -13,7 +13,8 @@
} }
}, },
"tileset.tsx": { "tileset.tsx": {
"scaleInDock": 1 "scaleInDock": 0.5,
"scaleInEditor": 1
} }
}, },
"last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban", "last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
@ -24,7 +25,9 @@
"map.tmx" "map.tmx"
], ],
"project": "sokoban.tiled-project", "project": "sokoban.tiled-project",
"property.type": "int",
"recentFiles": [ "recentFiles": [
"tileset.tsx",
"map.tmx" "map.tmx"
], ],
"tileset.lastUsedFormat": "tsx", "tileset.lastUsedFormat": "tsx",

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const Property = @import("./property.zig"); const Property = @import("./property.zig");
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
@ -11,41 +12,104 @@ const Layer = @This();
pub const TileVariant = struct { pub const TileVariant = struct {
width: u32, width: u32,
height: u32, height: u32,
data: []u32, chunks: []Chunk,
fn initDataFromXml( const Chunk = struct {
x: i32,
y: i32,
width: u32,
height: u32,
data: []u32,
};
fn parseCSVData(arena: std.mem.Allocator, csv: []const u8) ![]u32 {
const cell_count = std.mem.countScalar(u8, csv, ',')+1;
var cells: std.ArrayList(u32) = try .initCapacity(arena, cell_count);
var split_iter = std.mem.splitScalar(u8, csv, ',');
while (split_iter.next()) |raw_tile_id| {
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace);
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
cells.appendAssumeCapacity(tile_id);
}
return cells.items;
}
fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, doc: xml.Document,
lexer: *xml.Lexer, element_id: xml.Element.Id,
) ![]u32 { ) !TileVariant {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "layer");
const attrs = try iter.begin("data"); const attrs = tag.variant.tag.attributes;
const encoding = attrs.get("encoding") orelse "csv"; const layer_width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const layer_height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
const data_id = tag.tree.first_child;
var data_element = try doc.getTagExpect(data_id, "data");
const encoding = data_element.variant.tag.attributes.get("encoding") orelse "csv";
// TODO: compression // TODO: compression
var temp_tiles: std.ArrayList(u32) = .empty; var chunks: std.ArrayList(Chunk) = .empty;
if (std.mem.eql(u8, encoding, "csv")) { if (std.mem.eql(u8, encoding, "csv")) {
const text = try lexer.nextExpectText(); try chunks.ensureTotalCapacityPrecise(arena, doc.countChildren(data_id));
var split_iter = std.mem.splitScalar(u8, text, ',');
while (split_iter.next()) |raw_tile_id| { var iter = doc.iterChildren(data_id);
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace); while (iter.next()) |child_id| {
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10); const child = doc.get(child_id).?;
try temp_tiles.append(scratch.allocator(), tile_id); if (child.variant == .text) {
const data = try parseCSVData(arena, child.variant.text);
assert(data.len == layer_width * layer_height);
chunks.appendAssumeCapacity(Chunk{
.data = data,
.x = 0,
.y = 0,
.width = layer_width,
.height = layer_height,
});
} else if (child.variant == .tag and std.mem.eql(u8, child.variant.tag.name, "chunk")) {
const chunk_attrs = child.variant.tag.attributes;
const x = try chunk_attrs.getNumber(i32, "x") orelse return error.MissingWidth;
const y = try chunk_attrs.getNumber(i32, "y") orelse return error.MissingWidth;
const width = try chunk_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try chunk_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
const csv_text = doc.get(child.tree.first_child).?.variant.text; // TODO: Add checks
const data = try parseCSVData(arena, csv_text);
assert(data.len == width * height);
chunks.appendAssumeCapacity(Chunk{
.data = data,
.x = x,
.y = y,
.width = width,
.height = height,
});
}
} }
} else { } else {
return error.UnknownEncodingType; return error.UnknownEncodingType;
} }
try iter.finish("data"); return TileVariant{
.width = layer_width,
return try arena.dupe(u32, temp_tiles.items); .height = layer_height,
.chunks = chunks.items
};
} }
pub fn get(self: TileVariant, x: usize, y: usize) ?u32 { pub fn get(self: TileVariant, x: i32, y: i32) ?u32 {
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) { for (self.chunks) |chunk| {
return self.data[y * self.width + x]; const ox = x - chunk.x;
const oy = y - chunk.y;
if ((0 <= ox and ox < chunk.width) and (0 <= oy and oy < chunk.height)) {
return chunk.data[@as(u32, @intCast(oy)) * chunk.width + @as(u32, @intCast(ox))];
}
} }
return null; return null;
} }
@ -59,9 +123,9 @@ pub const ImageVariant = struct {
width: ?u32, width: ?u32,
height: ?u32, height: ?u32,
fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "image");
const attrs = try iter.begin("image"); const attrs = tag.variant.tag.attributes;
// TODO: format // TODO: format
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource; const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
@ -69,8 +133,6 @@ pub const ImageVariant = struct {
const height = try attrs.getNumber(u32, "height") orelse null; const height = try attrs.getNumber(u32, "height") orelse null;
const transparent_color = try attrs.getColor("trans", false); const transparent_color = try attrs.getColor("trans", false);
try iter.finish("image");
return Image{ return Image{
.source = source, .source = source,
.transparent_color = transparent_color, .transparent_color = transparent_color,
@ -126,15 +188,6 @@ pub const Type = enum {
.{ "imagelayer", .image }, .{ "imagelayer", .image },
.{ "group", .group } .{ "group", .group }
}); });
fn toXmlName(self: Type) []const u8 {
return switch (self) {
.tile => "layer",
.object => "objectgroup",
.image => "imagelayer",
.group => "group",
};
}
}; };
pub const Variant = union(Type) { pub const Variant = union(Type) {
@ -160,15 +213,13 @@ variant: Variant,
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer, doc: xml.Document,
element_id: xml.Element.Id
) !Layer { ) !Layer {
const value = try lexer.peek() orelse return error.MissingStartTag; const tag = doc.getTag(element_id) orelse return error.TagNotFound;
if (value != .start_tag) return error.MissingStartTag;
var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType; const layer_type = Type.name_map.get(tag.variant.tag.name) orelse return error.UnknownLayerType;
const attrs = tag.variant.tag.attributes;
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin(layer_type.toXmlName());
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse ""; const name = try attrs.getDupe(arena, "name") orelse "";
@ -184,14 +235,12 @@ pub fn initFromXml(
var variant: Variant = undefined; var variant: Variant = undefined;
switch (layer_type) { switch (layer_type) {
.tile => { .tile => {
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; variant = Variant{
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; .tile = try TileVariant.initFromXml(
variant = .{ arena,
.tile = TileVariant{ doc,
.width = width, element_id
.height = height, )
.data = &[0]u32{}
}
}; };
}, },
.image => { .image => {
@ -230,39 +279,27 @@ pub fn initFromXml(
var objects: std.ArrayList(Object) = .empty; var objects: std.ArrayList(Object) = .empty;
var layers: std.ArrayList(Layer) = .empty; var layers: std.ArrayList(Layer) = .empty;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
if (node.isTag("properties")) { while (iter.next()) |child_id| {
properties = try Property.List.initFromXml(arena, scratch, lexer); const child = doc.getTag(child_id) orelse continue;
continue; const child_name = child.variant.tag.name;
}
if (variant == .tile and node.isTag("data")) { if (std.mem.eql(u8, child_name, "properties")) {
variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer); properties = try Property.List.initFromXml(arena, scratch, doc, child_id);
continue;
}
if (variant == .image and node.isTag("image")) { } else if (variant == .image and std.mem.eql(u8, child_name, "image")) {
variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer); variant.image.image = try ImageVariant.Image.initFromXml(arena, doc, child_id);
continue;
}
if (variant == .object and node.isTag("object")) { } else if (variant == .object and std.mem.eql(u8, child_name, "object")) {
const object = try Object.initFromXml(arena, scratch, lexer); const object = try Object.initFromXml(arena, scratch, doc, child_id);
try objects.append(scratch.allocator(), object); try objects.append(scratch.allocator(), object);
continue;
}
if (variant == .group and isLayerNode(node)) { } else if (variant == .group and Layer.Type.name_map.has(child_name)) {
const layer = try initFromXml(arena, scratch, lexer); const layer = try initFromXml(arena, scratch, doc, child_id);
try layers.append(scratch.allocator(), layer); try layers.append(scratch.allocator(), layer);
continue;
} }
try iter.skip();
} }
try iter.finish(layer_type.toXmlName());
if (variant == .object) { if (variant == .object) {
variant.object.items = try arena.dupe(Object, objects.items); variant.object.items = try arena.dupe(Object, objects.items);
} }
@ -286,7 +323,3 @@ pub fn initFromXml(
.variant = variant, .variant = variant,
}; };
} }
pub fn isLayerNode(node: xml.TagParser.Node) bool {
return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group");
}

View File

@ -117,10 +117,11 @@ shape: Shape,
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer, doc: xml.Document,
element_id: xml.Element.Id
) !Object { ) !Object {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "object");
const attrs = try iter.begin("object"); const attrs = tag.variant.tag.attributes;
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse ""; const name = try attrs.getDupe(arena, "name") orelse "";
@ -134,9 +135,14 @@ pub fn initFromXml(
var shape: ?Shape = null; var shape: ?Shape = null;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
while (iter.next()) |child_id| {
const child = doc.getTag(child_id) orelse continue;
const child_name = child.variant.tag.name;
const child_attrs = child.variant.tag.attributes;
if (shape == null) { if (shape == null) {
if (node.isTag("point")) { if (std.mem.eql(u8, child_name, "point")) {
shape = .{ shape = .{
.point = Shape.Point{ .point = Shape.Point{
.x = x, .x = x,
@ -144,7 +150,7 @@ pub fn initFromXml(
} }
}; };
} else if (node.isTag("ellipse")) { } else if (std.mem.eql(u8, child_name, "ellipse")) {
shape = .{ shape = .{
.ellipse = Shape.Ellipse{ .ellipse = Shape.Ellipse{
.x = x, .x = x,
@ -154,11 +160,11 @@ pub fn initFromXml(
} }
}; };
} else if (node.isTag("text")) { } else if (std.mem.eql(u8, child_name, "text")) {
const HorizontalShape = Shape.Text.HorizontalAlign; const HorizontalShape = Shape.Text.HorizontalAlign;
const VerticalAlign = Shape.Text.VerticalAlign; const VerticalAlign = Shape.Text.VerticalAlign;
const text_attrs = node.tag.attributes; const text_attrs = child_attrs;
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false; const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
const color = try text_attrs.getColor("color", true) orelse Color.black; const color = try text_attrs.getColor("color", true) orelse Color.black;
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left; const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
@ -171,13 +177,13 @@ pub fn initFromXml(
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16; const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif"; const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
_ = try lexer.nextExpectStartTag("text");
var content: []const u8 = ""; var content: []const u8 = "";
const content_value = try lexer.peek(); const text_element = try doc.getTagExpect(child.tree.first_child, "text");
if (content_value != null and content_value.? == .text) { if (doc.get(text_element.tree.first_child)) |content_element| {
content = try arena.dupe(u8, content_value.?.text); if (content_element.variant == .text) {
content = try arena.dupe(u8, content_element.variant.text);
}
} }
try lexer.skipUntilMatchingEndTag("text");
shape = .{ shape = .{
.text = Shape.Text{ .text = Shape.Text{
@ -203,8 +209,8 @@ pub fn initFromXml(
}; };
continue; continue;
} else if (node.isTag("polygon")) { } else if (std.mem.eql(u8, child_name, "polygon")) {
const points_str = node.tag.attributes.get("points") orelse ""; const points_str = child_attrs.get("points") orelse "";
var points: std.ArrayList(Position) = .empty; var points: std.ArrayList(Position) = .empty;
var point_iter = std.mem.splitScalar(u8, points_str, ' '); var point_iter = std.mem.splitScalar(u8, points_str, ' ');
@ -223,7 +229,6 @@ pub fn initFromXml(
} }
} }
try iter.skip();
} }
if (shape == null) { if (shape == null) {
@ -249,8 +254,6 @@ pub fn initFromXml(
} }
} }
try iter.finish("object");
return Object{ return Object{
.id = id, .id = id,
.name = name, .name = name,

View File

@ -34,23 +34,23 @@ pub const List = struct {
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer doc: xml.Document,
element_id: xml.Element.Id
) !Property.List { ) !Property.List {
var iter = xml.TagParser.init(lexer); _ = try doc.getTagExpect(element_id, "properties");
_ = try iter.begin("properties");
var temp_properties: std.ArrayList(Property) = .empty; var temp_properties: std.ArrayList(Property) = .empty;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
if (node.isTag("property")) { while (iter.next()) |child_id| {
const property = try Property.initFromXml(arena, lexer); const child = doc.getTag(child_id) orelse continue;
try temp_properties.append(scratch.allocator(), property); const child_name = child.variant.tag.name;
continue;
}
try iter.skip(); if (std.mem.eql(u8, child_name, "property")) {
const property = try Property.initFromXml(arena, doc, child_id);
try temp_properties.append(scratch.allocator(), property);
}
} }
try iter.finish("properties");
const properties = try arena.dupe(Property, temp_properties.items); const properties = try arena.dupe(Property, temp_properties.items);
@ -83,9 +83,9 @@ pub fn init(name: []const u8, value: Value) Property {
}; };
} }
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property { pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Property {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "property");
const attrs = try iter.begin("property"); const attrs = tag.variant.tag.attributes;
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName; const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
const prop_type_str = attrs.get("type") orelse "string"; const prop_type_str = attrs.get("type") orelse "string";
@ -104,8 +104,6 @@ pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
} }
}; };
try iter.finish("property");
return Property{ return Property{
.name = name, .name = name,
.value = value .value = value

View File

@ -12,5 +12,5 @@ pub const Tileset = @import("./tileset.zig");
pub const Tilemap = @import("./tilemap.zig"); pub const Tilemap = @import("./tilemap.zig");
test { test {
_ = std.testing.refAllDeclsRecursive(@This()); _ = std.testing.refAllDecls(@This());
} }

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const log = std.log;
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
const Io = std.Io; const Io = std.Io;
const assert = std.debug.assert; const assert = std.debug.assert;
@ -54,15 +55,13 @@ pub const TilesetReference = struct {
source: []const u8, source: []const u8,
first_gid: u32, first_gid: u32,
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference { pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !TilesetReference {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "tileset");
const attrs = try iter.begin("tileset"); const attrs = tag.variant.tag.attributes;
const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid; const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid;
const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid; const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid;
try iter.finish("tileset");
return TilesetReference{ return TilesetReference{
.source = source, .source = source,
.first_gid = first_gid .first_gid = first_gid
@ -106,40 +105,21 @@ next_object_id: u32,
tilesets: []TilesetReference, tilesets: []TilesetReference,
layers: []Layer, layers: []Layer,
pub fn initFromBuffer(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
buffer: []const u8
) !Tilemap {
var reader = Io.Reader.fixed(buffer);
return initFromReader(gpa, scratch, xml_buffers, &reader);
}
pub fn initFromReader(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
reader: *Io.Reader,
) !Tilemap {
var lexer = xml.Lexer.init(reader, xml_buffers);
return initFromXml(gpa, scratch, &lexer);
}
// Map specification: // Map specification:
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map // https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
pub fn initFromXml( pub fn initFromXml(
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer, doc: xml.Document,
element_id: xml.Element.Id
) !Tilemap { ) !Tilemap {
var arena_allocator = std.heap.ArenaAllocator.init(gpa); var arena_allocator = std.heap.ArenaAllocator.init(gpa);
errdefer arena_allocator.deinit(); errdefer arena_allocator.deinit();
const arena = arena_allocator.allocator(); const arena = arena_allocator.allocator();
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "map");
const map_attrs = try iter.begin("map"); const map_attrs = tag.variant.tag.attributes;
const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion; const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
const tiled_version = try map_attrs.getDupe(arena, "tiledversion"); const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
@ -168,23 +148,22 @@ pub fn initFromXml(
var tileset_list: std.ArrayList(TilesetReference) = .empty; var tileset_list: std.ArrayList(TilesetReference) = .empty;
var layer_list: std.ArrayList(Layer) = .empty; var layer_list: std.ArrayList(Layer) = .empty;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
if (node.isTag("tileset")) { while (iter.next()) |child_id| {
try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer)); const child = doc.getTag(child_id) orelse continue;
continue; const child_name = child.variant.tag.name;
} else if (Layer.isLayerNode(node)) {
const layer = try Layer.initFromXml( if (std.mem.eql(u8, child_name, "tileset")) {
try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, doc, child_id));
} else {
try layer_list.append(scratch.allocator(), try Layer.initFromXml(
arena, arena,
scratch, scratch,
lexer, doc,
); child_id
try layer_list.append(scratch.allocator(), layer); ));
continue;
} }
try iter.skip();
} }
try iter.finish("map");
const tilesets = try arena.dupe(TilesetReference, tileset_list.items); const tilesets = try arena.dupe(TilesetReference, tileset_list.items);
const layers = try arena.dupe(Layer, layer_list.items); const layers = try arena.dupe(Layer, layer_list.items);
@ -223,7 +202,7 @@ fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
return result; return result;
} }
pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile { pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: i32, y: i32) ?Tile {
assert(layer.variant == .tile); assert(layer.variant == .tile);
const tile_variant = layer.variant.tile; const tile_variant = layer.variant.tile;
@ -248,10 +227,10 @@ pub fn getLayer(self: *const Tilemap, name: []const u8) ?*const Layer {
} }
pub const Bounds = struct { pub const Bounds = struct {
min_x: usize, min_x: i32,
min_y: usize, min_y: i32,
max_x: usize, max_x: i32,
max_y: usize, max_y: i32,
const zero = Bounds{ const zero = Bounds{
.min_x = 0, .min_x = 0,
@ -260,12 +239,12 @@ pub const Bounds = struct {
.max_y = 0, .max_y = 0,
}; };
pub fn width(self: Bounds) usize { pub fn width(self: Bounds) u32 {
return self.max_x - self.min_x + 1; return @intCast(self.max_x - self.min_x + 1);
} }
pub fn height(self: Bounds) usize { pub fn height(self: Bounds) u32 {
return self.max_y - self.min_y + 1; return @intCast(self.max_y - self.min_y + 1);
} }
}; };
@ -277,24 +256,28 @@ pub fn getTileBounds(self: *const Tilemap) Bounds {
continue; continue;
} }
for (0..self.height) |y| { for (layer.variant.tile.chunks) |chunk| {
for (0..self.width) |x| { for (0..chunk.height) |oy| {
const gid = layer.variant.tile.get(x, y).?; for (0..chunk.width) |ox| {
if (gid != 0) { const x = chunk.x + @as(i32, @intCast(ox));
if (result) |bounds| { const y = chunk.y + @as(i32, @intCast(oy));
result = Bounds{ const gid = layer.variant.tile.get(x, y).?;
.min_x = @min(x, bounds.min_x), if (gid != 0) {
.min_y = @min(y, bounds.min_y), if (result) |bounds| {
.max_x = @max(x, bounds.max_x), result = Bounds{
.max_y = @max(y, bounds.max_y) .min_x = @min(x, bounds.min_x),
}; .min_y = @min(y, bounds.min_y),
} else { .max_x = @max(x, bounds.max_x),
result = Bounds{ .max_y = @max(y, bounds.max_y)
.min_x = x, };
.min_y = y, } else {
.max_x = x, result = Bounds{
.max_y = y .min_x = x,
}; .min_y = y,
.max_x = x,
.max_y = y
};
}
} }
} }
} }

View File

@ -14,16 +14,14 @@ pub const Image = struct {
width: u32, width: u32,
height: u32, height: u32,
pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { pub fn intFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "image");
const attrs = try iter.begin("image"); const attrs = tag.variant.tag.attributes;
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource; const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
try iter.finish("image");
return Image{ return Image{
.source = source, .source = source,
.width = width, .width = width,
@ -39,26 +37,26 @@ pub const Tile = struct {
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer, doc: xml.Document,
element_id: xml.Element.Id
) !Tile { ) !Tile {
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "tile");
const tile_attrs = try iter.begin("tile"); const tile_attrs = tag.variant.tag.attributes;
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
var properties: Property.List = .empty; var properties: Property.List = .empty;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
if (node.isTag("properties")) { while (iter.next()) |child_id| {
properties = try Property.List.initFromXml(arena, scratch, lexer); const child = doc.getTag(child_id) orelse continue;
continue; const child_name = child.variant.tag.name;
if (std.mem.eql(u8, child_name, "properties")) {
properties = try Property.List.initFromXml(arena, scratch, doc, child_id);
} }
try iter.skip();
} }
try iter.finish("tile");
return Tile{ return Tile{
.id = id, .id = id,
.properties = properties .properties = properties
@ -124,36 +122,17 @@ image: Image,
tiles: []Tile, tiles: []Tile,
pub fn initFromBuffer(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
buffer: []const u8
) !Tileset {
var reader = Io.Reader.fixed(buffer);
return initFromReader(gpa, scratch, xml_buffers, &reader);
}
pub fn initFromReader(
gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
xml_buffers: *xml.Lexer.Buffers,
reader: *Io.Reader,
) !Tileset {
var lexer = xml.Lexer.init(reader, xml_buffers);
return initFromXml(gpa, scratch, &lexer);
}
pub fn initFromXml( pub fn initFromXml(
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator, scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer doc: xml.Document,
element_id: xml.Element.Id
) !Tileset { ) !Tileset {
var arena_state = std.heap.ArenaAllocator.init(gpa); var arena_state = std.heap.ArenaAllocator.init(gpa);
const arena = arena_state.allocator(); const arena = arena_state.allocator();
var iter = xml.TagParser.init(lexer); const tag = try doc.getTagExpect(element_id, "tileset");
const tileset_attrs = try iter.begin("tileset"); const tileset_attrs = tag.variant.tag.attributes;
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag; const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag;
const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag; const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
@ -166,19 +145,18 @@ pub fn initFromXml(
var image: ?Image = null; var image: ?Image = null;
var tiles_list: std.ArrayList(Tile) = .empty; var tiles_list: std.ArrayList(Tile) = .empty;
while (try iter.next()) |node| { var iter = doc.iterChildren(element_id);
if (node.isTag("image")) { while (iter.next()) |child_id| {
image = try Image.intFromXml(arena, lexer); const child = doc.getTag(child_id) orelse continue;
continue; const child_name = child.variant.tag.name;
} else if (node.isTag("tile")) {
const tile = try Tile.initFromXml(arena, scratch, lexer);
try tiles_list.append(scratch.allocator(), tile);
continue;
}
try iter.skip(); if (std.mem.eql(u8, child_name, "image")) {
image = try Image.intFromXml(arena, doc, child_id);
} else if (std.mem.eql(u8, child_name, "tile")) {
const tile = try Tile.initFromXml(arena, scratch, doc, child_id);
try tiles_list.append(scratch.allocator(), tile);
}
} }
try iter.finish("tileset");
const tiles = try arena.dupe(Tile, tiles_list.items); const tiles = try arena.dupe(Tile, tiles_list.items);

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const Color = @import("./color.zig"); const Color = @import("./color.zig");
@ -105,199 +106,124 @@ pub const Attribute = struct {
} }
}; };
pub const Tag = struct { pub const Element = struct {
name: []const u8, variant: union(enum) {
attributes: Attribute.List tag: struct {
name: []const u8,
attributes: Attribute.List,
},
text: []const u8,
},
// TODO: Maybe an element should contain a "inner contents" field?
// With which you could index into the text content.
// But, need to figure out how it would interact with comments.
//
// Maybe comment removal should be a separate pass?
tree: struct {
first_child: Id = .nil,
last_child: Id = .nil,
next_sibling: Id = .nil,
} = .{},
pub const Id = enum(i32) {
_,
pub const nil: Id = @enumFromInt(-1);
pub const root: Id = @enumFromInt(0);
};
}; };
pub const Lexer = struct { pub const Document = struct {
pub const Buffers = struct { elements: []Element,
scratch: std.heap.ArenaAllocator,
text: std.ArrayList(u8),
pub fn init(allocator: std.mem.Allocator) Buffers { pub fn get(self: Document, id: Element.Id) ?Element {
return Buffers{ if (id == Element.Id.nil) {
.scratch = std.heap.ArenaAllocator.init(allocator), return null;
.text = .empty
};
} }
const index = @intFromEnum(id);
assert(index >= 0);
return self.elements[@intCast(index)];
}
pub fn clear(self: *Buffers) void { pub fn getTag(self: Document, id: Element.Id) ?Element {
self.text.clearRetainingCapacity(); const element = self.get(id) orelse return null;
_ = self.scratch.reset(.retain_capacity); if (element.variant == .tag) {
return element;
} }
return null;
}
pub fn deinit(self: *Buffers) void { pub fn getTagExpect(self: Document, id: Element.Id, name: []const u8) !Element {
const allocator = self.scratch.child_allocator; const tag = self.getTag(id) orelse return error.TagNotFound;
self.scratch.deinit(); const tag_name = tag.variant.tag.name;
self.text.deinit(allocator); if (!std.mem.eql(u8, tag_name, name)) {
return error.UnexpectedTagName;
} }
}; return tag;
}
pub const Token = union(enum) { pub fn iterChildren(self: Document, id: Element.Id) SiblingIterator {
start_tag: Tag, var first_child: Element.Id = .nil;
end_tag: []const u8, if (self.get(id)) |tag| {
text: []const u8, first_child = tag.tree.first_child;
pub fn isStartTag(self: Token, name: []const u8) bool {
if (self == .start_tag) {
return std.mem.eql(u8, self.start_tag.name, name);
}
return false;
} }
return SiblingIterator{
pub fn isEndTag(self: Token, name: []const u8) bool { .doc = self,
if (self == .end_tag) { .current = first_child
return std.mem.eql(u8, self.end_tag, name);
}
return false;
}
};
pub const StepResult = struct {
token: ?Token,
self_closing: bool = false,
};
pub const TestingContext = struct {
io_reader: Io.Reader,
buffers: Buffers,
lexer: Lexer,
pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void {
self.* = TestingContext{
.lexer = undefined,
.io_reader = Io.Reader.fixed(body),
.buffers = Buffers.init(allocator)
};
self.lexer = Lexer.init(&self.io_reader, &self.buffers);
}
pub fn deinit(self: *TestingContext) void {
self.buffers.deinit();
}
};
io_reader: *Io.Reader,
buffers: *Buffers,
peeked_value: ?Token,
cursor: usize,
queued_end_tag: ?[]const u8,
pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer {
buffers.clear();
return Lexer{
.io_reader = reader,
.buffers = buffers,
.cursor = 0,
.queued_end_tag = null,
.peeked_value = null
}; };
} }
fn step(self: *Lexer) !StepResult { pub fn countChildren(self: Document, id: Element.Id) usize {
_ = self.buffers.scratch.reset(.retain_capacity); var count: usize = 0;
var iter = self.iterChildren(id);
if (try self.peekByte() == '<') { while (iter.next()) |_| {
self.tossByte(); count += 1;
if (try self.peekByte() == '/') {
// End tag
self.tossByte();
const name = try self.parseName();
try self.skipWhiteSpace();
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
return error.InvalidEndTag;
}
const token = Token{ .end_tag = name };
return .{ .token = token };
} else if (try self.peekByte() == '?') {
// Prolog tag
self.tossByte();
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
return error.InvalidPrologTag;
}
const attributes = try self.parseAttributes();
try self.skipWhiteSpace();
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
return error.MissingPrologEnd;
}
const version = attributes.get("version") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, version, "1.0")) {
return error.InvalidPrologVersion;
}
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, encoding, "UTF-8")) {
return error.InvalidPrologEncoding;
}
return .{ .token = null };
} else {
// Start tag
const name = try self.parseName();
const attributes = try self.parseAttributes();
try self.skipWhiteSpace();
const token = Token{
.start_tag = .{
.name = name,
.attributes = attributes
}
};
var self_closing = false;
if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
self.tossBytes(1);
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
self.tossBytes(2);
self_closing = true;
} else {
return error.UnfinishedStartTag;
}
return .{
.token = token,
.self_closing = self_closing
};
}
} else {
try self.skipWhiteSpace();
const text_start = self.cursor;
while (try self.peekByte() != '<') {
self.tossByte();
}
var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
var token: ?Token = null;
if (text.len > 0) {
token = Token{ .text = text };
}
return .{ .token = token };
} }
return count;
} }
pub fn next(self: *Lexer) !?Token { const SiblingIterator = struct {
if (self.peeked_value) |value| { doc: Document,
self.peeked_value = null; current: Element.Id,
return value;
}
pub fn next(self: *SiblingIterator) ?Element.Id {
const element = self.doc.get(self.current) orelse return null;
const current = self.current;
self.current = element.tree.next_sibling;
return current;
}
};
};
const Lexer = struct {
pub const Token = union(enum) {
start_tag_begin: []const u8,
attribute: Attribute,
start_tag_end: []const u8,
end_tag: []const u8,
text: []const u8,
// TODO: Add comment type?
pub fn format(self: Token, writer: *Io.Writer) Io.Writer.Error!void {
switch(self) {
.start_tag_begin => |name| try writer.print("Token{{ .start_tag_begin, {s} }}", .{name}),
.attribute => |attr| try writer.print("Token{{ .attribute, {s}='{s}' }}", .{attr.name, attr.value}),
.start_tag_end => try writer.print("Token{{ .start_tag_end }}", .{}),
.end_tag => |name| try writer.print("Token{{ .end_tag, {s} }}", .{name}),
.text => |text| try writer.print("Token{{ .text, {s} }}", .{text}),
}
}
};
text: []const u8,
cursor: usize = 0,
inside_start_tag: ?[]const u8 = null,
queued_end_tag: ?[]const u8 = null,
pub fn next(self: *Lexer) !?Token {
if (self.queued_end_tag) |name| { if (self.queued_end_tag) |name| {
self.queued_end_tag = null; self.queued_end_tag = null;
return Token{ return Token{
@ -305,39 +231,103 @@ pub const Lexer = struct {
}; };
} }
while (true) { while (self.cursor < self.text.len) {
if (self.buffers.text.items.len == 0) { if (self.inside_start_tag) |tag_name| {
self.readIntoTextBuffer() catch |e| switch (e) { if (try self.parseAttribute()) |attribute| {
error.EndOfStream => break, return Token{
else => return e .attribute = attribute
}; };
} } else {
try self.skipWhiteSpace();
const saved_cursor = self.cursor; if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
const result = self.step() catch |e| switch(e) { self.tossBytes(1);
error.EndOfTextBuffer => {
self.cursor = saved_cursor; } else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
self.tossBytes(2);
self.queued_end_tag = tag_name;
const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len;
if (unused_capacity == 0 and self.cursor > 0) {
self.rebaseBuffer();
} else { } else {
self.readIntoTextBuffer() catch |read_err| switch (read_err) { return error.UnfinishedStartTag;
error.EndOfStream => break,
else => return read_err
};
} }
continue; self.inside_start_tag = null;
},
else => return e
};
if (result.token) |token| { return Token{
if (token == .start_tag and result.self_closing) { .start_tag_end = tag_name
self.queued_end_tag = token.start_tag.name; };
}
} else if (try self.peekByte() == '<') {
self.tossByte();
if (try self.peekByte() == '/') {
// End tag
self.tossByte();
const name = try self.parseName();
try self.skipWhiteSpace();
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
return error.InvalidEndTag;
}
return Token{ .end_tag = name };
} else if (try self.peekByte() == '?') {
// Prolog tag
self.tossByte();
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
return error.InvalidPrologTag;
}
var attributes_buf: [8]Attribute = undefined;
const attributes_len = try self.parseAttributesBounded(&attributes_buf);
try self.skipWhiteSpace();
const attributes = Attribute.List{
.items = attributes_buf[0..attributes_len]
};
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
return error.MissingPrologEnd;
}
const version = attributes.get("version") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, version, "1.0")) {
return error.InvalidPrologVersion;
}
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
if (!std.mem.eql(u8, encoding, "UTF-8")) {
return error.InvalidPrologEncoding;
}
} else {
// Start tag
const name = try self.parseName();
self.inside_start_tag = name;
return Token{
.start_tag_begin = name
};
}
} else {
try self.skipWhiteSpace();
if (!self.hasBytes(1)) {
continue;
}
const text_start = self.cursor;
while (try self.peekByte() != '<') {
self.tossByte();
}
var text: []const u8 = self.text[text_start..self.cursor];
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
if (text.len > 0) {
return Token{ .text = text };
} }
return token;
} }
} }
@ -381,45 +371,6 @@ pub const Lexer = struct {
} }
} }
pub fn peek(self: *Lexer) !?Token {
if (try self.next()) |value| {
self.peeked_value = value;
return value;
}
return null;
}
fn readIntoTextBuffer(self: *Lexer) !void {
const gpa = self.buffers.scratch.child_allocator;
const text = &self.buffers.text;
try text.ensureUnusedCapacity(gpa, 1);
var writer = Io.Writer.fixed(text.allocatedSlice());
writer.end = text.items.len;
_ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) {
error.WriteFailed => unreachable,
else => |ee| return ee
};
text.items.len = writer.end;
}
fn rebaseBuffer(self: *Lexer) void {
if (self.cursor == 0) {
return;
}
const text = &self.buffers.text;
@memmove(
text.items[0..(text.items.len - self.cursor)],
text.items[self.cursor..]
);
text.items.len -= self.cursor;
self.cursor = 0;
}
fn isNameStartChar(c: u8) bool { fn isNameStartChar(c: u8) bool {
return c == ':' or c == '_' or std.ascii.isAlphabetic(c); return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
} }
@ -429,16 +380,14 @@ pub const Lexer = struct {
} }
fn hasBytes(self: *Lexer, n: usize) bool { fn hasBytes(self: *Lexer, n: usize) bool {
const text = self.buffers.text.items; return self.cursor + n <= self.text.len;
return self.cursor + n <= text.len;
} }
fn peekBytes(self: *Lexer, n: usize) ![]const u8 { fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
if (self.hasBytes(n)) { if (self.hasBytes(n)) {
const text = self.buffers.text.items; return self.text[self.cursor..][0..n];
return text[self.cursor..][0..n];
} }
return error.EndOfTextBuffer; return error.UnexpectedEOF;
} }
fn tossBytes(self: *Lexer, n: usize) void { fn tossBytes(self: *Lexer, n: usize) void {
@ -475,11 +424,11 @@ pub const Lexer = struct {
} }
} }
return self.buffers.text.items[name_start..self.cursor]; return self.text[name_start..self.cursor];
} }
fn skipWhiteSpace(self: *Lexer) !void { fn skipWhiteSpace(self: *Lexer) !void {
while (std.ascii.isWhitespace(try self.peekByte())) { while (self.cursor < self.text.len and std.ascii.isWhitespace(try self.peekByte())) {
self.tossByte(); self.tossByte();
} }
} }
@ -504,184 +453,304 @@ pub const Lexer = struct {
value_len += 1; value_len += 1;
} }
return self.buffers.text.items[value_start..][0..value_len]; return self.text[value_start..][0..value_len];
} }
fn parseAttributes(self: *Lexer) !Attribute.List { fn parseAttribute(self: *Lexer) !?Attribute {
const arena = self.buffers.scratch.allocator(); try self.skipWhiteSpace();
var attributes: std.ArrayList(Attribute) = .empty; const name = try self.parseName();
if (name.len == 0) {
while (true) {
try self.skipWhiteSpace();
const name = try self.parseName();
if (name.len == 0) {
break;
}
try self.skipWhiteSpace();
if (try self.takeByte() != '=') {
return error.MissingAttributeEquals;
}
try self.skipWhiteSpace();
const value = try self.parseAttributeValue();
const list = Attribute.List{ .items = attributes.items };
if (list.get(name) != null) {
return error.DuplicateAttribute;
}
try attributes.append(arena, Attribute{
.name = name,
.value = value
});
}
return Attribute.List{
.items = attributes.items
};
}
test "self closing tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello />
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag with prolog" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <?xml version="1.0" encoding="UTF-8"?>
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "text content" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello> Hello World </hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "attributes" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello a='1' b='2'/>
);
defer ctx.deinit();
const token = try ctx.lexer.next();
const attrs = token.?.start_tag.attributes;
try std.testing.expectEqualStrings("1", attrs.get("a").?);
try std.testing.expectEqualStrings("2", attrs.get("b").?);
}
};
// TODO: The API for this is easy to misuse.
// Design a better API for using Reader
// As a compromise `assert` was used to guard against some of the ways this can be misused
pub const TagParser = struct {
lexer: *Lexer,
begin_called: bool = false,
finish_called: bool = false,
pub const Node = union(enum) {
tag: Tag,
text: []const u8,
pub fn isTag(self: Node, name: []const u8) bool {
if (self == .tag) {
return std.mem.eql(u8, self.tag.name, name);
}
return false;
}
};
pub fn init(lexer: *Lexer) TagParser {
return TagParser{
.lexer = lexer,
};
}
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
assert(!self.begin_called);
self.begin_called = true;
return try self.lexer.nextExpectStartTag(name);
}
pub fn finish(self: *TagParser, name: []const u8) !void {
assert(self.begin_called);
assert(!self.finish_called);
self.finish_called = true;
try self.lexer.skipUntilMatchingEndTag(name);
}
pub fn next(self: *TagParser) !?Node {
assert(self.begin_called);
assert(!self.finish_called);
const value = try self.lexer.peek() orelse return error.MissingEndTag;
if (value == .end_tag) {
return null; return null;
} }
return switch (value) { try self.skipWhiteSpace();
.text => |text| Node{ .text = text }, if (try self.takeByte() != '=') {
.start_tag => |start_tag| Node{ .tag = start_tag }, return error.MissingAttributeEquals;
.end_tag => unreachable, }
try self.skipWhiteSpace();
const value = try self.parseAttributeValue();
return Attribute{
.name = name,
.value = value
}; };
} }
pub fn skip(self: *TagParser) !void { fn parseAttributesBounded(self: *Lexer, buffer: []Attribute) !usize {
assert(self.begin_called); var attributes: std.ArrayList(Attribute) = .initBuffer(buffer);
assert(!self.finish_called);
const value = try self.lexer.next() orelse return error.MissingNode; while (try self.parseAttribute()) |attr| {
if (value == .end_tag) { const list = Attribute.List{ .items = attributes.items };
return error.UnexpectedEndTag; if (list.get(attr.name) != null) {
} else if (value == .start_tag) { return error.DuplicateAttribute;
// TODO: Make this configurable }
var name_buffer: [64]u8 = undefined;
var name: std.ArrayList(u8) = .initBuffer(&name_buffer);
try name.appendSliceBounded(value.start_tag.name);
try self.lexer.skipUntilMatchingEndTag(name.items); try attributes.appendBounded(attr);
} }
return attributes.items.len;
}
fn expectStartTagBegin(lexer: *Lexer, expected_name: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .start_tag_begin);
try std.testing.expectEqualStrings(expected_name, token.start_tag_begin);
}
fn expectStartTagEnd(lexer: *Lexer) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .start_tag_end);
}
fn expectEndTag(lexer: *Lexer, expected_name: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .end_tag);
try std.testing.expectEqualStrings(expected_name, token.end_tag);
}
fn expectDone(lexer: *Lexer) !void {
try std.testing.expect(try lexer.next() == null);
}
fn expectText(lexer: *Lexer, expected_text: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .text);
try std.testing.expectEqualStrings(expected_text, token.text);
}
fn expectAttribute(lexer: *Lexer, expected: Attribute) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .attribute);
try std.testing.expectEqualStrings(expected.name, token.attribute.name);
try std.testing.expectEqualStrings(expected.value, token.attribute.value);
}
test "self closing tag" {
const text =
\\ <hello />
;
var lexer: Lexer = .{ .text = text };
try expectStartTagBegin(&lexer, "hello");
try expectStartTagEnd(&lexer);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
}
test "tag" {
const text =
\\ <hello></hello>
;
var lexer: Lexer = .{ .text = text };
try expectStartTagBegin(&lexer, "hello");
try expectStartTagEnd(&lexer);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
}
test "tag with prolog" {
const text =
\\ <?xml version="1.0" encoding="UTF-8"?>
\\ <hello></hello>
;
var lexer: Lexer = .{ .text = text };
try expectStartTagBegin(&lexer, "hello");
try expectStartTagEnd(&lexer);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
}
test "text content" {
const text =
\\ <hello> Hello World </hello>
;
var lexer: Lexer = .{ .text = text };
try expectStartTagBegin(&lexer, "hello");
try expectStartTagEnd(&lexer);
try expectText(&lexer, "Hello World");
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
}
test "attributes" {
const text =
\\ <hello a='1' b='2'/>
;
var lexer: Lexer = .{ .text = text };
try expectStartTagBegin(&lexer, "hello");
try expectAttribute(&lexer, .{ .name = "a", .value = "1" });
try expectAttribute(&lexer, .{ .name = "b", .value = "2" });
try expectStartTagEnd(&lexer);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
}; };
pub const Parser = struct {
elements: []Element,
attributes: []Attribute,
stack: []Element.Id,
const InitOptions = struct {
max_elements: u32 = 4096,
max_attributes: u32 = 4096,
max_depth: u32 = 256,
};
pub fn initCapacity(gpa: Allocator, opts: InitOptions) !Parser {
const tags = try gpa.alloc(Element, opts.max_elements);
errdefer gpa.free(tags);
const attributes = try gpa.alloc(Attribute, opts.max_attributes);
errdefer gpa.free(attributes);
const stack = try gpa.alloc(Element.Id, opts.max_depth);
errdefer gpa.free(stack);
return Parser{
.elements = tags,
.attributes = attributes,
.stack = stack,
};
}
pub fn deinit(self: Parser, gpa: Allocator) void {
gpa.free(self.elements);
gpa.free(self.attributes);
gpa.free(self.stack);
}
const ParserContext = struct {
elements: std.ArrayList(Element),
stack: std.ArrayList(Element.Id),
fn getElement(self: *ParserContext, id: Element.Id) *Element {
return &self.elements.items[@intCast(@intFromEnum(id))];
}
pub fn pushElement(self: *ParserContext) !*Element {
const element_index = self.elements.items.len;
const element_id: Element.Id = @enumFromInt(element_index);
const element = self.elements.addOneBounded() catch return error.TooManyElements;
element.tree = .{};
if (self.stack.getLastOrNull()) |parent_id| {
const parent = self.getElement(parent_id);
if (parent.tree.last_child != Element.Id.nil) {
const last_child = &self.elements.items[@intCast(@intFromEnum(parent.tree.last_child))];
last_child.tree.next_sibling = element_id;
} else {
parent.tree.first_child = element_id;
}
parent.tree.last_child = element_id;
}
self.stack.appendBounded(element_id) catch return error.StackTooDeep;
return element;
}
pub fn popElement(self: *ParserContext) !void {
if (self.stack.items.len == 0) {
return error.UnbalancedBeginAndEndTags;
}
_ = self.stack.pop();
}
};
pub fn parse(self: Parser, text: []const u8) !Document {
var ctx = ParserContext{
.elements = .initBuffer(self.elements),
.stack = .initBuffer(self.stack)
};
var attributes_used: usize = 0;
var attributes: std.ArrayList(Attribute) = .initBuffer(self.attributes[attributes_used..]);
var lexer = Lexer{ .text = text };
while (try lexer.next()) |token| {
switch (token) {
.start_tag_begin => |name| {
_ = name; // autofix
},
.attribute => |attribute| {
attributes.appendBounded(attribute) catch return error.TooManyAttributes;
},
.start_tag_end => |name| {
const element = try ctx.pushElement();
element.variant = .{
.tag = .{
.name = name,
.attributes = .{ .items = attributes.items }
}
};
attributes_used += attributes.items.len;
attributes = .initBuffer(self.attributes[attributes_used..]);
},
.end_tag => {
try ctx.popElement();
},
.text => |element_text| {
const element = try ctx.pushElement();
element.variant = .{
.text = element_text
};
try ctx.popElement();
},
}
}
if (ctx.stack.items.len > 0) {
return error.UnbalancedBeginAndEndTags;
}
return Document{
.elements = ctx.elements.items
};
}
test {
const gpa = std.testing.allocator;
var parser = try Parser.initCapacity(gpa, .{});
defer parser.deinit(gpa);
const doc = try parser.parse(
\\ <text>Hello, World!</text>
);
const root = try doc.getTagExpect(.root, "text");
const first_child = doc.get(root.tree.first_child).?;
try std.testing.expectEqualStrings("Hello, World!", first_child.variant.text);
}
test {
const gpa = std.testing.allocator;
var parser = try Parser.initCapacity(gpa, .{});
defer parser.deinit(gpa);
const doc = try parser.parse(
\\ <container>
\\ <box>1</box>
\\ <box>2</box>
\\ <box>3</box>
\\ </container>
);
var iter = doc.iterChildren(.root);
var count: usize = 0;
while (iter.next()) |_| {
count += 1;
}
try std.testing.expectEqual(3, count);
}
};
test {
_ = Lexer;
_ = Parser;
}

View File

@ -125,8 +125,10 @@ const World = struct {
if (opts.pos_x != -1 or opts.pos_y != -1) { if (opts.pos_x != -1 or opts.pos_y != -1) {
assert(opts.pos_x != -1 and opts.pos_y != -1); assert(opts.pos_x != -1 and opts.pos_y != -1);
const success = self.setEntityPosition(entity_id, opts.pos_x, opts.pos_y); if (!self.setEntityPosition(entity_id, opts.pos_x, opts.pos_y)) {
assert(success); log.err("Entity position: {}, {}", .{opts.pos_x, opts.pos_y});
@panic("Failed to set initial entity position");
}
} }
return entity_id; return entity_id;
@ -271,16 +273,24 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
var scratch = std.heap.ArenaAllocator.init(plt.gpa); var scratch = std.heap.ArenaAllocator.init(plt.gpa);
defer scratch.deinit(); defer scratch.deinit();
var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa); var xml_parser = try Tiled.xml.Parser.initCapacity(plt.gpa, .{});
defer buffers.deinit(); defer xml_parser.deinit(plt.gpa);
const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx")); const tilemap = try Tiled.Tilemap.initFromXml(
plt.gpa, &scratch,
try xml_parser.parse(plt.assets.readFile("tiled/map.tmx")),
.root
);
defer tilemap.deinit(); defer tilemap.deinit();
var tilesets: Tiled.Tileset.List = .empty; var tilesets: Tiled.Tileset.List = .empty;
defer tilesets.deinit(plt.gpa); defer tilesets.deinit(plt.gpa);
try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx"))); try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromXml(
plt.gpa, &scratch,
try xml_parser.parse(plt.assets.readFile("tiled/tileset.tsx")),
.root
));
const bounds = tilemap.getTileBounds(); const bounds = tilemap.getTileBounds();
@ -290,8 +300,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
const foreground = tilemap.getLayer("Foreground").?; const foreground = tilemap.getLayer("Foreground").?;
for (0..bounds.height()) |oy| { for (0..bounds.height()) |oy| {
for (0..bounds.width()) |ox| { for (0..bounds.width()) |ox| {
const x = bounds.min_x + ox; const x = bounds.min_x + @as(i32, @intCast(ox));
const y = bounds.min_y + oy; const y = bounds.min_y + @as(i32, @intCast(oy));
const tile = self.world.tilemap.get(ox, oy); const tile = self.world.tilemap.get(ox, oy);

View File

@ -92,7 +92,7 @@ pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*F
assert(box_height < Font.Glyph.max_height); assert(box_height < Font.Glyph.max_height);
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
const bitmap = Gfx.ImageData{ const bitmap = Gfx.Image{
.pixels = .{ .r8 = (&bitmap_buffer).ptr }, .pixels = .{ .r8 = (&bitmap_buffer).ptr },
.width = box_width, .width = box_width,
.height = box_height .height = box_height
@ -418,8 +418,8 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insert(key).?; const id1 = cache.insertOrGet(key).?.id;
const id2 = cache.insert(key).?; const id2 = cache.insertOrGet(key).?.id;
try std.testing.expectEqual(id1, id2); try std.testing.expectEqual(id1, id2);
} }
@ -429,9 +429,9 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insert(key).?; const id1 = cache.insertOrGet(key).?.id;
cache.remove(id1); cache.remove(id1);
const id2 = cache.insert(key).?; const id2 = cache.insertOrGet(key).?.id;
try std.testing.expectEqual(id1, id2); try std.testing.expectEqual(id1, id2);
} }
@ -440,8 +440,8 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id1 = cache.insertOrGet(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id2 = cache.insertOrGet(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
try std.testing.expect(id1 != id2); try std.testing.expect(id1 != id2);
} }
@ -452,10 +452,10 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
for (0..limit-1) |i| { for (0..limit-1) |i| {
const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id != null); try std.testing.expect(id != null);
} }
const id = cache.insert(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insertOrGet(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id == null); try std.testing.expect(id == null);
} }
@ -470,9 +470,9 @@ const GlyphCache = struct {
defer ids.deinit(gpa); defer ids.deinit(gpa);
for (0..(limit-1)) |i| { for (0..(limit-1)) |i| {
const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id != null); try std.testing.expect(id != null);
try ids.append(gpa, id.?); try ids.append(gpa, id.?.id);
} }
assert(cache.free_head == GlyphSlot.nil_id); assert(cache.free_head == GlyphSlot.nil_id);
@ -498,9 +498,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
defer gpa.free(lru); defer gpa.free(lru);
@ -513,9 +513,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
defer gpa.free(lru); defer gpa.free(lru);
@ -528,7 +528,7 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); try std.testing.expect(cache.lru_head != GlyphSlot.nil_id);
try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id);
cache.remove(id); cache.remove(id);
@ -542,13 +542,13 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insert(key1).?; const id1 = cache.insertOrGet(key1).?.id;
const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id2 = cache.insert(key2).?; const id2 = cache.insertOrGet(key2).?.id;
const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id3 = cache.insert(key3).?; const id3 = cache.insertOrGet(key3).?.id;
_ = cache.lookup(key2); _ = cache.insertOrGet(key2);
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
@ -556,7 +556,7 @@ const GlyphCache = struct {
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru);
} }
_ = cache.lookup(key1); _ = cache.insertOrGet(key1);
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
@ -570,9 +570,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id;
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);