diff --git a/assets/tiled/map.tmx b/assets/tiled/map.tmx
new file mode 100644
index 0000000..f392817
--- /dev/null
+++ b/assets/tiled/map.tmx
@@ -0,0 +1,76 @@
+
+
diff --git a/assets/tiled/sokoban.tiled-project b/assets/tiled/sokoban.tiled-project
new file mode 100644
index 0000000..d0eb592
--- /dev/null
+++ b/assets/tiled/sokoban.tiled-project
@@ -0,0 +1,14 @@
+{
+ "automappingRulesFile": "",
+ "commands": [
+ ],
+ "compatibilityVersion": 1100,
+ "extensionsPath": "extensions",
+ "folders": [
+ "."
+ ],
+ "properties": [
+ ],
+ "propertyTypes": [
+ ]
+}
diff --git a/assets/tiled/sokoban.tiled-session b/assets/tiled/sokoban.tiled-session
new file mode 100644
index 0000000..7fda9ae
--- /dev/null
+++ b/assets/tiled/sokoban.tiled-session
@@ -0,0 +1,21 @@
+{
+ "activeFile": "",
+ "expandedProjectPaths": [
+ ],
+ "fileStates": {
+ },
+ "last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
+ "map.lastUsedFormat": "tmx",
+ "map.tileHeight": 64,
+ "map.tileWidth": 64,
+ "openFiles": [
+ ],
+ "project": "sokoban.tiled-project",
+ "recentFiles": [
+ ],
+ "tileset.lastUsedFormat": "tsx",
+ "tileset.tileSize": {
+ "height": 64,
+ "width": 64
+ }
+}
diff --git a/assets/tiled/tileset.tsx b/assets/tiled/tileset.tsx
new file mode 100644
index 0000000..1433f9c
--- /dev/null
+++ b/assets/tiled/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/build.zig b/build.zig
index 94c6221..218d296 100644
--- a/build.zig
+++ b/build.zig
@@ -8,7 +8,9 @@ pub fn build(b: *std.Build) !void {
const assets_dir = "assets";
const asset_files = [_][]const u8{
"sokoban/sokoban_tilesheet.png",
- "roboto-font/Roboto-Regular.ttf"
+ "roboto-font/Roboto-Regular.ttf",
+ "tiled/map.tmx",
+ "tiled/tileset.tsx"
};
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
@@ -42,6 +44,9 @@ pub fn build(b: *std.Build) !void {
});
exe_mod.addImport("math", math_mod);
+ const dep_tiled = b.dependency("tiled", .{});
+ exe_mod.addImport("tiled", dep_tiled.module("tiled"));
+
const dep_sokol = b.dependency("sokol", .{
.target = target,
.optimize = optimize,
diff --git a/build.zig.zon b/build.zig.zon
index 80d956e..bf65b84 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -11,6 +11,9 @@
.stb = .{
.path = "./libs/stb",
},
+ .tiled = .{
+ .path = "./libs/tiled",
+ },
.tracy = .{
.path = "./libs/tracy",
},
diff --git a/libs/tiled/build.zig b/libs/tiled/build.zig
new file mode 100644
index 0000000..e717649
--- /dev/null
+++ b/libs/tiled/build.zig
@@ -0,0 +1,29 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) !void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const mod = b.addModule("tiled", .{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("src/root.zig")
+ });
+
+ const lib = b.addLibrary(.{
+ .name = "tiled",
+ .root_module = mod
+ });
+ b.installArtifact(lib);
+
+ {
+ const tests = b.addTest(.{
+ .root_module = mod
+ });
+
+ const run_tests = b.addRunArtifact(tests);
+
+ const test_step = b.step("test", "Run tests");
+ test_step.dependOn(&run_tests.step);
+ }
+}
diff --git a/libs/tiled/src/buffers.zig b/libs/tiled/src/buffers.zig
new file mode 100644
index 0000000..b9ab166
--- /dev/null
+++ b/libs/tiled/src/buffers.zig
@@ -0,0 +1,18 @@
+const std = @import("std");
+
+const Position = @import("./position.zig");
+
+const Buffers = @This();
+
+allocator: std.mem.Allocator,
+points: std.ArrayList(Position) = .empty,
+
+pub fn init(gpa: std.mem.Allocator) Buffers {
+ return Buffers{
+ .allocator = gpa
+ };
+}
+
+pub fn deinit(self: *Buffers) void {
+ self.points.deinit(self.allocator);
+}
diff --git a/libs/tiled/src/color.zig b/libs/tiled/src/color.zig
new file mode 100644
index 0000000..d8cd764
--- /dev/null
+++ b/libs/tiled/src/color.zig
@@ -0,0 +1,52 @@
+const std = @import("std");
+
+const Color = @This();
+
+r: u8,
+g: u8,
+b: u8,
+a: u8,
+
+pub const black = Color{
+ .r = 0,
+ .g = 0,
+ .b = 0,
+ .a = 255,
+};
+
+pub fn parse(str: []const u8, hash_required: bool) !Color {
+ var color = Color{
+ .r = undefined,
+ .g = undefined,
+ .b = undefined,
+ .a = 255,
+ };
+
+ if (str.len < 1) {
+ return error.InvalidColorFormat;
+ }
+
+ const has_hash = str[0] == '#';
+ if (hash_required and !has_hash) {
+ return error.InvalidColorFormat;
+ }
+
+ const hex_str = if (has_hash) str[1..] else str;
+
+ if (hex_str.len == 6) {
+ color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16);
+ color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16);
+ color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16);
+
+ } else if (hex_str.len == 8) {
+ color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16);
+ color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16);
+ color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16);
+ color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16);
+
+ } else {
+ return error.InvalidColorFormat;
+ }
+
+ return color;
+}
diff --git a/libs/tiled/src/global_tile_id.zig b/libs/tiled/src/global_tile_id.zig
new file mode 100644
index 0000000..96d93ad
--- /dev/null
+++ b/libs/tiled/src/global_tile_id.zig
@@ -0,0 +1,15 @@
+
+pub const Flag = enum(u32) {
+ flipped_horizontally = 1 << 31, // bit 32
+ flipped_vertically = 1 << 30, // bit 31
+ flipped_diagonally = 1 << 29, // bit 30
+ rotated_hexagonal_120 = 1 << 28, // bit 29
+ _,
+
+ pub const clear: u32 = ~(
+ @intFromEnum(Flag.flipped_horizontally) |
+ @intFromEnum(Flag.flipped_vertically) |
+ @intFromEnum(Flag.flipped_diagonally) |
+ @intFromEnum(Flag.rotated_hexagonal_120)
+ );
+};
diff --git a/libs/tiled/src/layer.zig b/libs/tiled/src/layer.zig
new file mode 100644
index 0000000..4efc102
--- /dev/null
+++ b/libs/tiled/src/layer.zig
@@ -0,0 +1,283 @@
+const std = @import("std");
+
+const Property = @import("./property.zig");
+const xml = @import("./xml.zig");
+const Color = @import("./color.zig");
+const Object = @import("./object.zig");
+const Position = @import("./position.zig");
+
+const Layer = @This();
+
+pub const TileVariant = struct {
+ width: u32,
+ height: u32,
+ data: []u32,
+
+ fn initDataFromXml(
+ arena: std.mem.Allocator,
+ scratch: *std.heap.ArenaAllocator,
+ lexer: *xml.Lexer,
+ ) ![]u32 {
+ var iter = xml.TagParser.init(lexer);
+ const attrs = try iter.begin("data");
+
+ const encoding = attrs.get("encoding") orelse "csv";
+ // TODO: compression
+
+ var temp_tiles: std.ArrayList(u32) = .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);
+ }
+ } else {
+ return error.UnknownEncodingType;
+ }
+
+ try iter.finish("data");
+
+ return try arena.dupe(u32, temp_tiles.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];
+ }
+ return null;
+ }
+};
+
+pub const ImageVariant = struct {
+ pub const Image = struct {
+ // TODO: format
+ source: []const u8,
+ transparent_color: ?Color,
+ 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");
+
+ // TODO: format
+ const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
+ const width = try attrs.getNumber(u32, "width") orelse null;
+ 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,
+ .width = width,
+ .height = height
+ };
+ }
+ };
+
+ repeat_x: bool,
+ repeat_y: bool,
+ image: ?Image
+};
+
+pub const ObjectVariant = struct {
+ pub const DrawOrder = enum {
+ top_down,
+ index,
+
+ const map: std.StaticStringMap(DrawOrder) = .initComptime(.{
+ .{ "topdown", .top_down },
+ .{ "index", .index },
+ });
+ };
+
+ color: ?Color,
+ draw_order: DrawOrder,
+ items: []Object
+};
+
+pub const GroupVariant = struct {
+ layers: []Layer
+};
+
+pub const Type = enum {
+ tile,
+ object,
+ image,
+ group,
+
+ const name_map: std.StaticStringMap(Type) = .initComptime(.{
+ .{ "layer", .tile },
+ .{ "objectgroup", .object },
+ .{ "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) {
+ tile: TileVariant,
+ object: ObjectVariant,
+ image: ImageVariant,
+ group: GroupVariant
+};
+
+id: u32,
+name: []const u8,
+class: []const u8,
+opacity: f32,
+visible: bool,
+tint_color: ?Color,
+offset_x: f32,
+offset_y: f32,
+parallax_x: f32,
+parallax_y: f32,
+properties: Property.List,
+variant: Variant,
+
+pub fn initFromXml(
+ arena: std.mem.Allocator,
+ scratch: *std.heap.ArenaAllocator,
+ lexer: *xml.Lexer,
+) !Layer {
+ const value = try lexer.peek() orelse return error.MissingStartTag;
+ if (value != .start_tag) return error.MissingStartTag;
+
+ 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 id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
+ const name = try attrs.getDupe(arena, "name") orelse "";
+ const class = try attrs.getDupe(arena, "class") orelse "";
+ const opacity = try attrs.getNumber(f32, "opacity") orelse 1;
+ const visible = try attrs.getBool("visible", "1", "0") orelse true;
+ const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0;
+ const offset_y = try attrs.getNumber(f32, "offsety") orelse 0;
+ const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1;
+ const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1;
+ const tint_color = try attrs.getColor("tintcolor", true);
+
+ 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{}
+ }
+ };
+ },
+ .image => {
+ const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false;
+ const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false;
+ variant = .{
+ .image = ImageVariant{
+ .repeat_x = repeat_x,
+ .repeat_y = repeat_y,
+ .image = null
+ }
+ };
+ },
+ .object => {
+ const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down;
+ const color = try attrs.getColor("color", true);
+
+ variant = .{
+ .object = ObjectVariant{
+ .color = color,
+ .draw_order = draw_order,
+ .items = &.{}
+ }
+ };
+ },
+ .group => {
+ variant = .{
+ .group = .{
+ .layers = &.{}
+ }
+ };
+ },
+ }
+
+ var properties: Property.List = .empty;
+ 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;
+ }
+
+ if (variant == .tile and node.isTag("data")) {
+ variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer);
+ continue;
+ }
+
+ if (variant == .image and node.isTag("image")) {
+ variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer);
+ continue;
+ }
+
+ if (variant == .object and node.isTag("object")) {
+ const object = try Object.initFromXml(arena, scratch, lexer);
+ try objects.append(scratch.allocator(), object);
+ continue;
+ }
+
+ if (variant == .group and isLayerNode(node)) {
+ const layer = try initFromXml(arena, scratch, lexer);
+ 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);
+ }
+
+ if (variant == .group) {
+ variant.group.layers = try arena.dupe(Layer, layers.items);
+ }
+
+ return Layer{
+ .id = id,
+ .name = name,
+ .class = class,
+ .opacity = opacity,
+ .visible = visible,
+ .tint_color = tint_color,
+ .offset_x = offset_x,
+ .offset_y = offset_y,
+ .parallax_x = parallax_x,
+ .parallax_y = parallax_y,
+ .properties = properties,
+ .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
new file mode 100644
index 0000000..e52ba05
--- /dev/null
+++ b/libs/tiled/src/object.zig
@@ -0,0 +1,421 @@
+const std = @import("std");
+
+const xml = @import("./xml.zig");
+const Position = @import("./position.zig");
+const Color = @import("./color.zig");
+
+const Object = @This();
+
+pub const Shape = union (Type) {
+ pub const Type = enum {
+ rectangle,
+ point,
+ ellipse,
+ polygon,
+ tile,
+ // TODO: template
+ text
+ };
+
+ pub const Rectangle = struct {
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ };
+
+ pub const Tile = struct {
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ gid: u32
+ };
+
+ pub const Ellipse = struct {
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ };
+
+ pub const Polygon = struct {
+ x: f32,
+ y: f32,
+ points: []const Position
+ };
+
+ pub const Text = struct {
+ pub const Font = struct {
+ family: []const u8,
+ pixel_size: f32,
+ bold: bool,
+ italic: bool,
+ underline: bool,
+ strikeout: bool,
+ kerning: bool
+ };
+
+ pub const HorizontalAlign = enum {
+ left,
+ center,
+ right,
+ justify,
+
+ const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
+ .{ "left", .left },
+ .{ "center", .center },
+ .{ "right", .right },
+ .{ "justify", .justify },
+ });
+ };
+
+ pub const VerticalAlign = enum {
+ top,
+ center,
+ bottom,
+
+ const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
+ .{ "top", .top },
+ .{ "center", .center },
+ .{ "bottom", .bottom },
+ });
+ };
+
+ x: f32,
+ y: f32,
+ width: f32,
+ height: f32,
+ word_wrap: bool,
+ color: Color,
+ font: Font,
+ horizontal_align: HorizontalAlign,
+ vertical_align: VerticalAlign,
+ content: []const u8
+ };
+
+ pub const Point = struct {
+ x: f32,
+ y: f32,
+ };
+
+ rectangle: Rectangle,
+ point: Point,
+ ellipse: Ellipse,
+ polygon: Polygon,
+ tile: Tile,
+ text: Text
+};
+
+id: u32,
+name: []const u8,
+class: []const u8,
+rotation: f32, // TODO: maybe this field should be moved to Shape struct
+visible: bool,
+shape: Shape,
+
+pub fn initFromXml(
+ arena: std.mem.Allocator,
+ scratch: *std.heap.ArenaAllocator,
+ lexer: *xml.Lexer,
+) !Object {
+ var iter = xml.TagParser.init(lexer);
+ const attrs = try iter.begin("object");
+
+ const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
+ const name = try attrs.getDupe(arena, "name") orelse "";
+ const class = try attrs.getDupe(arena, "type") orelse "";
+ const x = try attrs.getNumber(f32, "x") orelse 0;
+ const y = try attrs.getNumber(f32, "y") orelse 0;
+ const width = try attrs.getNumber(f32, "width") orelse 0;
+ const height = try attrs.getNumber(f32, "height") orelse 0;
+ const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
+ const visible = try attrs.getBool("visible", "1", "0") orelse true;
+
+ var shape: ?Shape = null;
+
+ while (try iter.next()) |node| {
+ if (shape == null) {
+ if (node.isTag("point")) {
+ shape = .{
+ .point = Shape.Point{
+ .x = x,
+ .y = y,
+ }
+ };
+
+ } else if (node.isTag("ellipse")) {
+ shape = .{
+ .ellipse = Shape.Ellipse{
+ .x = x,
+ .y = y,
+ .width = width,
+ .height = height
+ }
+ };
+
+ } else if (node.isTag("text")) {
+ const HorizontalShape = Shape.Text.HorizontalAlign;
+ const VerticalAlign = Shape.Text.VerticalAlign;
+
+ const text_attrs = node.tag.attributes;
+ 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;
+ const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
+ const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
+ const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
+ const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
+ const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
+ const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
+ 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);
+ }
+ try lexer.skipUntilMatchingEndTag("text");
+
+ shape = .{
+ .text = Shape.Text{
+ .x = x,
+ .y = y,
+ .width = width,
+ .height = height,
+ .word_wrap = word_wrap,
+ .color = color,
+ .horizontal_align = horizontal_align,
+ .vertical_align = vertical_align,
+ .content = try arena.dupe(u8, content),
+ .font = .{
+ .bold = bold,
+ .italic = italic,
+ .strikeout = strikeout,
+ .underline = underline,
+ .pixel_size = pixel_size,
+ .kerning = kerning,
+ .family = font_family,
+ }
+ }
+ };
+ continue;
+
+ } else if (node.isTag("polygon")) {
+ const points_str = node.tag.attributes.get("points") orelse "";
+
+ var points: std.ArrayList(Position) = .empty;
+ var point_iter = std.mem.splitScalar(u8, points_str, ' ');
+ while (point_iter.next()) |point_str| {
+ const point = try Position.parseCommaDelimited(point_str);
+ try points.append(scratch.allocator(), point);
+ }
+
+ shape = .{
+ .polygon = Shape.Polygon{
+ .x = x,
+ .y = y,
+ .points = try arena.dupe(Position, points.items)
+ }
+ };
+ }
+ }
+
+ try iter.skip();
+ }
+
+ if (shape == null) {
+ if (try attrs.getNumber(u32, "gid")) |gid| {
+ shape = .{
+ .tile = Shape.Tile{
+ .x = x,
+ .y = y,
+ .width = width,
+ .height = height,
+ .gid = gid
+ }
+ };
+ } else {
+ shape = .{
+ .rectangle = Shape.Rectangle{
+ .x = x,
+ .y = y,
+ .width = width,
+ .height = height
+ }
+ };
+ }
+ }
+
+ try iter.finish("object");
+
+ return Object{
+ .id = id,
+ .name = name,
+ .class = class,
+ .rotation = rotation,
+ .visible = visible,
+ .shape = shape orelse return error.UnknownShapeType
+ };
+}
+
+fn expectParsedEquals(expected: Object, body: []const u8) !void {
+ const allocator = std.testing.allocator;
+ var ctx: xml.Lexer.TestingContext = undefined;
+ ctx.init(allocator, body);
+ defer ctx.deinit();
+
+ var scratch = std.heap.ArenaAllocator.init(allocator);
+ defer scratch.deinit();
+
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+
+ const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
+ try std.testing.expectEqualDeep(expected, parsed);
+}
+
+test Object {
+ try expectParsedEquals(
+ Object{
+ .id = 10,
+ .name = "rectangle",
+ .class = "object class",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .rectangle = .{
+ .x = 12.34,
+ .y = 56.78,
+ .width = 31.5,
+ .height = 20.25,
+ }
+ }
+ },
+ \\
+ );
+
+ try expectParsedEquals(
+ Object{
+ .id = 3,
+ .name = "point",
+ .class = "foo",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .point = .{
+ .x = 77.125,
+ .y = 99.875
+ }
+ }
+ },
+ \\
+ );
+
+ try expectParsedEquals(
+ Object{
+ .id = 4,
+ .name = "ellipse",
+ .class = "",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .ellipse = .{
+ .x = 64.25,
+ .y = 108.25,
+ .width = 22.375,
+ .height = 15.375
+ }
+ }
+ },
+ \\
+ );
+
+ try expectParsedEquals(
+ Object{
+ .id = 5,
+ .name = "",
+ .class = "",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .polygon = .{
+ .x = 40.125,
+ .y = 96.25,
+ .points = &[_]Position{
+ .{ .x = 0, .y = 0 },
+ .{ .x = 13.25, .y = -4.25 },
+ .{ .x = 10.125, .y = 18.625 },
+ .{ .x = 2.25, .y = 17.375 },
+ .{ .x = -0.125, .y = 25.75 },
+ .{ .x = -3.875, .y = 20.75 },
+ }
+ }
+ }
+ },
+ \\
+ );
+
+ try expectParsedEquals(
+ Object{
+ .id = 2,
+ .name = "tile",
+ .class = "",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .tile = .{
+ .x = 60.125,
+ .y = 103.5,
+ .width = 8,
+ .height = 8,
+ .gid = 35
+ }
+ }
+ },
+ \\
+ );
+
+ try expectParsedEquals(
+ Object{
+ .id = 6,
+ .name = "text",
+ .class = "",
+ .rotation = 0,
+ .visible = true,
+ .shape = .{
+ .text = .{
+ .x = 64.3906,
+ .y = 92.8594,
+ .width = 87.7188,
+ .height = 21.7813,
+ .content = "Hello World",
+ .word_wrap = true,
+ .color = .black,
+ .horizontal_align = .center,
+ .vertical_align = .top,
+ .font = .{
+ .family = "sans-serif",
+ .pixel_size = 16,
+ .bold = false,
+ .italic = false,
+ .underline = false,
+ .strikeout = false,
+ .kerning = true
+ },
+ }
+ }
+ },
+ \\
+ );
+}
diff --git a/libs/tiled/src/position.zig b/libs/tiled/src/position.zig
new file mode 100644
index 0000000..79179d4
--- /dev/null
+++ b/libs/tiled/src/position.zig
@@ -0,0 +1,20 @@
+const std = @import("std");
+
+const Position = @This();
+
+x: f32,
+y: f32,
+
+pub fn parseCommaDelimited(str: []const u8) !Position {
+ const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma;
+ const x_str = str[0..comma_index];
+ const y_str = str[(comma_index+1)..];
+
+ const x = try std.fmt.parseFloat(f32, x_str);
+ const y = try std.fmt.parseFloat(f32, y_str);
+
+ return Position{
+ .x = x,
+ .y = y
+ };
+}
diff --git a/libs/tiled/src/property.zig b/libs/tiled/src/property.zig
new file mode 100644
index 0000000..1fa29df
--- /dev/null
+++ b/libs/tiled/src/property.zig
@@ -0,0 +1,153 @@
+const std = @import("std");
+const xml = @import("./xml.zig");
+
+const Property = @This();
+
+pub const Type = enum {
+ string,
+ int,
+ bool,
+
+ const map: std.StaticStringMap(Type) = .initComptime(.{
+ .{ "string", .string },
+ .{ "int", .int },
+ .{ "bool", .bool },
+ });
+};
+
+pub const Value = union(Type) {
+ string: []const u8,
+ int: i32,
+ bool: bool
+};
+
+name: []const u8,
+value: Value,
+
+pub const List = struct {
+ items: []Property,
+
+ pub const empty = List{
+ .items = &[0]Property{}
+ };
+
+ pub fn initFromXml(
+ arena: std.mem.Allocator,
+ scratch: *std.heap.ArenaAllocator,
+ lexer: *xml.Lexer
+ ) !Property.List {
+ var iter = xml.TagParser.init(lexer);
+ _ = try iter.begin("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;
+ }
+
+ try iter.skip();
+ }
+ try iter.finish("properties");
+
+ const properties = try arena.dupe(Property, temp_properties.items);
+
+ return List{
+ .items = properties
+ };
+ }
+
+ pub fn get(self: List, name: []const u8) ?Value {
+ for (self.items) |item| {
+ if (std.mem.eql(u8, item.name, name)) {
+ return item.value;
+ }
+ }
+ return null;
+ }
+
+ pub fn getString(self: List, name: []const u8) ?[]const u8 {
+ if (self.get(name)) |value| {
+ return value.string;
+ }
+ return null;
+ }
+};
+
+pub fn init(name: []const u8, value: Value) Property {
+ return Property{
+ .name = name,
+ .value = value
+ };
+}
+
+pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
+ var iter = xml.TagParser.init(lexer);
+ const attrs = try iter.begin("property");
+
+ const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
+ const prop_type_str = attrs.get("type") orelse "string";
+ const value_str = attrs.get("value") orelse "";
+
+ const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType;
+ const value = switch(prop_type) {
+ .string => Value{
+ .string = try arena.dupe(u8, value_str)
+ },
+ .int => Value{
+ .int = try std.fmt.parseInt(i32, value_str, 10)
+ },
+ .bool => Value{
+ .bool = std.mem.eql(u8, value_str, "true")
+ }
+ };
+
+ try iter.finish("property");
+
+ return Property{
+ .name = name,
+ .value = value
+ };
+}
+
+fn expectParsedEquals(expected: Property, body: []const u8) !void {
+ const allocator = std.testing.allocator;
+ var ctx: xml.Lexer.TestingContext = undefined;
+ ctx.init(allocator, body);
+ defer ctx.deinit();
+
+ var arena = std.heap.ArenaAllocator.init(allocator);
+ defer arena.deinit();
+
+ const parsed = try initFromXml(arena.allocator(), &ctx.lexer);
+ try std.testing.expectEqualDeep(expected, parsed);
+}
+
+test Property {
+ try expectParsedEquals(
+ Property.init("solid", .{ .string = "hello" }),
+ \\
+ );
+
+ try expectParsedEquals(
+ Property.init("solid", .{ .string = "hello" }),
+ \\
+ );
+
+ try expectParsedEquals(
+ Property.init("integer", .{ .int = 123 }),
+ \\
+ );
+
+ try expectParsedEquals(
+ Property.init("boolean", .{ .bool = true }),
+ \\
+ );
+
+ try expectParsedEquals(
+ Property.init("boolean", .{ .bool = false }),
+ \\
+ );
+}
diff --git a/libs/tiled/src/root.zig b/libs/tiled/src/root.zig
new file mode 100644
index 0000000..50a97ee
--- /dev/null
+++ b/libs/tiled/src/root.zig
@@ -0,0 +1,16 @@
+const std = @import("std");
+
+// Warning:
+// This library is not complete, it does not cover all features that the specification provides.
+// But there are enough features implemented so that I could use this for my games.
+
+// Map format specification:
+// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#
+
+pub const xml = @import("./xml.zig");
+pub const Tileset = @import("./tileset.zig");
+pub const Tilemap = @import("./tilemap.zig");
+
+test {
+ _ = std.testing.refAllDeclsRecursive(@This());
+}
diff --git a/libs/tiled/src/tilemap.zig b/libs/tiled/src/tilemap.zig
new file mode 100644
index 0000000..c6cde2e
--- /dev/null
+++ b/libs/tiled/src/tilemap.zig
@@ -0,0 +1,235 @@
+const std = @import("std");
+const xml = @import("./xml.zig");
+const Io = std.Io;
+const assert = std.debug.assert;
+
+const Property = @import("./property.zig");
+const Layer = @import("./layer.zig");
+const Object = @import("./object.zig");
+const Position = @import("./position.zig");
+const Tileset = @import("./tileset.zig");
+const GlobalTileId = @import("./global_tile_id.zig");
+
+const Tilemap = @This();
+
+pub const Orientation = enum {
+ orthogonal,
+ staggered,
+ hexagonal,
+ isometric,
+
+ const map: std.StaticStringMap(Orientation) = .initComptime(.{
+ .{ "orthogonal", .orthogonal },
+ .{ "staggered", .staggered },
+ .{ "hexagonal", .hexagonal },
+ .{ "isometric", .isometric }
+ });
+};
+
+pub const RenderOrder = enum {
+ right_down,
+ right_up,
+ left_down,
+ left_up,
+
+ const map: std.StaticStringMap(RenderOrder) = .initComptime(.{
+ .{ "right-down", .right_down },
+ .{ "right-up", .right_up },
+ .{ "left-down", .left_down },
+ .{ "left-up", .left_up },
+ });
+};
+
+pub const StaggerAxis = enum {
+ x,
+ y,
+
+ const map: std.StaticStringMap(StaggerAxis) = .initComptime(.{
+ .{ "x", .x },
+ .{ "y", .y }
+ });
+};
+
+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");
+
+ 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
+ };
+ }
+};
+
+pub const Tile = struct {
+ tileset: *const Tileset,
+ id: u32,
+
+ pub fn getProperties(self: Tile) Property.List {
+ return self.tileset.getTileProperties(self.id) orelse .empty;
+ }
+};
+
+arena: std.heap.ArenaAllocator,
+
+version: []const u8,
+tiled_version: ?[]const u8,
+orientation: Orientation,
+render_order: RenderOrder,
+width: u32,
+height: u32,
+tile_width: u32,
+tile_height: u32,
+infinite: bool,
+
+stagger_axis: ?StaggerAxis,
+next_layer_id: u32,
+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,
+) !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 version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
+ const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
+ const orientation = try map_attrs.getEnum(Orientation, "orientation", Orientation.map) orelse return error.MissingOrientation;
+ const render_order = try map_attrs.getEnum(RenderOrder, "renderorder", RenderOrder.map) orelse return error.MissingRenderOrder;
+ // TODO: compressionlevel
+ const width = try map_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
+ const height = try map_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
+ const tile_width = try map_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTileWidth;
+ const tile_height = try map_attrs.getNumber(u32, "tileheight") orelse return error.MissingTileHeight;
+ // TODO: hexidelength
+ const infinite_int = try map_attrs.getNumber(u32, "infinite") orelse 0;
+ const infinite = infinite_int != 0;
+ const next_layer_id = try map_attrs.getNumber(u32, "nextlayerid") orelse return error.MissingLayerId;
+ const next_object_id = try map_attrs.getNumber(u32, "nextobjectid") orelse return error.MissingObjectId;
+ // TODO: parallaxoriginx
+ // TODO: parallaxoriginy
+ // TODO: backgroundcolor
+
+ var stagger_axis: ?StaggerAxis = null;
+ if (orientation == .hexagonal or orientation == .staggered) {
+ stagger_axis = try map_attrs.getEnum(StaggerAxis, "staggeraxis", StaggerAxis.map) orelse return error.MissingRenderOrder;
+ // TODO: staggerindex
+ }
+
+ 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(
+ arena,
+ scratch,
+ lexer,
+ );
+ 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 layers = try arena.dupe(Layer, layer_list.items);
+
+ return Tilemap{
+ .arena = arena_allocator,
+ .version = version,
+ .tiled_version = tiled_version,
+ .orientation = orientation,
+ .render_order = render_order,
+ .width = width,
+ .height = height,
+ .tile_width = tile_width,
+ .tile_height = tile_height,
+ .infinite = infinite,
+ .stagger_axis = stagger_axis,
+ .next_object_id = next_object_id,
+ .next_layer_id = next_layer_id,
+ .tilesets = tilesets,
+ .layers = layers,
+ };
+}
+
+fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
+ var result: ?TilesetReference = null;
+ for (self.tilesets) |tileset| {
+ if (gid < tileset.first_gid) {
+ continue;
+ }
+ if (result != null and result.?.first_gid < tileset.first_gid) {
+ continue;
+ }
+ result = tileset;
+ }
+
+ return result;
+}
+
+pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile {
+ assert(layer.variant == .tile);
+ const tile_variant = layer.variant.tile;
+
+ const gid = tile_variant.get(x, y) orelse return null;
+ const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null;
+ const tileset = tilesets.get(tileset_ref.source) orelse return null;
+ const id = gid - tileset_ref.first_gid;
+
+ return Tile{
+ .tileset = tileset,
+ .id = id
+ };
+}
+
+pub fn deinit(self: *const Tilemap) void {
+ self.arena.deinit();
+}
diff --git a/libs/tiled/src/tileset.zig b/libs/tiled/src/tileset.zig
new file mode 100644
index 0000000..04b9305
--- /dev/null
+++ b/libs/tiled/src/tileset.zig
@@ -0,0 +1,226 @@
+const std = @import("std");
+const Io = std.Io;
+const Allocator = std.mem.Allocator;
+
+const xml = @import("./xml.zig");
+const Property = @import("./property.zig");
+const Position = @import("./position.zig");
+
+const Tileset = @This();
+
+pub const Image = struct {
+ source: []const u8,
+ 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");
+
+ 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,
+ .height = height
+ };
+ }
+};
+
+pub const Tile = struct {
+ id: u32,
+ properties: Property.List,
+
+ pub fn initFromXml(
+ arena: std.mem.Allocator,
+ scratch: *std.heap.ArenaAllocator,
+ lexer: *xml.Lexer,
+ ) !Tile {
+ var iter = xml.TagParser.init(lexer);
+ const tile_attrs = try iter.begin("tile");
+
+ 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;
+ }
+
+ try iter.skip();
+ }
+
+ try iter.finish("tile");
+
+ return Tile{
+ .id = id,
+ .properties = properties
+ };
+ }
+};
+
+pub const List = struct {
+ const Entry = struct {
+ name: []const u8,
+ tileset: Tileset
+ };
+
+ list: std.ArrayList(Entry),
+
+ pub const empty = List{
+ .list = .empty
+ };
+
+ pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void {
+ if (self.get(name) != null) {
+ return error.DuplicateName;
+ }
+
+ const name_dupe = try gpa.dupe(u8, name);
+ errdefer gpa.free(name_dupe);
+
+ try self.list.append(gpa, .{
+ .name = name_dupe,
+ .tileset = tileset
+ });
+ }
+
+ pub fn get(self: *const List, name: []const u8) ?*const Tileset {
+ for (self.list.items) |*entry| {
+ if (std.mem.eql(u8, entry.name, name)) {
+ return &entry.tileset;
+ }
+ }
+ return null;
+ }
+
+ pub fn deinit(self: *List, gpa: Allocator) void {
+ for (self.list.items) |entry| {
+ gpa.free(entry.name);
+ entry.tileset.deinit();
+ }
+ self.list.deinit(gpa);
+ }
+};
+
+arena: std.heap.ArenaAllocator,
+
+version: []const u8,
+tiled_version: []const u8,
+name: []const u8,
+tile_width: u32,
+tile_height: u32,
+tile_count: u32,
+columns: u32,
+
+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
+) !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 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 name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag;
+ const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag;
+ const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag;
+ const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag;
+ const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag;
+
+ 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;
+ }
+
+ try iter.skip();
+ }
+ try iter.finish("tileset");
+
+ const tiles = try arena.dupe(Tile, tiles_list.items);
+
+ return Tileset{
+ .arena = arena_state,
+ .version = version,
+ .tiled_version = tiled_version,
+ .name = name,
+ .tile_width = tile_width,
+ .tile_height = tile_height,
+ .tile_count = tile_count,
+ .columns = columns,
+ .image = image orelse return error.MissingImageTag,
+ .tiles = tiles
+ };
+}
+
+pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
+ for (self.tiles) |tile| {
+ if (tile.id == id) {
+ return tile.properties;
+ }
+ }
+ return null;
+}
+
+pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
+ if (id >= self.tile_count) {
+ return null;
+ }
+
+ const tileset_width = @divExact(self.image.width, self.tile_width);
+
+ const tile_x = @mod(id, tileset_width);
+ const tile_y = @divFloor(id, tileset_width);
+
+ return Position{
+ .x = @floatFromInt(tile_x * self.tile_width),
+ .y = @floatFromInt(tile_y * self.tile_height),
+ };
+
+}
+
+pub fn deinit(self: *const Tileset) void {
+ self.arena.deinit();
+}
diff --git a/libs/tiled/src/xml.zig b/libs/tiled/src/xml.zig
new file mode 100644
index 0000000..661c065
--- /dev/null
+++ b/libs/tiled/src/xml.zig
@@ -0,0 +1,687 @@
+const std = @import("std");
+const Io = std.Io;
+const assert = std.debug.assert;
+
+const Color = @import("./color.zig");
+
+pub const Attribute = struct {
+ name: []const u8,
+ value: []const u8,
+
+ pub const List = struct {
+ items: []const Attribute,
+
+ pub fn get(self: List, name: []const u8) ?[]const u8 {
+ for (self.items) |attr| {
+ if (std.mem.eql(u8, attr.name, name)) {
+ return attr.value;
+ }
+ }
+ return null;
+ }
+
+ pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 {
+ if (self.get(name)) |value| {
+ return try gpa.dupe(u8, value);
+ }
+ return null;
+ }
+
+ pub fn getNumber(self: List, T: type, name: []const u8) !?T {
+ if (self.get(name)) |value| {
+ if (@typeInfo(T) == .int) {
+ return try std.fmt.parseInt(T, value, 10);
+ } else if (@typeInfo(T) == .float) {
+ return try std.fmt.parseFloat(T, value);
+ }
+ }
+ return null;
+ }
+
+ pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool {
+ if (self.get(name)) |value| {
+ if (std.mem.eql(u8, value, true_value)) {
+ return true;
+ } else if (std.mem.eql(u8, value, false_value)) {
+ return false;
+ } else {
+ return error.InvalidBoolean;
+ }
+ }
+ return null;
+ }
+
+ pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T {
+ if (self.get(name)) |value| {
+ return map.get(value) orelse return error.InvalidEnumValue;
+ }
+ return null;
+ }
+
+ pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color {
+ if (self.get(name)) |value| {
+ return try Color.parse(value, hash_required);
+ }
+ return null;
+ }
+
+ pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void {
+ if (self.items.len > 0) {
+ try writer.writeAll("{ ");
+ for (self.items, 0..) |attribute, i| {
+ if (i > 0) {
+ try writer.writeAll(", ");
+ }
+ try writer.print("{f}", .{attribute});
+ }
+ try writer.writeAll(" }");
+ } else {
+ try writer.writeAll("{ }");
+ }
+ }
+ };
+
+ pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
+ try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value });
+ }
+
+ pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
+ if (data.len > 0) {
+ try writer.writeAll("{ ");
+ for (data, 0..) |attribute, i| {
+ if (i > 0) {
+ try writer.writeAll(", ");
+ }
+ try writer.print("{f}", .{attribute});
+ }
+ try writer.writeAll(" }");
+ } else {
+ try writer.writeAll("{ }");
+ }
+ }
+
+ fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) {
+ return .{ .data = data };
+ }
+};
+
+pub const Tag = struct {
+ name: []const u8,
+ attributes: Attribute.List
+};
+
+pub const Lexer = struct {
+ pub const Buffers = struct {
+ scratch: std.heap.ArenaAllocator,
+ text: std.ArrayList(u8),
+
+ pub fn init(allocator: std.mem.Allocator) Buffers {
+ return Buffers{
+ .scratch = std.heap.ArenaAllocator.init(allocator),
+ .text = .empty
+ };
+ }
+
+ pub fn clear(self: *Buffers) void {
+ self.text.clearRetainingCapacity();
+ _ = self.scratch.reset(.retain_capacity);
+ }
+
+ pub fn deinit(self: *Buffers) void {
+ const allocator = self.scratch.child_allocator;
+ self.scratch.deinit();
+ self.text.deinit(allocator);
+ }
+ };
+
+ 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 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
+ };
+ }
+
+ 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 next(self: *Lexer) !?Token {
+ if (self.peeked_value) |value| {
+ self.peeked_value = null;
+ return value;
+ }
+
+ if (self.queued_end_tag) |name| {
+ self.queued_end_tag = null;
+ return Token{
+ .end_tag = name
+ };
+ }
+
+ while (true) {
+ if (self.buffers.text.items.len == 0) {
+ self.readIntoTextBuffer() catch |e| switch (e) {
+ error.EndOfStream => break,
+ else => return e
+ };
+ }
+
+ const saved_cursor = self.cursor;
+ const result = self.step() catch |e| switch(e) {
+ error.EndOfTextBuffer => {
+ self.cursor = saved_cursor;
+
+ 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
+ };
+ }
+
+ continue;
+ },
+ else => return e
+ };
+
+ if (result.token) |token| {
+ if (token == .start_tag and result.self_closing) {
+ self.queued_end_tag = token.start_tag.name;
+ }
+ return token;
+ }
+ }
+
+ return null;
+ }
+
+ pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void {
+ const value = try self.next() orelse return error.MissingEndTag;
+ if (!value.isEndTag(name)) return error.MissingEndTag;
+ }
+
+ pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List {
+ const value = try self.next() orelse return error.MissingStartTag;
+ if (!value.isStartTag(name)) return error.MissingStartTag;
+ return value.start_tag.attributes;
+ }
+
+ pub fn nextExpectText(self: *Lexer) ![]const u8 {
+ const value = try self.next() orelse return error.MissingTextTag;
+ if (value != .text) return error.MissingTextTag;
+ return value.text;
+ }
+
+ pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void {
+ var depth: usize = 0;
+ while (true) {
+ const value = try self.next() orelse return error.MissingEndTag;
+
+ if (depth == 0 and value == .end_tag) {
+ if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) {
+ return error.MismatchedEndTag;
+ }
+ break;
+ }
+
+ if (value == .start_tag) {
+ depth += 1;
+ } else if (value == .end_tag) {
+ depth -= 1;
+ }
+ }
+ }
+
+ 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);
+ }
+
+ fn isNameChar(c: u8) bool {
+ return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9');
+ }
+
+ fn hasBytes(self: *Lexer, n: usize) bool {
+ const text = self.buffers.text.items;
+ return self.cursor + n <= 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 error.EndOfTextBuffer;
+ }
+
+ fn tossBytes(self: *Lexer, n: usize) void {
+ assert(self.hasBytes(n));
+ self.cursor += n;
+ }
+
+ fn takeBytes(self: *Lexer, n: usize) ![]const u8 {
+ const result = try self.peekBytes(n);
+ self.tossBytes(n);
+ return result;
+ }
+
+ fn peekByte(self: *Lexer) !u8 {
+ return (try self.peekBytes(1))[0];
+ }
+
+ fn tossByte(self: *Lexer) void {
+ self.tossBytes(1);
+ }
+
+ fn takeByte(self: *Lexer) !u8 {
+ return (try self.takeBytes(1))[0];
+ }
+
+ fn parseName(self: *Lexer) ![]const u8 {
+ const name_start = self.cursor;
+
+ if (isNameStartChar(try self.peekByte())) {
+ self.tossByte();
+
+ while (isNameChar(try self.peekByte())) {
+ self.tossByte();
+ }
+ }
+
+ return self.buffers.text.items[name_start..self.cursor];
+ }
+
+ fn skipWhiteSpace(self: *Lexer) !void {
+ while (std.ascii.isWhitespace(try self.peekByte())) {
+ self.tossByte();
+ }
+ }
+
+ fn parseAttributeValue(self: *Lexer) ![]const u8 {
+ const quote = try self.takeByte();
+ if (quote != '"' and quote != '\'') {
+ return error.InvalidAttributeValue;
+ }
+
+ const value_start: usize = self.cursor;
+ var value_len: usize = 0;
+
+ while (true) {
+ const c = try self.takeByte();
+ if (c == '<' or c == '&') {
+ return error.InvalidAttributeValue;
+ }
+ if (c == quote) {
+ break;
+ }
+ value_len += 1;
+ }
+
+ return self.buffers.text.items[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) {
+ return null;
+ }
+
+ return switch (value) {
+ .text => |text| Node{ .text = text },
+ .start_tag => |start_tag| Node{ .tag = start_tag },
+ .end_tag => unreachable,
+ };
+ }
+
+ pub fn skip(self: *TagParser) !void {
+ assert(self.begin_called);
+ assert(!self.finish_called);
+
+ 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);
+
+ try self.lexer.skipUntilMatchingEndTag(name.items);
+ }
+ }
+};
diff --git a/src/app.zig b/src/app.zig
index b9ee9c6..0d916bd 100644
--- a/src/app.zig
+++ b/src/app.zig
@@ -1,12 +1,14 @@
const std = @import("std");
const CLI = @import("./cli.zig");
const log = std.log.scoped(.app);
+const Allocator = std.mem.Allocator;
const Math = @import("math");
const Vec2 = Math.Vec2;
const STBImage = @import("stb_image");
const STBTrueType = @import("stb_truetype");
+const Tiled = @import("tiled");
const Platform = @import("./platform.zig");
const Gfx = Platform.Gfx;
@@ -15,61 +17,162 @@ const Rect = Platform.Rect;
const App = @This();
-bg: Platform.Color,
+const Direction = enum {
+ up,
+ down,
+ left,
+ right
+};
+
+const PlayerSprites = struct {
+ up: [3]Gfx.Sprite.Id,
+ down: [3]Gfx.Sprite.Id,
+ left: [3]Gfx.Sprite.Id,
+ right: [3]Gfx.Sprite.Id,
+
+ pub fn getDirection(self: *PlayerSprites, dir: Direction) []Gfx.Sprite.Id {
+ return switch (dir) {
+ .up => &self.up,
+ .down => &self.down,
+ .left => &self.left,
+ .right => &self.right,
+ };
+ }
+};
+
show_first_window: bool,
check: bool,
player_pos: Vec2,
-sprite_sprite: Gfx.Sprite.Id,
wall_sprite: Gfx.Sprite.Id,
roboto_font: Gfx.Font.Id,
+tilesheet: Tilesheet,
+
+player_sprites: PlayerSprites,
+player_direction: Direction,
+player_frame_index: u32,
+animation_timer: Platform.Nanoseconds,
+
+const Tilesheet = struct {
+ image: Gfx.ImageData,
+ sprites: []Gfx.Sprite.Id,
+ tile_size: Vec2,
+ width: u32,
+ height: u32,
+
+ pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet {
+ const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
+ errdefer gpa.free(sprites);
+ @memset(sprites, Gfx.getNilSprite());
+
+ return Tilesheet{
+ .sprites = sprites,
+ .image = image,
+ .width = width,
+ .height = height,
+ .tile_size = tile_size
+ };
+ }
+
+ pub fn deinit(self: Tilesheet) void {
+ for (self.sprites) |sprite| {
+ Gfx.deinitSprite(sprite);
+ }
+ }
+
+ pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id {
+ if (x >= self.width or y >= self.height) {
+ log.warn("Attempt to get tile which is out of bounds", .{});
+ return Gfx.getNilSprite();
+ }
+
+ const sprite_index = y * self.width + x;
+ if (self.sprites[sprite_index] == Gfx.getNilSprite()) {
+ const sprite = Gfx.initSprite(.{ .padding = 1 });
+ Gfx.setSprite(sprite, .{
+ .image = self.image,
+ .rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
+ });
+ self.sprites[sprite_index] = sprite;
+ }
+
+ return self.sprites[sprite_index];
+ }
+};
+
pub fn init(self: *App, plt: Platform.Init) !?u8 {
- const sokoban_spritesheet = plt.assets.readFile("sokoban/sokoban_tilesheet.png");
-
- // const font = try STBTrueType.Font.init(robot_font, STBTrueType.Font.getFontOffsetForIndex(robot_font, 0).?);
-
- // const glyph = font.findGlyphIndex('A').?;
- // const scale = font.scaleForPixelHeight(16);
- // const box = font.getGlyphBitmapBox(glyph, scale, scale);
- // const width = box.x1 - box.x0;
- // const height = box.y1 - box.y0;
- // font.makeGlyphBitmap
- // std.debug.print("{}\n", .{box});
-
const robot_font = Gfx.initFont();
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
- var image = try STBImage.load(sokoban_spritesheet);
- defer image.deinit();
+ var scratch = std.heap.ArenaAllocator.init(plt.gpa);
+ defer scratch.deinit();
- const spritesheet = Gfx.ImageData{
- .width = image.width,
- .height = image.height,
- .pixels = .{ .rgba8 = image.rgba8_pixels }
- };
+ var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa);
+ defer buffers.deinit();
- const tile_size = Vec2.init(64, 64);
+ const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx"));
+ defer tilemap.deinit();
- const player_sprite = Gfx.initSprite(.{});
- Gfx.setSprite(player_sprite, .{
- .image = spritesheet,
- .rect = Rect.init(0, 4, 1, 1).multiply(tile_size)
- });
+ var tilesets: Tiled.Tileset.List = .empty;
+ defer tilesets.deinit(plt.gpa);
- const wall_sprite = Gfx.initSprite(.{});
- Gfx.setSprite(wall_sprite, .{
- .image = spritesheet,
- .rect = Rect.init(1, 0, 1, 1).multiply(tile_size)
- });
+ try tilesets.add(plt.gpa, "tiled/tilemap.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx")));
+
+ for (tilemap.layers) |layer| {
+ std.debug.print("{s}\n", .{layer.name});
+ }
+
+ const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png"));
+ const tilesheet = try Tilesheet.init(
+ plt.arena,
+ Gfx.ImageData{
+ .width = tilesheet_png.width,
+ .height = tilesheet_png.height,
+ .pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
+ },
+ 13,
+ 8,
+ .init(64, 64)
+ );
+
+ const wall_sprite = tilesheet.get(1, 0);
+
+ var player_sprites: PlayerSprites = undefined;
+ inline for (.{
+ .{
+ .dir = .up,
+ .positions = [_]Vec2{ .init(3, 4), .init(4, 4), .init(5, 4) }
+ },
+ .{
+ .dir = .down,
+ .positions = [_]Vec2{ .init(0, 4), .init(1, 4), .init(2, 4) }
+ },
+ .{
+ .dir = .right,
+ .positions = [_]Vec2{ .init(0, 6), .init(1, 6), .init(2, 6) },
+ },
+ .{
+ .dir = .left,
+ .positions = [_]Vec2{ .init(3, 6), .init(4, 6), .init(5, 6) }
+ }
+ }) |entry| {
+ var sprite_ids = player_sprites.getDirection(entry.dir);
+ inline for (0.., entry.positions) |i, pos| {
+ sprite_ids[i] = tilesheet.get(@intFromFloat(pos.x), @intFromFloat(pos.y));
+ }
+ }
self.* = App{
- .bg = .black,
.player_pos = .init(100, 100),
.show_first_window = true,
.check = false,
- .sprite_sprite = player_sprite,
+ .player_sprites = player_sprites,
.wall_sprite = wall_sprite,
- .roboto_font = robot_font
+ .roboto_font = robot_font,
+ .player_direction = .left,
+ .player_frame_index = 0,
+ .animation_timer = 0,
+ .tilesheet = tilesheet
};
return null;
@@ -96,37 +199,44 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
dir = dir.normalized();
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
- Gfx.setClearColor(self.bg);
- Gfx.drawSprite(self.sprite_sprite, self.player_pos, .init(100, 100), .white);
- Gfx.drawSprite(self.wall_sprite, self.player_pos.add(.init(0, 200)), .init(100, 100), .white);
+ var new_direction = self.player_direction;
+ if (dir.x < 0) {
+ new_direction = .left;
+ } else if (dir.x > 0) {
+ new_direction = .right;
+ } else if (dir.y > 0) {
+ new_direction = .down;
+ } else if (dir.y < 0) {
+ new_direction = .up;
+ }
+
+ if (self.player_direction != new_direction) {
+ self.player_frame_index = 0;
+ self.player_direction = new_direction;
+ }
+
+ const frames = self.player_sprites.getDirection(self.player_direction);
+
+ if (dir.x != 0 or dir.y != 0) {
+ self.animation_timer += plt.dt;
+ const animatin_time = std.time.ns_per_s / 5;
+ while (self.animation_timer >= animatin_time) {
+ self.player_frame_index = (self.player_frame_index + 1) % @as(u32, @intCast(frames.len));
+ self.animation_timer -= animatin_time;
+ }
+ } else {
+ self.animation_timer = 0;
+ self.player_frame_index = 0;
+ }
+
+ Gfx.setClearColor(.rgb(40, 40, 40));
+ Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white);
+ Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white);
Gfx.drawText(self.roboto_font, .{
.pos = .init(300, 100),
.text = "Hello, World!",
- .height = 32,
+ .height = 64,
});
-
- if (ImGUI.beginWindow(.{
- .name = "Hello",
- .size = .init(400, 200),
- .open = &self.show_first_window
- })) {
- defer ImGUI.endWindow();
-
- ImGUI.text("Player pos: {}", .{self.player_pos});
- if (ImGUI.button("foo")) {
- std.debug.print("pressed\n", .{});
- }
- if (ImGUI.checkbox("Hello", &self.check)) {
- std.debug.print("checkbox changed\n", .{});
- }
- ImGUI.separator();
- if (ImGUI.colorEdit(.{
- .label = "Color",
- .value = &self.bg,
- })) {
- std.debug.print("color changed\n", .{});
- }
- }
}
pub fn deinit(self: *App, plt: Platform.Deinit) void {
diff --git a/src/graphics.zig b/src/graphics.zig
index 6961596..4e9639e 100644
--- a/src/graphics.zig
+++ b/src/graphics.zig
@@ -34,7 +34,10 @@ const State = struct {
error_sprite: Sprite.Id,
default_spritesheet: Spritesheet.Id,
default_font_spritesheet: Spritesheet.Id,
- default_sampler: sg.Sampler,
+
+ linear_sampler: sg.Sampler,
+ nearest_sampler: sg.Sampler,
+ default_sampler: Sampler,
quads_buffer: [2048]Quad,
quads: std.ArrayList(Quad),
@@ -71,13 +74,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend()));
assert(shader.id != sg.invalid_id);
- // TODO: Make sampler configurable.
- const default_sampler = sg.makeSampler(.{
+ const linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR,
.mag_filter = .LINEAR,
- .label = "default-sampler"
+ .label = "linear-sampler"
});
- assert(default_sampler.id != sg.invalid_id);
+ assert(linear_sampler.id != sg.invalid_id);
+
+ const nearest_sampler = sg.makeSampler(.{
+ .min_filter = .NEAREST,
+ .mag_filter = .NEAREST,
+ .label = "nearest-sampler"
+ });
+ assert(linear_sampler.id != sg.invalid_id);
const pipeline = sg.makePipeline(.{
.primitive_type = .TRIANGLES,
@@ -144,7 +153,6 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.shader = shader,
.pipeline = pipeline,
.bindings = bindings,
- .default_sampler = default_sampler,
.quads_buffer = undefined,
.quads = .initBuffer(&self.quads_buffer),
.clear_color = .black,
@@ -153,6 +161,10 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.default_spritesheet = undefined,
.default_font_spritesheet = undefined,
+ .linear_sampler = linear_sampler,
+ .nearest_sampler = nearest_sampler,
+ .default_sampler = .linear,
+
.textures_buffer = undefined,
.textures = .init(.initBuffer(&self.textures_buffer)),
.nil_texture = undefined,
@@ -285,9 +297,6 @@ pub fn deinit() void {
}
}
- sg.destroyPipeline(self.pipeline);
- sg.destroyShader(self.shader);
- sg.destroySampler(self.default_sampler);
sg.shutdown();
}
@@ -336,7 +345,7 @@ pub fn beginFrame() void {
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
- self.bindings.samplers[shd.SMP_smp] = self.default_sampler;
+ self.bindings.samplers[shd.SMP_smp] = self.linear_sampler;
}
pub fn showDebug() void {
@@ -667,7 +676,18 @@ fn bindView(view: sg.View, opts: FlushOptions) void {
}
}
+pub fn setDefaultSampler(sampler: Sampler) void {
+ const self = &g_state;
+ self.default_sampler = sampler;
+}
+
+pub const Sampler = enum {
+ linear,
+ nearest,
+};
+
const DrawOptions = struct {
+ sampler: ?Sampler = null,
texture: Texture.Id,
quad: Quad
};
@@ -704,6 +724,16 @@ pub fn draw(opts: DrawOptions) void {
bindView(view, .{});
+ const sampler = switch (opts.sampler orelse self.default_sampler) {
+ .linear => self.linear_sampler,
+ .nearest => self.nearest_sampler,
+ };
+ const bound_sampler = &self.bindings.samplers[shd.SMP_smp];
+ if (bound_sampler.id != sampler.id) {
+ flush(.{});
+ bound_sampler.* = sampler;
+ }
+
if (self.quads.items.len == self.quads.capacity) {
flush(.{});
}
@@ -716,6 +746,12 @@ const SpriteOptions = struct {
padding: u32 = 0,
};
+// TODO: I don't like this getter, maybe nil_sprite should be a public global variable.
+pub fn getNilSprite() Sprite.Id {
+ const self = &g_state;
+ return self.nil_sprite;
+}
+
pub fn initSprite(opts: SpriteOptions) Sprite.Id {
const self = &g_state;
const id = self.sprites.insertBounded() catch {
@@ -1112,8 +1148,8 @@ pub fn deinitFont(id: Font.Id) void {
return;
}
- if (self.fonts.get(id)) {
- // TODO: deinit sprites
+ if (self.fonts.get(id)) |font| {
+ font.cache.deinit();
self.fonts.removeAssumeExists(id);
}
}
@@ -1142,6 +1178,8 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void {
// TODO: deinit previous glyphs
font.stb = stb_font;
+
+ font.cache.deinit();
font.cache = .init();
}
@@ -1508,6 +1546,17 @@ pub const Font = struct {
return self;
}
+ fn deinit(self: *GlyphCache) void {
+ for (self.glyphs_hashmap) |glyph_id| {
+ if (glyph_id == GlyphSlot.nil_id) {
+ continue;
+ }
+ const slot = self.glyphs[glyph_id];
+ slot.glyph.deinit();
+ }
+ self.* = undefined;
+ }
+
fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph {
return &self.glyphs[id].glyph;
}
diff --git a/src/main.zig b/src/main.zig
index e8f15ee..c1fb15c 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -3,6 +3,12 @@ const std = @import("std");
const Platform = @import("platform.zig");
const App = @import("app.zig");
+pub const std_options: std.Options = .{
+ .log_scope_levels = &.{
+ .{ .scope = .graphics, .level = .info }
+ }
+};
+
pub fn main(init: std.process.Init) void {
Platform.run(App, .{
.init = init,
diff --git a/src/platform.zig b/src/platform.zig
index cbde187..5d6b26f 100644
--- a/src/platform.zig
+++ b/src/platform.zig
@@ -34,6 +34,7 @@ fn PlatformType(App: type) type {
const Self = @This();
gpa: Allocator,
+ arena: *std.heap.ArenaAllocator,
io: Io,
cli_args: []const [:0]const u8,
@@ -71,6 +72,7 @@ fn PlatformType(App: type) type {
if (@hasDecl(App, "init")) {
const plt = Init{
.gpa = self.gpa,
+ .arena = self.arena.allocator(),
.io = self.io,
.assets = &self.assets
};
@@ -112,9 +114,10 @@ fn PlatformType(App: type) type {
if (@hasDecl(App, "frame")) {
const plt = Frame{
- .t = t,
- .dt = dt,
+ .t = t.nanoseconds,
+ .dt = dt.nanoseconds,
.gpa = self.gpa,
+ .arena = self.arena.allocator(),
.io = self.io,
.input = &self.input,
.input_events = self.events.items,
@@ -494,6 +497,7 @@ pub const Assets = struct {
pub const Init = struct {
gpa: Allocator,
+ arena: Allocator,
io: Io,
assets: *Assets
};
@@ -503,12 +507,15 @@ pub const Deinit = struct {
io: Io,
};
-pub const Frame = struct {
- t: Io.Duration,
- dt: Io.Duration,
+pub const Nanoseconds = i96;
+
+pub const Frame = struct {
+ t: Nanoseconds,
+ dt: Nanoseconds,
- frame: Allocator,
gpa: Allocator,
+ arena: Allocator,
+ frame: Allocator,
io: Io,
assets: *Assets,
input: *Input,
@@ -519,11 +526,11 @@ pub const Frame = struct {
}
pub fn deltaTime(self: Frame) f32 {
- return nanosecondsToSeconds(self.dt.nanoseconds);
+ return nanosecondsToSeconds(self.dt);
}
pub fn time(self: Frame) f32 {
- return nanosecondsToSeconds(self.t.nanoseconds);
+ return nanosecondsToSeconds(self.t);
}
};
@@ -545,6 +552,7 @@ pub fn run(App: type, opts: RunOptions) void {
const plt = arena.create(PlatformApp) catch @panic("OOM");
plt.* = PlatformApp{
.gpa = gpa,
+ .arena = opts.init.arena,
.io = io,
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"),
.app = undefined,