From 96cfcd4626888dd2939bb81e3158519472595ac2 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 21 Sep 2026 02:16:36 +0300 Subject: [PATCH] add support for parsing chunked maps --- assets/tiled/map.tmx | 83 ++- assets/tiled/sokoban.tiled-session | 5 +- libs/tiled/src/layer.zig | 189 +++--- libs/tiled/src/object.zig | 39 +- libs/tiled/src/property.zig | 30 +- libs/tiled/src/root.zig | 2 +- libs/tiled/src/tilemap.zig | 119 ++-- libs/tiled/src/tileset.zig | 78 +-- libs/tiled/src/xml.zig | 901 ++++++++++++++++------------- src/app.zig | 26 +- src/font.zig | 52 +- 11 files changed, 799 insertions(+), 725 deletions(-) diff --git a/assets/tiled/map.tmx b/assets/tiled/map.tmx index 214cbc3..6c925c0 100644 --- a/assets/tiled/map.tmx +++ b/assets/tiled/map.tmx @@ -1,57 +1,54 @@ - + -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 - + +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,98,0,0,98,0,0,0,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,98,0,7,0,0,26,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,98,0,0,98,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,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + + + diff --git a/assets/tiled/sokoban.tiled-session b/assets/tiled/sokoban.tiled-session index cf7d775..14b830c 100644 --- a/assets/tiled/sokoban.tiled-session +++ b/assets/tiled/sokoban.tiled-session @@ -13,7 +13,8 @@ } }, "tileset.tsx": { - "scaleInDock": 1 + "scaleInDock": 0.5, + "scaleInEditor": 1 } }, "last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban", @@ -24,7 +25,9 @@ "map.tmx" ], "project": "sokoban.tiled-project", + "property.type": "int", "recentFiles": [ + "tileset.tsx", "map.tmx" ], "tileset.lastUsedFormat": "tsx", diff --git a/libs/tiled/src/layer.zig b/libs/tiled/src/layer.zig index eb05984..9e34834 100644 --- a/libs/tiled/src/layer.zig +++ b/libs/tiled/src/layer.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const Property = @import("./property.zig"); const xml = @import("./xml.zig"); @@ -11,41 +12,104 @@ const Layer = @This(); pub const TileVariant = struct { width: 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, - scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer, - ) ![]u32 { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("data"); + doc: xml.Document, + element_id: xml.Element.Id, + ) !TileVariant { + const tag = try doc.getTagExpect(element_id, "layer"); + 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 - var temp_tiles: std.ArrayList(u32) = .empty; + var chunks: std.ArrayList(Chunk) = .empty; if (std.mem.eql(u8, encoding, "csv")) { - const text = try lexer.nextExpectText(); - var split_iter = std.mem.splitScalar(u8, text, ','); - 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); - try temp_tiles.append(scratch.allocator(), tile_id); + try chunks.ensureTotalCapacityPrecise(arena, doc.countChildren(data_id)); + + var iter = doc.iterChildren(data_id); + while (iter.next()) |child_id| { + const child = doc.get(child_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 { return error.UnknownEncodingType; } - try iter.finish("data"); - - return try arena.dupe(u32, temp_tiles.items); + return TileVariant{ + .width = layer_width, + .height = layer_height, + .chunks = chunks.items + }; } - pub fn get(self: TileVariant, x: usize, y: usize) ?u32 { - if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) { - return self.data[y * self.width + x]; + pub fn get(self: TileVariant, x: i32, y: i32) ?u32 { + for (self.chunks) |chunk| { + 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; } @@ -59,9 +123,9 @@ pub const ImageVariant = struct { width: ?u32, height: ?u32, - fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("image"); + fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image { + const tag = try doc.getTagExpect(element_id, "image"); + const attrs = tag.variant.tag.attributes; // TODO: format 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 transparent_color = try attrs.getColor("trans", false); - try iter.finish("image"); - return Image{ .source = source, .transparent_color = transparent_color, @@ -126,15 +188,6 @@ pub const Type = enum { .{ "imagelayer", .image }, .{ "group", .group } }); - - fn toXmlName(self: Type) []const u8 { - return switch (self) { - .tile => "layer", - .object => "objectgroup", - .image => "imagelayer", - .group => "group", - }; - } }; pub const Variant = union(Type) { @@ -160,15 +213,13 @@ variant: Variant, pub fn initFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer, + doc: xml.Document, + element_id: xml.Element.Id ) !Layer { - const value = try lexer.peek() orelse return error.MissingStartTag; - if (value != .start_tag) return error.MissingStartTag; + const tag = doc.getTag(element_id) orelse return error.TagNotFound; - var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType; - - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin(layer_type.toXmlName()); + const layer_type = Type.name_map.get(tag.variant.tag.name) orelse return error.UnknownLayerType; + const attrs = tag.variant.tag.attributes; const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const name = try attrs.getDupe(arena, "name") orelse ""; @@ -184,14 +235,12 @@ pub fn initFromXml( var variant: Variant = undefined; switch (layer_type) { .tile => { - const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; - const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; - variant = .{ - .tile = TileVariant{ - .width = width, - .height = height, - .data = &[0]u32{} - } + variant = Variant{ + .tile = try TileVariant.initFromXml( + arena, + doc, + element_id + ) }; }, .image => { @@ -230,39 +279,27 @@ pub fn initFromXml( var objects: std.ArrayList(Object) = .empty; var layers: std.ArrayList(Layer) = .empty; - while (try iter.next()) |node| { - if (node.isTag("properties")) { - properties = try Property.List.initFromXml(arena, scratch, lexer); - continue; - } + 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; - if (variant == .tile and node.isTag("data")) { - variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer); - continue; - } + if (std.mem.eql(u8, child_name, "properties")) { + properties = try Property.List.initFromXml(arena, scratch, doc, child_id); - if (variant == .image and node.isTag("image")) { - variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer); - continue; - } + } else if (variant == .image and std.mem.eql(u8, child_name, "image")) { + variant.image.image = try ImageVariant.Image.initFromXml(arena, doc, child_id); - if (variant == .object and node.isTag("object")) { - const object = try Object.initFromXml(arena, scratch, lexer); + } else if (variant == .object and std.mem.eql(u8, child_name, "object")) { + const object = try Object.initFromXml(arena, scratch, doc, child_id); try objects.append(scratch.allocator(), object); - continue; - } - if (variant == .group and isLayerNode(node)) { - const layer = try initFromXml(arena, scratch, lexer); + } else if (variant == .group and Layer.Type.name_map.has(child_name)) { + const layer = try initFromXml(arena, scratch, doc, child_id); try layers.append(scratch.allocator(), layer); - continue; } - - try iter.skip(); } - try iter.finish(layer_type.toXmlName()); - if (variant == .object) { variant.object.items = try arena.dupe(Object, objects.items); } @@ -286,7 +323,3 @@ pub fn initFromXml( .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"); -} diff --git a/libs/tiled/src/object.zig b/libs/tiled/src/object.zig index e52ba05..47c276b 100644 --- a/libs/tiled/src/object.zig +++ b/libs/tiled/src/object.zig @@ -117,10 +117,11 @@ shape: Shape, pub fn initFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer, + doc: xml.Document, + element_id: xml.Element.Id ) !Object { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("object"); + const tag = try doc.getTagExpect(element_id, "object"); + const attrs = tag.variant.tag.attributes; const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const name = try attrs.getDupe(arena, "name") orelse ""; @@ -134,9 +135,14 @@ pub fn initFromXml( 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 (node.isTag("point")) { + if (std.mem.eql(u8, child_name, "point")) { shape = .{ .point = Shape.Point{ .x = x, @@ -144,7 +150,7 @@ pub fn initFromXml( } }; - } else if (node.isTag("ellipse")) { + } else if (std.mem.eql(u8, child_name, "ellipse")) { shape = .{ .ellipse = Shape.Ellipse{ .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 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 color = try text_attrs.getColor("color", true) orelse Color.black; 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 font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif"; - _ = try lexer.nextExpectStartTag("text"); var content: []const u8 = ""; - const content_value = try lexer.peek(); - if (content_value != null and content_value.? == .text) { - content = try arena.dupe(u8, content_value.?.text); + const text_element = try doc.getTagExpect(child.tree.first_child, "text"); + if (doc.get(text_element.tree.first_child)) |content_element| { + if (content_element.variant == .text) { + content = try arena.dupe(u8, content_element.variant.text); + } } - try lexer.skipUntilMatchingEndTag("text"); shape = .{ .text = Shape.Text{ @@ -203,8 +209,8 @@ pub fn initFromXml( }; continue; - } else if (node.isTag("polygon")) { - const points_str = node.tag.attributes.get("points") orelse ""; + } else if (std.mem.eql(u8, child_name, "polygon")) { + const points_str = child_attrs.get("points") orelse ""; var points: std.ArrayList(Position) = .empty; var point_iter = std.mem.splitScalar(u8, points_str, ' '); @@ -223,7 +229,6 @@ pub fn initFromXml( } } - try iter.skip(); } if (shape == null) { @@ -249,8 +254,6 @@ pub fn initFromXml( } } - try iter.finish("object"); - return Object{ .id = id, .name = name, diff --git a/libs/tiled/src/property.zig b/libs/tiled/src/property.zig index 1fa29df..6eb121c 100644 --- a/libs/tiled/src/property.zig +++ b/libs/tiled/src/property.zig @@ -34,23 +34,23 @@ pub const List = struct { pub fn initFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer + doc: xml.Document, + element_id: xml.Element.Id ) !Property.List { - var iter = xml.TagParser.init(lexer); - _ = try iter.begin("properties"); + _ = try doc.getTagExpect(element_id, "properties"); var temp_properties: std.ArrayList(Property) = .empty; - while (try iter.next()) |node| { - if (node.isTag("property")) { - const property = try Property.initFromXml(arena, lexer); - try temp_properties.append(scratch.allocator(), property); - continue; - } + 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; - 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); @@ -83,9 +83,9 @@ pub fn init(name: []const u8, value: Value) Property { }; } -pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("property"); +pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Property { + const tag = try doc.getTagExpect(element_id, "property"); + const attrs = tag.variant.tag.attributes; const name = try attrs.getDupe(arena, "name") orelse return error.MissingName; 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{ .name = name, .value = value diff --git a/libs/tiled/src/root.zig b/libs/tiled/src/root.zig index 50a97ee..e10029b 100644 --- a/libs/tiled/src/root.zig +++ b/libs/tiled/src/root.zig @@ -12,5 +12,5 @@ pub const Tileset = @import("./tileset.zig"); pub const Tilemap = @import("./tilemap.zig"); test { - _ = std.testing.refAllDeclsRecursive(@This()); + _ = std.testing.refAllDecls(@This()); } diff --git a/libs/tiled/src/tilemap.zig b/libs/tiled/src/tilemap.zig index 49bf926..def2cbb 100644 --- a/libs/tiled/src/tilemap.zig +++ b/libs/tiled/src/tilemap.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const log = std.log; const xml = @import("./xml.zig"); const Io = std.Io; const assert = std.debug.assert; @@ -54,15 +55,13 @@ pub const TilesetReference = struct { source: []const u8, first_gid: u32, - pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("tileset"); + pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !TilesetReference { + const tag = try doc.getTagExpect(element_id, "tileset"); + const attrs = tag.variant.tag.attributes; const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid; const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid; - try iter.finish("tileset"); - return TilesetReference{ .source = source, .first_gid = first_gid @@ -106,40 +105,21 @@ next_object_id: u32, tilesets: []TilesetReference, 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: // https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map pub fn initFromXml( gpa: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer, + doc: xml.Document, + element_id: xml.Element.Id ) !Tilemap { var arena_allocator = std.heap.ArenaAllocator.init(gpa); errdefer arena_allocator.deinit(); const arena = arena_allocator.allocator(); - var iter = xml.TagParser.init(lexer); - const map_attrs = try iter.begin("map"); + const tag = try doc.getTagExpect(element_id, "map"); + const map_attrs = tag.variant.tag.attributes; const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion; const tiled_version = try map_attrs.getDupe(arena, "tiledversion"); @@ -168,23 +148,22 @@ pub fn initFromXml( var tileset_list: std.ArrayList(TilesetReference) = .empty; var layer_list: std.ArrayList(Layer) = .empty; - while (try iter.next()) |node| { - if (node.isTag("tileset")) { - try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer)); - continue; - } else if (Layer.isLayerNode(node)) { - const layer = try Layer.initFromXml( + 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; + + 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, scratch, - lexer, - ); - try layer_list.append(scratch.allocator(), layer); - continue; + doc, + child_id + )); } - - try iter.skip(); } - try iter.finish("map"); const tilesets = try arena.dupe(TilesetReference, tileset_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; } -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); 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 { - min_x: usize, - min_y: usize, - max_x: usize, - max_y: usize, + min_x: i32, + min_y: i32, + max_x: i32, + max_y: i32, const zero = Bounds{ .min_x = 0, @@ -260,12 +239,12 @@ pub const Bounds = struct { .max_y = 0, }; - pub fn width(self: Bounds) usize { - return self.max_x - self.min_x + 1; + pub fn width(self: Bounds) u32 { + return @intCast(self.max_x - self.min_x + 1); } - pub fn height(self: Bounds) usize { - return self.max_y - self.min_y + 1; + pub fn height(self: Bounds) u32 { + return @intCast(self.max_y - self.min_y + 1); } }; @@ -277,24 +256,28 @@ pub fn getTileBounds(self: *const Tilemap) Bounds { continue; } - for (0..self.height) |y| { - for (0..self.width) |x| { - const gid = layer.variant.tile.get(x, y).?; - if (gid != 0) { - if (result) |bounds| { - result = Bounds{ - .min_x = @min(x, bounds.min_x), - .min_y = @min(y, bounds.min_y), - .max_x = @max(x, bounds.max_x), - .max_y = @max(y, bounds.max_y) - }; - } else { - result = Bounds{ - .min_x = x, - .min_y = y, - .max_x = x, - .max_y = y - }; + for (layer.variant.tile.chunks) |chunk| { + for (0..chunk.height) |oy| { + for (0..chunk.width) |ox| { + const x = chunk.x + @as(i32, @intCast(ox)); + const y = chunk.y + @as(i32, @intCast(oy)); + const gid = layer.variant.tile.get(x, y).?; + if (gid != 0) { + if (result) |bounds| { + result = Bounds{ + .min_x = @min(x, bounds.min_x), + .min_y = @min(y, bounds.min_y), + .max_x = @max(x, bounds.max_x), + .max_y = @max(y, bounds.max_y) + }; + } else { + result = Bounds{ + .min_x = x, + .min_y = y, + .max_x = x, + .max_y = y + }; + } } } } diff --git a/libs/tiled/src/tileset.zig b/libs/tiled/src/tileset.zig index f5e5801..f105656 100644 --- a/libs/tiled/src/tileset.zig +++ b/libs/tiled/src/tileset.zig @@ -14,16 +14,14 @@ pub const Image = struct { width: u32, height: u32, - pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image { - var iter = xml.TagParser.init(lexer); - const attrs = try iter.begin("image"); + pub fn intFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image { + const tag = try doc.getTagExpect(element_id, "image"); + const attrs = tag.variant.tag.attributes; const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource; const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; - try iter.finish("image"); - return Image{ .source = source, .width = width, @@ -39,26 +37,26 @@ pub const Tile = struct { pub fn initFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer, + doc: xml.Document, + element_id: xml.Element.Id ) !Tile { - var iter = xml.TagParser.init(lexer); - const tile_attrs = try iter.begin("tile"); + const tag = try doc.getTagExpect(element_id, "tile"); + const tile_attrs = tag.variant.tag.attributes; const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; var properties: Property.List = .empty; - while (try iter.next()) |node| { - if (node.isTag("properties")) { - properties = try Property.List.initFromXml(arena, scratch, lexer); - continue; + 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; + + 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{ .id = id, .properties = properties @@ -124,36 +122,17 @@ image: Image, 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( gpa: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, - lexer: *xml.Lexer + doc: xml.Document, + element_id: xml.Element.Id ) !Tileset { var arena_state = std.heap.ArenaAllocator.init(gpa); const arena = arena_state.allocator(); - var iter = xml.TagParser.init(lexer); - const tileset_attrs = try iter.begin("tileset"); + const tag = try doc.getTagExpect(element_id, "tileset"); + const tileset_attrs = tag.variant.tag.attributes; 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; @@ -166,19 +145,18 @@ pub fn initFromXml( var image: ?Image = null; var tiles_list: std.ArrayList(Tile) = .empty; - while (try iter.next()) |node| { - if (node.isTag("image")) { - image = try Image.intFromXml(arena, lexer); - continue; - } else if (node.isTag("tile")) { - const tile = try Tile.initFromXml(arena, scratch, lexer); - try tiles_list.append(scratch.allocator(), tile); - continue; - } + 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; - 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); diff --git a/libs/tiled/src/xml.zig b/libs/tiled/src/xml.zig index 661c065..698072d 100644 --- a/libs/tiled/src/xml.zig +++ b/libs/tiled/src/xml.zig @@ -1,5 +1,6 @@ const std = @import("std"); const Io = std.Io; +const Allocator = std.mem.Allocator; const assert = std.debug.assert; const Color = @import("./color.zig"); @@ -105,199 +106,124 @@ pub const Attribute = struct { } }; -pub const Tag = struct { - name: []const u8, - attributes: Attribute.List +pub const Element = struct { + variant: union(enum) { + 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 Buffers = struct { - scratch: std.heap.ArenaAllocator, - text: std.ArrayList(u8), +pub const Document = struct { + elements: []Element, - pub fn init(allocator: std.mem.Allocator) Buffers { - return Buffers{ - .scratch = std.heap.ArenaAllocator.init(allocator), - .text = .empty - }; + pub fn get(self: Document, id: Element.Id) ?Element { + if (id == Element.Id.nil) { + return null; } + const index = @intFromEnum(id); + assert(index >= 0); + return self.elements[@intCast(index)]; + } - pub fn clear(self: *Buffers) void { - self.text.clearRetainingCapacity(); - _ = self.scratch.reset(.retain_capacity); + pub fn getTag(self: Document, id: Element.Id) ?Element { + const element = self.get(id) orelse return null; + if (element.variant == .tag) { + return element; } + return null; + } - pub fn deinit(self: *Buffers) void { - const allocator = self.scratch.child_allocator; - self.scratch.deinit(); - self.text.deinit(allocator); + pub fn getTagExpect(self: Document, id: Element.Id, name: []const u8) !Element { + const tag = self.getTag(id) orelse return error.TagNotFound; + const tag_name = tag.variant.tag.name; + if (!std.mem.eql(u8, tag_name, name)) { + return error.UnexpectedTagName; } - }; + return tag; + } - pub const Token = union(enum) { - start_tag: Tag, - end_tag: []const u8, - text: []const u8, - - 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; + pub fn iterChildren(self: Document, id: Element.Id) SiblingIterator { + var first_child: Element.Id = .nil; + if (self.get(id)) |tag| { + first_child = tag.tree.first_child; } - - pub fn isEndTag(self: Token, name: []const u8) bool { - if (self == .end_tag) { - 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 + return SiblingIterator{ + .doc = self, + .current = first_child }; } - fn step(self: *Lexer) !StepResult { - _ = self.buffers.scratch.reset(.retain_capacity); - - 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; - } - - 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 }; + pub fn countChildren(self: Document, id: Element.Id) usize { + var count: usize = 0; + var iter = self.iterChildren(id); + while (iter.next()) |_| { + count += 1; } + return count; } - pub fn next(self: *Lexer) !?Token { - if (self.peeked_value) |value| { - self.peeked_value = null; - return value; - } + const SiblingIterator = struct { + doc: Document, + current: Element.Id, + 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| { self.queued_end_tag = null; return Token{ @@ -305,39 +231,103 @@ pub const Lexer = struct { }; } - while (true) { - if (self.buffers.text.items.len == 0) { - self.readIntoTextBuffer() catch |e| switch (e) { - error.EndOfStream => break, - else => return e - }; - } + while (self.cursor < self.text.len) { + if (self.inside_start_tag) |tag_name| { + if (try self.parseAttribute()) |attribute| { + return Token{ + .attribute = attribute + }; + } else { + try self.skipWhiteSpace(); - const saved_cursor = self.cursor; - const result = self.step() catch |e| switch(e) { - error.EndOfTextBuffer => { - self.cursor = saved_cursor; + 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.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 { - self.readIntoTextBuffer() catch |read_err| switch (read_err) { - error.EndOfStream => break, - else => return read_err - }; + return error.UnfinishedStartTag; } - continue; - }, - else => return e - }; + self.inside_start_tag = null; - if (result.token) |token| { - if (token == .start_tag and result.self_closing) { - self.queued_end_tag = token.start_tag.name; + return Token{ + .start_tag_end = 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 { return c == ':' or c == '_' or std.ascii.isAlphabetic(c); } @@ -429,16 +380,14 @@ pub const Lexer = struct { } fn hasBytes(self: *Lexer, n: usize) bool { - const text = self.buffers.text.items; - return self.cursor + n <= text.len; + return self.cursor + n <= self.text.len; } fn peekBytes(self: *Lexer, n: usize) ![]const u8 { if (self.hasBytes(n)) { - const text = self.buffers.text.items; - return text[self.cursor..][0..n]; + return self.text[self.cursor..][0..n]; } - return error.EndOfTextBuffer; + return error.UnexpectedEOF; } 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 { - while (std.ascii.isWhitespace(try self.peekByte())) { + while (self.cursor < self.text.len and std.ascii.isWhitespace(try self.peekByte())) { self.tossByte(); } } @@ -504,184 +453,304 @@ pub const Lexer = struct { 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 { - const arena = self.buffers.scratch.allocator(); - var attributes: std.ArrayList(Attribute) = .empty; - - 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, - \\ - ); - 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, - \\ - ); - 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, - \\ - \\ - ); - 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 World - ); - 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, - \\ - ); - 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) { + fn parseAttribute(self: *Lexer) !?Attribute { + try self.skipWhiteSpace(); + const name = try self.parseName(); + if (name.len == 0) { return null; } - return switch (value) { - .text => |text| Node{ .text = text }, - .start_tag => |start_tag| Node{ .tag = start_tag }, - .end_tag => unreachable, + try self.skipWhiteSpace(); + if (try self.takeByte() != '=') { + return error.MissingAttributeEquals; + } + try self.skipWhiteSpace(); + const value = try self.parseAttributeValue(); + + return Attribute{ + .name = name, + .value = value }; } - pub fn skip(self: *TagParser) !void { - assert(self.begin_called); - assert(!self.finish_called); + fn parseAttributesBounded(self: *Lexer, buffer: []Attribute) !usize { + var attributes: std.ArrayList(Attribute) = .initBuffer(buffer); - const value = try self.lexer.next() orelse return error.MissingNode; - if (value == .end_tag) { - return error.UnexpectedEndTag; - } else if (value == .start_tag) { - // 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); + while (try self.parseAttribute()) |attr| { + const list = Attribute.List{ .items = attributes.items }; + if (list.get(attr.name) != null) { + return error.DuplicateAttribute; + } - 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 = + \\ + ; + + var lexer: Lexer = .{ .text = text }; + try expectStartTagBegin(&lexer, "hello"); + try expectStartTagEnd(&lexer); + try expectEndTag(&lexer, "hello"); + try expectDone(&lexer); + } + + test "tag" { + const text = + \\ + ; + + 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 = + \\ + \\ + ; + + 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 World + ; + + 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 = + \\ + ; + + 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( + \\ Hello, World! + ); + 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( + \\ + \\ 1 + \\ 2 + \\ 3 + \\ + ); + var iter = doc.iterChildren(.root); + var count: usize = 0; + while (iter.next()) |_| { + count += 1; + } + try std.testing.expectEqual(3, count); + } +}; + +test { + _ = Lexer; + _ = Parser; +} diff --git a/src/app.zig b/src/app.zig index 4ae9db6..ba0ae8d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -125,8 +125,10 @@ const World = struct { if (opts.pos_x != -1 or 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); - assert(success); + if (!self.setEntityPosition(entity_id, opts.pos_x, opts.pos_y)) { + log.err("Entity position: {}, {}", .{opts.pos_x, opts.pos_y}); + @panic("Failed to set initial entity position"); + } } return entity_id; @@ -271,16 +273,24 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { var scratch = std.heap.ArenaAllocator.init(plt.gpa); defer scratch.deinit(); - var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa); - defer buffers.deinit(); + var xml_parser = try Tiled.xml.Parser.initCapacity(plt.gpa, .{}); + 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(); var tilesets: Tiled.Tileset.List = .empty; 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(); @@ -290,8 +300,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { const foreground = tilemap.getLayer("Foreground").?; for (0..bounds.height()) |oy| { for (0..bounds.width()) |ox| { - const x = bounds.min_x + ox; - const y = bounds.min_y + oy; + const x = bounds.min_x + @as(i32, @intCast(ox)); + const y = bounds.min_y + @as(i32, @intCast(oy)); const tile = self.world.tilemap.get(ox, oy); diff --git a/src/font.zig b/src/font.zig index c5c3866..2ddc6d9 100644 --- a/src/font.zig +++ b/src/font.zig @@ -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); 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 }, .width = box_width, .height = box_height @@ -418,8 +418,8 @@ const GlyphCache = struct { defer cache.deinit(gpa); const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; - const id1 = cache.insert(key).?; - const id2 = cache.insert(key).?; + const id1 = cache.insertOrGet(key).?.id; + const id2 = cache.insertOrGet(key).?.id; try std.testing.expectEqual(id1, id2); } @@ -429,9 +429,9 @@ const GlyphCache = struct { defer cache.deinit(gpa); 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); - const id2 = cache.insert(key).?; + const id2 = cache.insertOrGet(key).?.id; try std.testing.expectEqual(id1, id2); } @@ -440,8 +440,8 @@ const GlyphCache = struct { var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); - const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id2 = cache.insert(Glyph.Key{ .index = 11, .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.insertOrGet(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; try std.testing.expect(id1 != id2); } @@ -452,10 +452,10 @@ const GlyphCache = struct { defer cache.deinit(gpa); 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); } - 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); } @@ -470,9 +470,9 @@ const GlyphCache = struct { defer ids.deinit(gpa); 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 ids.append(gpa, id.?); + try ids.append(gpa, id.?.id); } assert(cache.free_head == GlyphSlot.nil_id); @@ -498,9 +498,9 @@ const GlyphCache = struct { var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); - const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id3 = cache.insert(.{ .index = 2, .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.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; + const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); @@ -513,9 +513,9 @@ const GlyphCache = struct { var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); - const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id3 = cache.insert(.{ .index = 2, .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.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; + const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const lru = try cache.getLRUItems(gpa); defer gpa.free(lru); @@ -528,7 +528,7 @@ const GlyphCache = struct { var cache = try GlyphCache.init(gpa, 256); 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_tail != GlyphSlot.nil_id); cache.remove(id); @@ -542,13 +542,13 @@ const GlyphCache = struct { defer cache.deinit(gpa); 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 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 id3 = cache.insert(key3).?; + const id3 = cache.insertOrGet(key3).?.id; - _ = cache.lookup(key2); + _ = cache.insertOrGet(key2); { const lru = try cache.getLRUItems(gpa); @@ -556,7 +556,7 @@ const GlyphCache = struct { try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); } - _ = cache.lookup(key1); + _ = cache.insertOrGet(key1); { const lru = try cache.getLRUItems(gpa); @@ -570,9 +570,9 @@ const GlyphCache = struct { var cache = try GlyphCache.init(gpa, 256); defer cache.deinit(gpa); - const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?; - const id3 = cache.insert(.{ .index = 2, .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.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; + const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; { const lru = try cache.getLRUItems(gpa);