reimplement libtmx in zig

This commit is contained in:
Rokas Puzonas 2025-12-30 03:28:18 +02:00
parent 0349bfe50b
commit 6809b5538c
18 changed files with 2224 additions and 257 deletions

View File

@ -37,41 +37,8 @@ pub fn build(b: *std.Build) !void {
const fontstash_dependency = b.dependency("fontstash", .{}); const fontstash_dependency = b.dependency("fontstash", .{});
exe_mod.addIncludePath(fontstash_dependency.path("src")); exe_mod.addIncludePath(fontstash_dependency.path("src"));
const libxml2_dependency = b.dependency("libxml2", .{ const tiled_dependency = b.dependency("tiled", .{});
.target = target, exe_mod.addImport("tiled", tiled_dependency.module("tiled"));
.optimize = optimize,
.linkage = .static,
});
{
const libtmx_dependency = b.dependency("libtmx", .{});
const libtmx = b.addLibrary(.{
.name = "tmx",
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true
}),
});
libtmx.installHeader(libtmx_dependency.path("src/tmx.h"), "tmx.h");
libtmx.root_module.addCSourceFiles(.{
.root = libtmx_dependency.path("src"),
.files = &.{
"tmx.c",
"tmx_utils.c",
"tmx_err.c",
"tmx_xml.c",
"tmx_mem.c",
"tmx_hash.c"
},
.flags = &.{
"-fno-delete-null-pointer-checks"
}
});
libtmx.linkLibrary(libxml2_dependency.artifact("xml"));
exe_mod.linkLibrary(libtmx);
}
const sokol_dependency = b.dependency("sokol", .{ const sokol_dependency = b.dependency("sokol", .{
.target = target, .target = target,

View File

@ -29,14 +29,9 @@
.url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be", .url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
.hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6", .hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
}, },
.libtmx = .{ .tiled = .{
.url = "git+https://github.com/baylej/tmx.git#11ffdcdc9bd65669f1a8dbd3a0362a324dda2e0c", .path = "./libs/tiled"
.hash = "N-V-__8AAKQvBQCTT3Q6_we7vTVX-MkAWDZ91YkUev040IRo", }
},
.libxml2 = .{
.url = "git+https://github.com/allyourcodebase/libxml2.git?ref=2.14.3-4#86c4742a9becd6c86dc79180f806ed344fd2a727",
.hash = "libxml2-2.14.3-4-qHdjhn9FAACpyisv_5DDFVQlegox6QE3mTpdlr44RcbT",
},
}, },
.paths = .{ .paths = .{
"build.zig", "build.zig",

29
libs/tiled/build.zig Normal file
View File

@ -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);
}
}

View File

@ -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);
}

52
libs/tiled/src/color.zig Normal file
View File

@ -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;
}

View File

@ -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)
);
};

283
libs/tiled/src/layer.zig Normal file
View File

@ -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");
}

421
libs/tiled/src/object.zig Normal file
View File

@ -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,
}
}
},
\\ <object id="10" name="rectangle" type="object class" 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
}
}
},
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
\\ <point/>
\\ </object>
);
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
}
}
},
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
\\ <ellipse/>
\\ </object>
);
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 },
}
}
}
},
\\ <object id="5" x="40.125" y="96.25">
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
\\ </object>
);
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
}
}
},
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
);
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
},
}
}
},
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
\\ <text wrap="1" halign="center"> Hello World </text>
\\ </object>
);
}

View File

@ -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
};
}

153
libs/tiled/src/property.zig Normal file
View File

@ -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" }),
\\ <property name="solid" value="hello"/>
);
try expectParsedEquals(
Property.init("solid", .{ .string = "hello" }),
\\ <property name="solid" type="string" value="hello"/>
);
try expectParsedEquals(
Property.init("integer", .{ .int = 123 }),
\\ <property name="integer" type="int" value="123"/>
);
try expectParsedEquals(
Property.init("boolean", .{ .bool = true }),
\\ <property name="boolean" type="bool" value="true"/>
);
try expectParsedEquals(
Property.init("boolean", .{ .bool = false }),
\\ <property name="boolean" type="bool" value="false"/>
);
}

16
libs/tiled/src/root.zig Normal file
View File

@ -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());
}

235
libs/tiled/src/tilemap.zig Normal file
View File

@ -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();
}

226
libs/tiled/src/tileset.zig Normal file
View File

@ -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();
}

688
libs/tiled/src/xml.zig Normal file
View File

@ -0,0 +1,688 @@
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() != '=') {
std.debug.print("{s}\n", .{self.buffers.text.items[self.cursor..]});
return error.MissingAttributeEquals;
}
try self.skipWhiteSpace();
const value = try self.parseAttributeValue();
const list = Attribute.List{ .items = attributes.items };
if (list.get(name) != null) {
return error.DuplicateAttribute;
}
try attributes.append(arena, Attribute{
.name = name,
.value = value
});
}
return Attribute.List{
.items = attributes.items
};
}
test "self closing tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello />
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "tag with prolog" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <?xml version="1.0" encoding="UTF-8"?>
\\ <hello></hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "text content" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello> Hello World </hello>
);
defer ctx.deinit();
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try std.testing.expect((try ctx.lexer.next()) == null);
}
test "attributes" {
const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello a='1' b='2'/>
);
defer ctx.deinit();
const token = try ctx.lexer.next();
const attrs = token.?.start_tag.attributes;
try std.testing.expectEqualStrings("1", attrs.get("a").?);
try std.testing.expectEqualStrings("2", attrs.get("b").?);
}
};
// TODO: The API for this is easy to misuse.
// Design a better API for using Reader
// As a compromise `assert` was used to guard against some of the ways this can be misused
pub const TagParser = struct {
lexer: *Lexer,
begin_called: bool = false,
finish_called: bool = false,
pub const Node = union(enum) {
tag: Tag,
text: []const u8,
pub fn isTag(self: Node, name: []const u8) bool {
if (self == .tag) {
return std.mem.eql(u8, self.tag.name, name);
}
return false;
}
};
pub fn init(lexer: *Lexer) TagParser {
return TagParser{
.lexer = lexer,
};
}
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
assert(!self.begin_called);
self.begin_called = true;
return try self.lexer.nextExpectStartTag(name);
}
pub fn finish(self: *TagParser, name: []const u8) !void {
assert(self.begin_called);
assert(!self.finish_called);
self.finish_called = true;
try self.lexer.skipUntilMatchingEndTag(name);
}
pub fn next(self: *TagParser) !?Node {
assert(self.begin_called);
assert(!self.finish_called);
const value = try self.lexer.peek() orelse return error.MissingEndTag;
if (value == .end_tag) {
return null;
}
return 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);
}
}
};

View File

@ -14,7 +14,7 @@ const imgui = @import("./imgui.zig");
const Gfx = @import("./graphics.zig"); const Gfx = @import("./graphics.zig");
const Entity = @import("./entity.zig"); const Entity = @import("./entity.zig");
const tiled = @import("./tiled.zig"); const tiled = @import("tiled");
const Game = @This(); const Game = @This();
@ -90,15 +90,30 @@ pub fn init(gpa: Allocator) !Game {
}; };
errdefer self.deinit(); errdefer self.deinit();
const manager = try tiled.ResourceManager.init(); var scratch = std.heap.ArenaAllocator.init(gpa);
defer manager.deinit(); defer scratch.deinit();
try manager.loadTilesetFromBuffer(@embedFile("assets/tiled/tileset.tsx"), "tileset.tsx"); var xml_buffers = tiled.xml.Lexer.Buffers.init(gpa);
defer xml_buffers.deinit();
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/first.tmx")); var tilesets: tiled.Tileset.List = .empty;
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/second.tmx")); defer tilesets.deinit(gpa);
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/third.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedFile(gpa, manager, "assets/tiled/fourth.tmx")); try tilesets.add(
gpa,
"tileset.tsx",
try tiled.Tileset.initFromBuffer(
gpa,
&scratch,
&xml_buffers,
@embedFile("assets/tiled/tileset.tsx")
)
);
try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/first.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/second.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/third.tmx"));
try self.levels.append(gpa, try loadLevelFromEmbedTiled(gpa, tilesets, "assets/tiled/fourth.tmx"));
try self.restartLevel(); try self.restartLevel();
@ -125,39 +140,50 @@ fn nextLevel(self: *Game) !void {
} }
} }
fn loadLevelFromEmbedFile(gpa: Allocator, manager: tiled.ResourceManager, comptime path: []const u8) !Level { fn loadLevelFromEmbedTiled(gpa: Allocator, tilesets: tiled.Tileset.List, comptime path: []const u8) !Level {
const map = try manager.loadMapFromBuffer(@embedFile(path)); var scratch = std.heap.ArenaAllocator.init(gpa);
defer scratch.deinit();
var xml_buffers = tiled.xml.Lexer.Buffers.init(gpa);
defer xml_buffers.deinit();
const map = try tiled.Tilemap.initFromBuffer(
gpa,
&scratch,
&xml_buffers,
@embedFile(path)
);
defer map.deinit(); defer map.deinit();
return try loadLevelFromTiled(gpa, map);
}
fn loadLevelFromTiled(gpa: Allocator, map: tiled.Map) !Level {
var level: Level = .empty; var level: Level = .empty;
errdefer level.deinit(gpa); errdefer level.deinit(gpa);
var layer_iter = map.iterLayers(); for (map.layers) |*layer| {
while (layer_iter.next()) |layer| { if (!layer.visible) {
if (layer.layer.visible == 0) {
continue; continue;
} }
if (layer.layer.type != @intFromEnum(tiled.Layer.Type.layer)) { if (layer.variant != .tile) {
continue; continue;
} }
const map_width = map.map.width; for (0..map.height) |y| {
for (0..map.map.height) |y| { for (0..map.width) |x| {
for (0..map_width) |x| { const tile = map.getTile(layer, tilesets, x, y) orelse continue;
const tile_props = tile.getProperties();
const tile_type = tile_props.getString("type") orelse "";
const tile = map.getTile(layer, x, y) orelse continue; const tile_width: f32 = @floatFromInt(tile.tileset.tile_width);
const tile_props = tiled.Properties{ .inner = tile.tile.properties }; const tile_height: f32 = @floatFromInt(tile.tileset.tile_height);
const tile_type: []const u8 = std.mem.span(tile_props.getPropertyString("type") orelse ""); const tile_position_in_image = tile.tileset.getTilePositionInImage(tile.id).?;
const tile_size = Vec2.init(8, 8);
var entity: Entity = .{ var entity: Entity = .{
.type = .nil, .type = .nil,
.position = Vec2.init(@floatFromInt(x), @floatFromInt(y)), .position = Vec2.init(@floatFromInt(x), @floatFromInt(y)),
.render_tile = .{ .position = tile.getUpperLeft().divide(tile_size) }, .render_tile = .{
.position = .{
.x = tile_position_in_image.x / tile_width,
.y = tile_position_in_image.y / tile_height,
}
},
}; };
if (std.mem.eql(u8, tile_type, "player")) { if (std.mem.eql(u8, tile_type, "player")) {
entity.type = .player; entity.type = .player;

View File

@ -94,12 +94,14 @@ pub fn main() !void {
tracy.setThreadName("Main"); tracy.setThreadName("Main");
// var sa: std.posix.Sigaction = .{ if (builtin.os.tag == .linux) {
// .handler = .{ .handler = signalHandler }, var sa: std.posix.Sigaction = .{
// .mask = std.posix.sigemptyset(), .handler = .{ .handler = signalHandler },
// .flags = std.posix.SA.RESTART, .mask = std.posix.sigemptyset(),
// }; .flags = std.posix.SA.RESTART,
// std.posix.sigaction(std.posix.SIG.INT, &sa, null); };
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
}
sapp.run(.{ sapp.run(.{
.init_cb = init, .init_cb = init,

View File

@ -1,177 +0,0 @@
const std = @import("std");
const c = @cImport({
@cInclude("stdlib.h");
@cInclude("tmx.h");
});
const math = @import("math.zig");
const Vec2 = math.Vec2;
const log = std.log.scoped(.tiled);
pub const TmxError = error {
MakeResourceManager,
LoadTileset,
LoadMap,
};
pub fn init() void {
c.tmx_alloc_func = c.realloc;
c.tmx_free_func = c.free;
}
pub const ResourceManager = struct {
manager: *anyopaque,
pub fn init() !ResourceManager {
const manager = c.tmx_make_resource_manager();
if (manager == null) {
log.err("tmx_make_resource_manager: {s}", .{c.tmx_strerr()});
return TmxError.MakeResourceManager;
}
return ResourceManager{
.manager = manager.?,
};
}
pub fn loadTilesetFromBuffer(self: *const ResourceManager, tileset: []const u8, key: [*:0]const u8) !void {
const success = c.tmx_load_tileset_buffer(self.manager, tileset.ptr, @intCast(tileset.len), key);
if (success != 1) {
log.err("tmx_load_tileset_buffer: {s}", .{c.tmx_strerr()});
return TmxError.LoadTileset;
}
}
pub fn loadMapFromBuffer(self: *const ResourceManager, map: []const u8) !Map {
const map_handle = c.tmx_rcmgr_load_buffer(self.manager, map.ptr, @intCast(map.len));
if (map_handle == null) {
log.err("tmx_rcmgr_load_buffer: {s}", .{c.tmx_strerr()});
return TmxError.LoadMap;
}
return Map{
.map = map_handle.?
};
}
pub fn deinit(self: *const ResourceManager) void {
c.tmx_free_resource_manager(self.manager);
}
};
pub const Tile = struct {
tile: *c.tmx_tile,
pub fn getUpperLeft(self: Tile) Vec2 {
return Vec2.init(
@floatFromInt(self.tile.ul_x),
@floatFromInt(self.tile.ul_y),
);
}
};
pub const TileWithFlags = struct {
tile: Tile,
flags: u32
};
pub const Map = struct {
map: *c.tmx_map,
pub fn deinit(self: *const Map) void {
c.tmx_map_free(self.map);
}
pub fn iterLayers(self: *const Map) Layer.Iterator {
return Layer.Iterator{
.current = self.map.ly_head
};
}
pub fn getTile(self: *const Map, layer: Layer, x: usize, y: usize) ?Tile {
if (self.getTileWithFlags(layer, x, y)) |tile_with_flags| {
return tile_with_flags.tile;
}
return null;
}
pub fn getTileWithFlags(self: *const Map, layer: Layer, x: usize, y: usize) ?TileWithFlags {
if (layer.layer.type != @intFromEnum(Layer.Type.layer)) {
return null;
}
const gid = layer.layer.content.gids[(y*self.map.width) + x];
const flags = gid & ~FLIP_BITS_REMOVAL;
const maybe_tile = self.map.tiles[gid & FLIP_BITS_REMOVAL];
if (maybe_tile == null) {
return null;
}
return TileWithFlags{
.tile = Tile{ .tile = maybe_tile.? },
.flags = flags
};
}
};
pub const Layer = struct {
layer: *c.tmx_layer,
pub const Type = enum(c_uint) {
none = c.L_NONE,
layer = c.L_LAYER,
object_group = c.L_OBJGR,
image = c.L_IMAGE,
group = c.L_GROUP
};
pub const Iterator = struct {
current: ?*c.tmx_layer,
pub fn next(self: *Iterator) ?Layer {
if (self.current) |current| {
self.current = current.next;
return Layer{ .layer = current };
}
return null;
}
};
};
pub const Properties = struct {
inner: ?*c.tmx_properties,
pub fn getPropertyString(self: Properties, key: [*:0]const u8) ?[*:0]const u8 {
const inner = self.inner orelse return null;
const maybe_prop = c.tmx_get_property(inner, key);
if (maybe_prop == null) {
return null;
}
const prop: *c.tmx_property = maybe_prop.?;
if (prop.type != c.PT_STRING) {
return null;
}
return prop.value.string;
}
pub fn getPropertyBool(self: Properties, key: [*:0]const u8) ?bool {
const inner = self.inner orelse return null;
const maybe_prop = c.tmx_get_property(inner, key);
if (maybe_prop == null) {
return null;
}
const prop: *c.tmx_property = maybe_prop.?;
if (prop.type != c.PT_BOOL) {
return null;
}
return prop.value.boolean != 0;
}
};
pub const FLIP_BITS_REMOVAL: u32 = c.TMX_FLIP_BITS_REMOVAL;

View File

@ -5,7 +5,7 @@ const sokol = @import("sokol");
const sapp = sokol.app; const sapp = sokol.app;
const Gfx = @import("./graphics.zig"); const Gfx = @import("./graphics.zig");
const Tiled = @import("./tiled.zig"); const tiled = @import("tiled");
const Math = @import("./math.zig"); const Math = @import("./math.zig");
const Vec2 = Math.Vec2; const Vec2 = Math.Vec2;
@ -228,8 +228,6 @@ released_keys: std.EnumSet(KeyCode) = .initEmpty(),
pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}), pressed_keys_at: std.EnumMap(KeyCode, Nanoseconds) = .init(.{}),
pub fn init(self: *Window, gpa: Allocator) !void { pub fn init(self: *Window, gpa: Allocator) !void {
Tiled.init();
var events: std.ArrayList(Event) = .empty; var events: std.ArrayList(Event) = .empty;
errdefer events.deinit(gpa); errdefer events.deinit(gpa);
try events.ensureTotalCapacityPrecise(gpa, 50); try events.ensureTotalCapacityPrecise(gpa, 50);