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, tilesets: Tileset.List, gid: u32) ?Tile { 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 getTileByPosition(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 getTileBounds(self: *const Tilemap) Layer.Bounds { var result: ?Layer.Bounds = null; for (self.layers) |layer| { if (layer.variant != .tile) { continue; } const layer_bounds = layer.variant.tile.getBounds(); if (result == null) { result = layer_bounds; } else { result = layer_bounds.combine(result.?); } } return result orelse .zero; } pub fn deinit(self: *const Tilemap) void { self.arena.deinit(); }