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 Bounds = struct { left: i32, right: i32, top: i32, bottom: i32, pub const zero = Bounds{ .left = 0, .right = 0, .top = 0, .bottom = 0, }; pub fn initFromRect(x: i32, y: i32, width: i32, height: i32) Bounds { return Bounds{ .left = x, .right = x + width, .top = y, .bottom = y + height, }; } pub fn getWidth(self: Bounds) u32 { return @intCast(self.right - self.left + 1); } pub fn getHeight(self: Bounds) u32 { return @intCast(self.bottom - self.top + 1); } pub fn combine(lhs: Bounds, rhs: Bounds) Bounds { return Bounds{ .left = @min(lhs.left, rhs.left), .right = @max(lhs.right, rhs.right), .top = @min(lhs.top, rhs.top), .bottom = @max(lhs.bottom, rhs.bottom) }; } }; pub const TileVariant = struct { pub const Chunk = struct { x: i32, y: i32, width: u32, height: u32, data: []u32, pub fn getBounds(self: Chunk) Bounds { return Bounds.initFromRect(self.x, self.y, @intCast(self.width), @intCast(self.height)); } }; pub const Data = union(enum) { fixed: []u32, chunks: []Chunk }; const Encoding = enum { csv, const map: std.StaticStringMap(Encoding) = .initComptime(.{ .{ "csv", .csv }, }); }; width: u32, height: u32, data: Data, fn initChunkDataFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, lexer: *xml.Lexer, encoding: Encoding ) !Chunk { var iter = xml.TagParser.init(lexer); const attrs = try iter.begin("chunk"); const x = try attrs.getNumber(i32, "x") orelse return error.MissingAttribute; const y = try attrs.getNumber(i32, "y") orelse return error.MissingAttribute; const width = try attrs.getNumber(u32, "width") orelse return error.MissingAttribute; const height = try attrs.getNumber(u32, "height") orelse return error.MissingAttribute; var temp_tiles: std.ArrayList(u32) = .empty; while (try iter.next()) |node| { if (node == .text) { const encoded_tiles = try lexer.nextExpectText(); const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles); try temp_tiles.appendSlice(scratch.allocator(), tiles.items); continue; } try iter.skip(); } try iter.finish("chunk"); return Chunk{ .x = x, .y = y, .width = width, .height = height, .data = try arena.dupe(u32, temp_tiles.items) }; } fn initDataFromXml( arena: std.mem.Allocator, scratch: *std.heap.ArenaAllocator, lexer: *xml.Lexer, ) !Data { var iter = xml.TagParser.init(lexer); const attrs = try iter.begin("data"); const encoding = try attrs.getEnum(Encoding, "encoding", Encoding.map) orelse .csv; // TODO: compression var temp_chunks: std.ArrayList(Chunk) = .empty; var temp_tiles: std.ArrayList(u32) = .empty; while (try iter.next()) |node| { if (node == .text) { const encoded_tiles = try lexer.nextExpectText(); const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles); try temp_tiles.appendSlice(scratch.allocator(), tiles.items); } else if (node.isTag("chunk")) { const chunk = try initChunkDataFromXml(arena, scratch, lexer, encoding); try temp_chunks.append(scratch.allocator(), chunk); continue; } try iter.skip(); } try iter.finish("data"); if (temp_chunks.items.len > 0) { return .{ .chunks = try arena.dupe(Chunk, temp_chunks.items) }; } else { return .{ .fixed = try arena.dupe(u32, temp_tiles.items) }; } } fn parseEncoding( encoding: Encoding, allocator: std.mem.Allocator, text: []const u8 ) !std.ArrayList(u32) { return switch (encoding) { .csv => try parseCSVEncoding(allocator, text) }; } fn parseCSVEncoding( allocator: std.mem.Allocator, text: []const u8 ) !std.ArrayList(u32) { var result: std.ArrayList(u32) = .empty; 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 result.append(allocator, tile_id); } return result; } pub fn getBounds(self: TileVariant) Bounds { if (self.data == .fixed) { return Bounds.initFromRect(0, 0, @intCast(self.width), @intCast(self.height)); } else if (self.data == .chunks) { const chunks = self.data.chunks; var result: Bounds = .zero; if (chunks.len > 0) { result = chunks[0].getBounds(); for (chunks[1..]) |chunk| { result = result.combine(chunk.getBounds()); } } return result; } else { unreachable; } } pub fn get(self: TileVariant, x: i32, y: i32) ?u32 { if (self.data == .fixed) { if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) { const x_u32: u32 = @intCast(x); const y_u32: u32 = @intCast(y); return self.data.fixed[y_u32 * self.width + x_u32]; } } else if (self.data == .chunks) { for (self.data.chunks) |chunk| { const ox = x - chunk.x; const oy = y - chunk.y; if ((0 <= ox and ox < chunk.width) and (0 <= oy and oy < chunk.height)) { const ox_u32: u32 = @intCast(ox); const oy_u32: u32 = @intCast(oy); return chunk.data[oy_u32 * chunk.width + ox_u32]; } } } else { unreachable; } 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 = .{ .fixed = &[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"); }