Something went wrong. Try again.
A library for parsing Tiled maps.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462pub const Layer = struct { id: u32, name: []const u8, content: LayerContent, visible: bool, class: ?[]const u8 = null, properties: std.StringHashMapUnmanaged(Property),
pub fn fromJson(io: Io, allocator: Allocator, json_layer: JsonLayer) !Layer { var properties: std.StringHashMapUnmanaged(Property) = .empty;
if (json_layer.properties) |props| { for (props) |property| { const prop = try Property.fromJson(allocator, property); try properties.put(allocator, prop.name, prop); } }
return .{ .class = if (json_layer.class) |class| try allocator.dupe(u8, class) else null, .id = json_layer.id, .name = try allocator.dupe(u8, json_layer.name), .visible = json_layer.visible, .content = try LayerContent.fromJson(io, allocator, json_layer), .properties = properties, }; }
pub fn deinit(self: *Layer, allocator: Allocator) void { if (self.class) |class| allocator.free(class); allocator.free(self.name); self.content.deinit(allocator);
var properties_it = self.properties.valueIterator(); while (properties_it.next()) |value_ptr| { value_ptr.*.deinit(allocator); } self.properties.deinit(allocator); }
/// https://doc.mapeditor.org/en/stable/reference/jsonk-map-format/#layer pub const JsonLayer = struct { /// `tilelayer` only. chunks: ?[]Chunk.JsonChunk = null, class: ?[]const u8 = null, /// `tilelayer` only. compression: ?Compression = null, /// Array of unsigned int (GIDs) /// `tilelayer` only. layer_data: ?[]u32 = null, /// `objectgroup` only. draw_order: ?DrawOrder = .topdown, /// `tilelayer` only. encoding: ?Encoding = .csv, /// Row count. Same as map height for fixed-size maps. /// `tilelayer` only. height: ?u32 = null, /// Incremental ID - unique across all layers id: u32, /// `imagelayer` only image: ?[]const u8 = null, /// `group` only layers: ?[]JsonLayer = null, /// Whether layer is locked in the editor locked: bool = false, name: []const u8, /// `objectgroup` only. objects: ?[]Object.JsonObject = null, /// Horizontal layer offset in pixels offsetx: f32 = 0, /// Vertical layer offset in pixels offsety: f32 = 0, opacity: f32, parallaxx: f32 = 1, parallaxy: f32 = 1, properties: ?[]Property = null, /// `imagelayer` only repeatx: ?bool = null, /// `imagelayer` only repeaty: ?bool = null, /// X coordinate where layer content starts (for infinite maps) startx: ?i32 = null, /// Y coordinate where layer content starts (for infinite maps) starty: ?i32 = null, /// Hex-formatted tint color (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers tintcolor: ?Color = null, /// `imagelayer` only transparentcolor: ?Color = null, type: Type, visible: bool, /// Column count. Same as map width for fixed-size maps. /// `tilelayer` only. width: ?u32 = null, /// Horizontal layer offset in tiles. Always 0. x: i32 = 0, /// Vertical layer offset in tiles. Always 0. y: i32 = 0,
pub const DrawOrder = enum { topdown, index }; pub const Encoding = enum { csv, base64 }; pub const Type = enum { tilelayer, objectgroup, imagelayer, group };
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !JsonLayer { var layer: JsonLayer = .{ .class = if (source.object.get("class")) |class| try innerParseFromValue([]const u8, allocator, class, options) else null, .compression = if (source.object.get("compression")) |compression| try innerParseFromValue(Compression, allocator, compression, options) else null, .chunks = if (source.object.get("chunks")) |chunks| try innerParseFromValue([]Chunk.JsonChunk, allocator, chunks, options) else null, .layer_data = if (source.object.get("layerdata")) |layerdata| try innerParseFromValue([]u32, allocator, layerdata, options) else null, .draw_order = if (source.object.get("draworder")) |draworder| try innerParseFromValue(DrawOrder, allocator, draworder, options) else .topdown, .encoding = if (source.object.get("encoding")) |encoding| try innerParseFromValue(Encoding, allocator, encoding, options) else .csv, .height = if (source.object.get("height")) |height| try innerParseFromValue(u32, allocator, height, options) else null,
.id = try innerParseFromValue(u32, allocator, source.object.get("id").?, options), .image = if (source.object.get("image")) |image| try innerParseFromValue([]const u8, allocator, image, options) else null, .layers = if (source.object.get("layers")) |layers| try innerParseFromValue([]JsonLayer, allocator, layers, options) else null, .locked = if (source.object.get("locked")) |locked| try innerParseFromValue(bool, allocator, locked, options) else false, .name = try innerParseFromValue([]const u8, allocator, source.object.get("name").?, options), .objects = if (source.object.get("objects")) |objects| try innerParseFromValue([]Object.JsonObject, allocator, objects, options) else null, .offsetx = if (source.object.get("offsetx")) |offsetx| try innerParseFromValue(f32, allocator, offsetx, options) else 0, .offsety = if (source.object.get("offsety")) |offsety| try innerParseFromValue(f32, allocator, offsety, options) else 0, .opacity = try innerParseFromValue(f32, allocator, source.object.get("opacity").?, options), .parallaxx = if (source.object.get("parallaxx")) |parallaxx| try innerParseFromValue(f32, allocator, parallaxx, options) else 1, .parallaxy = if (source.object.get("parallaxy")) |parallaxy| try innerParseFromValue(f32, allocator, parallaxy, options) else 1, .properties = if (source.object.get("properties")) |properties| try innerParseFromValue([]Property, allocator, properties, options) else null, .repeatx = if (source.object.get("repeatx")) |repeatx| try innerParseFromValue(bool, allocator, repeatx, options) else null, .startx = if (source.object.get("startx")) |startx| try innerParseFromValue(i32, allocator, startx, options) else null, .starty = if (source.object.get("starty")) |starty| try innerParseFromValue(i32, allocator, starty, options) else null, .tintcolor = if (source.object.get("tintcolor")) |tintcolor| try innerParseFromValue(Color, allocator, tintcolor, options) else null, .transparentcolor = if (source.object.get("transparentcolor")) |transparentcolor| try innerParseFromValue(Color, allocator, transparentcolor, options) else null, .type = try innerParseFromValue(Type, allocator, source.object.get("type").?, options), .visible = try innerParseFromValue(bool, allocator, source.object.get("visible").?, options), .width = if (source.object.get("width")) |width| try innerParseFromValue(u32, allocator, width, options) else null, .x = if (source.object.get("x")) |x| try innerParseFromValue(i32, allocator, x, options) else 0, .y = if (source.object.get("y")) |y| try innerParseFromValue(i32, allocator, y, options) else 0, };
if (layer.type == .tilelayer) { if (source.object.get("data")) |data| { if (layer.encoding == .csv) { layer.layer_data = try innerParseFromValue([]u32, allocator, data, options); } else { const base64_data = try innerParseFromValue([]const u8, allocator, data, options); const layer_size: usize = (layer.width orelse 0) * (layer.height orelse 0);
layer.layer_data = parseBase64Data(allocator, base64_data, layer_size, layer.compression orelse .none) catch return error.UnexpectedToken; } } }
return layer; } };};
pub const Compression = enum { none, zlib, gzip, zstd,
pub fn jsonParseFromValue(_: Allocator, source: Value, _: ParseOptions) !@This() { return switch (source) { .string, .number_string => |value| cmp: { if (value.len == 0) { break :cmp .none; } else { break :cmp std.meta.stringToEnum(Compression, value) orelse .none; } }, else => .none, }; }};
pub const TileLayer = struct { data: std.ArrayList(u32), chunks: ?[]Chunk = null,
pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !TileLayer { var layer: TileLayer = .{ .data = .empty, }; if (json_layer.layer_data) |json_data| { try layer.data.insertSlice(allocator, 0, json_data); } if (json_layer.chunks) |json_chunks| { layer.chunks = try allocator.alloc(Chunk, json_chunks.len); for (json_chunks, 0..) |json_chunk, i| { layer.chunks.?[i] = try Chunk.fromJson(allocator, json_chunk, json_layer.compression orelse .none); } } return layer; }
pub fn deinit(self: *TileLayer, allocator: Allocator) void { if (self.chunks) |chunks| { for (chunks) |*chunk| { chunk.deinit(allocator); } allocator.free(chunks); } self.data.deinit(allocator); }};
pub const ObjectGroup = struct { objects: std.ArrayList(Object),
pub fn fromJson(io: Io, allocator: Allocator, json_layer: Layer.JsonLayer) !ObjectGroup { var object_group: ObjectGroup = .{ .objects = .empty, }; if (json_layer.objects) |json_objects| { for (json_objects) |json_object| { const object = try Object.fromJson(io, allocator, json_object); try object_group.objects.append(allocator, object); } }
return object_group; }
pub fn deinit(self: *ObjectGroup, allocator: Allocator) void { for (self.objects.items) |*object| { object.deinit(allocator); } self.objects.deinit(allocator); }
pub fn getByClass(self: ObjectGroup, class: []const u8) ?Object { for (self.objects.items) |object| { if (object.class) |object_class| { if (std.mem.eql(u8, object_class, class)) { return object; } } } return null; }
pub fn get(self: ObjectGroup, name: []const u8) ?Object { for (self.objects.items) |object| { if (std.mem.eql(u8, object.name, name)) { return object; } } return null; }};pub const ImageLayer = struct { image: []const u8, repeat_x: bool, repeat_y: bool, transparent_color: ?Color = null,
pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !ImageLayer { return .{ .image = try allocator.dupe(u8, json_layer.image orelse return error.MissingField), .repeat_x = json_layer.repeatx orelse false, .repeat_y = json_layer.repeaty orelse false, .transparent_color = json_layer.transparentcolor, }; }
pub fn deinit(self: *ImageLayer, allocator: Allocator) void { allocator.free(self.image); }};
pub const Group = struct { layers: std.StringHashMapUnmanaged(Layer),
pub fn fromJson(io: Io, allocator: Allocator, json_layer: Layer.JsonLayer) anyerror!Group { var layers: std.StringHashMapUnmanaged(Layer) = .empty; if (json_layer.layers) |json_layers| { for (json_layers) |sub_layer| { const l = try Layer.fromJson(io, allocator, sub_layer); try layers.put(allocator, l.name, l); } } return .{ .layers = layers }; }
pub fn deinit(self: *Group, allocator: Allocator) void { var it = self.layers.valueIterator(); while (it.next()) |l| { l.*.deinit(allocator); } self.layers.deinit(allocator); }};
pub const LayerContent = union(enum) { tile_layer: TileLayer, object_group: ObjectGroup, image_layer: ImageLayer, group: Group,
pub fn fromJson(io: Io, allocator: Allocator, json_layer: Layer.JsonLayer) !LayerContent { return switch (json_layer.type) { .tilelayer => .{ .tile_layer = try TileLayer.fromJson(allocator, json_layer) }, .objectgroup => .{ .object_group = try ObjectGroup.fromJson(io, allocator, json_layer) }, .imagelayer => .{ .image_layer = try ImageLayer.fromJson(allocator, json_layer) }, .group => .{ .group = try Group.fromJson(io, allocator, json_layer) }, }; }
pub fn deinit(self: *LayerContent, allocator: Allocator) void { switch (self.*) { .tile_layer => |*layer| layer.deinit(allocator), .object_group => |*og| og.deinit(allocator), .image_layer => |*image| image.deinit(allocator), .group => |*group| group.deinit(allocator), } }};
/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunkpub const Chunk = struct { data: []u32, height: u32, width: u32, x: i32, y: i32,
/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk pub const JsonChunk = struct { /// Array of unsigned int (GIDs) or base64-encoded data data: EncodedData, height: u32, width: u32, x: i32, y: i32,
const EncodedData = union(Layer.JsonLayer.Encoding) { csv: []u32, base64: []const u8, };
pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { var chunk: JsonChunk = .{ .height = try innerParseFromValue(u32, allocator, source.object.get("height").?, options), .width = try innerParseFromValue(u32, allocator, source.object.get("width").?, options), .x = try innerParseFromValue(i32, allocator, source.object.get("x").?, options), .y = try innerParseFromValue(i32, allocator, source.object.get("y").?, options), .data = undefined, };
if (source.object.get("data")) |data| { if (data == .array) { chunk.data = .{ .csv = try innerParseFromValue([]u32, allocator, data, options) }; } if (data == .string) { chunk.data = .{ .base64 = try innerParseFromValue([]const u8, allocator, data, options) }; } } return chunk; } };
pub fn fromJson(allocator: Allocator, json_chunk: JsonChunk, compression: Compression) !Chunk { var arena_state = std.heap.ArenaAllocator.init(allocator); defer arena_state.deinit(); const arena = arena_state.allocator();
const data: []u32 = try allocator.dupe( u32, set_data: { switch (json_chunk.data) { .csv => break :set_data json_chunk.data.csv, .base64 => { const base64_data = try parseBase64Data(arena, json_chunk.data.base64, json_chunk.width * json_chunk.height, compression);
break :set_data base64_data; }, } }, );
return .{ .x = json_chunk.x, .y = json_chunk.y, .width = json_chunk.width, .height = json_chunk.height,
.data = data, }; }
pub fn deinit(self: *Chunk, allocator: Allocator) void { allocator.free(self.data); }};
// Decode base64 data (and optionally decompress) into a slice of u32 Global Tile Ids allocated on the heap, caller owns slicefn parseBase64Data(allocator: Allocator, base64_data: []const u8, size: usize, compression: Compression) ![]u32 { const decoded_size = try base64_decoder.calcSizeForSlice(base64_data); const base64_decoded = try allocator.alloc(u8, decoded_size); defer allocator.free(base64_decoded);
try base64_decoder.decode(base64_decoded, base64_data);
const decoded = if (compression != .none) try decompress(allocator, base64_decoded, compression) else base64_decoded; defer if (compression != .none) allocator.free(decoded);
const stride = @sizeOf(u32); if (size * stride != decoded.len) return error.DataSizeMismatch;
const data = try allocator.alloc(u32, size);
for (data, 0..) |*tile, i| { const tile_index = i * stride; tile.* = std.mem.readInt(u32, decoded[tile_index..][0..stride], .little); }
return data;}
fn decompress(allocator: Allocator, compressed: []const u8, compression: Compression) ![]u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit();
var compressed_reader: std.Io.Reader = .fixed(compressed);
switch (compression) { .gzip, .zlib => { const format: std.compress.flate.Container = switch (compression) { .gzip => .gzip, .zlib => .zlib, else => unreachable, }; var decompresser: std.compress.flate.Decompress = .init(&compressed_reader, format, &.{}); _ = try decompresser.reader.streamRemaining(&out.writer); }, .zstd => { var decompresser: std.compress.zstd.Decompress = .init(&compressed_reader, &.{}, .{}); _ = try decompresser.reader.streamRemaining(&out.writer); }, .none => unreachable, }
return try out.toOwnedSlice();}
const tmz = @import("root.zig");const Color = tmz.Color;const Property = tmz.Property;const Object = tmz.Object;
const std = @import("std");const Io = std.Io;const base64_decoder = std.base64.standard.Decoder;const ParseOptions = std.json.ParseOptions;const Value = std.json.Value;const Allocator = std.mem.Allocator;const innerParseFromValue = std.json.innerParseFromValue;