A library for parsing Tiled maps.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Tileset support

This adds two new methods to the tmz API: `initTilesetFromSlice` and
`initTilesetFromFile` that will parse a JSON formatted Tileset document
into a `tmz.Tileset` struct.

+1049
+463
src/layer.zig
··· 1 + pub const TileLayer = struct { 2 + data: std.ArrayListUnmanaged(u32), 3 + chunks: ?[]Chunk = null, 4 + 5 + pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !TileLayer { 6 + var layer: TileLayer = .{ 7 + .data = .empty, 8 + }; 9 + if (json_layer.layer_data) |json_data| { 10 + try layer.data.insertSlice(allocator, 0, json_data); 11 + } 12 + return layer; 13 + } 14 + 15 + pub fn deinit(self: *TileLayer, allocator: Allocator) void { 16 + self.data.deinit(allocator); 17 + } 18 + }; 19 + 20 + pub const ObjectGroup = struct { 21 + objects: std.ArrayListUnmanaged(Object), 22 + 23 + pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !ObjectGroup { 24 + var object_group: ObjectGroup = .{ 25 + .objects = .empty, 26 + }; 27 + if (json_layer.objects) |json_objects| { 28 + for (json_objects) |json_object| { 29 + // var object = json_object; 30 + // if (json_object.type) |object_type| object.type = try allocator.dupe(u8, object_type); 31 + const object = try Object.fromJson(allocator, json_object); 32 + try object_group.objects.append(allocator, object); 33 + } 34 + } 35 + 36 + return object_group; 37 + } 38 + 39 + pub fn deinit(self: *ObjectGroup, allocator: Allocator) void { 40 + for (self.objects.items) |*object| { 41 + object.deinit(allocator); 42 + } 43 + self.objects.deinit(allocator); 44 + } 45 + 46 + pub fn findByClass(self: ObjectGroup, class: []const u8) ?Object { 47 + for (self.objects.items) |object| { 48 + if (object.class) |object_class| { 49 + if (std.mem.eql(u8, object_class, class)) { 50 + return object; 51 + } 52 + } 53 + } 54 + return null; 55 + } 56 + }; 57 + pub const ImageLayer = struct {}; 58 + pub const Group = struct {}; 59 + 60 + pub const Layer = struct { 61 + id: u32, 62 + class: ?[]const u8 = null, 63 + content: LayerContent, 64 + visible: bool, 65 + 66 + pub fn fromJson(allocator: Allocator, json_layer: JsonLayer) !Layer { 67 + return .{ 68 + .class = if (json_layer.class) |class| try allocator.dupe(u8, class) else null, 69 + .id = json_layer.id, 70 + .visible = json_layer.visible, 71 + .content = try LayerContent.fromJson(allocator, json_layer), 72 + }; 73 + } 74 + 75 + pub fn deinit(self: *Layer, allocator: Allocator) void { 76 + if (self.class) |class| allocator.free(class); 77 + self.content.deinit(allocator); 78 + } 79 + 80 + /// https://doc.mapeditor.org/en/stable/reference/jsonk-map-format/#layer 81 + pub const JsonLayer = struct { 82 + /// `tilelayer` only. 83 + chunks: ?[]Chunk = null, 84 + class: ?[]const u8 = null, 85 + /// `tilelayer` only. 86 + compression: ?Compression = null, 87 + /// Array of unsigned int (GIDs) 88 + /// `tilelayer` only. 89 + layer_data: ?[]u32 = null, 90 + /// `objectgroup` only. 91 + draw_order: ?DrawOrder = .topdown, 92 + /// `tilelayer` only. 93 + encoding: ?Encoding = .csv, 94 + /// Row count. Same as map height for fixed-size maps. 95 + /// `tilelayer` only. 96 + height: ?u32 = null, 97 + /// Incremental ID - unique across all layers 98 + id: u32, 99 + /// `imagelayer` only 100 + image: ?[]const u8 = null, 101 + /// `group` only 102 + layers: ?[]JsonLayer = null, 103 + /// Whether layer is locked in the editor 104 + locked: bool = false, 105 + name: []const u8, 106 + /// `objectgroup` only. 107 + objects: ?[]Object.JsonObject = null, 108 + /// Horizontal layer offset in pixels 109 + offset_x: f32 = 0, 110 + /// Vertical layer offset in pixels 111 + offset_y: f32 = 0, 112 + opacity: f32, 113 + parallax_x: f32 = 1, 114 + parallax_y: f32 = 1, 115 + properties: ?std.StringHashMapUnmanaged(Property) = null, 116 + /// `imagelayer` only 117 + repeat_x: ?bool = null, 118 + /// `imagelayer` only 119 + repeat_y: ?bool = null, 120 + /// X coordinate where layer content starts (for infinite maps) 121 + start_x: ?i32 = null, 122 + /// Y coordinate where layer content starts (for infinite maps) 123 + start_y: ?i32 = null, 124 + /// Hex-formatted tint color (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers 125 + tint_color: ?[]const u8 = null, 126 + /// `imagelayer` only 127 + transparent_color: ?[]const u8 = null, 128 + type: Type, 129 + visible: bool, 130 + /// Column count. Same as map width for fixed-size maps. 131 + /// `tilelayer` only. 132 + width: ?u32 = null, 133 + /// Horizontal layer offset in tiles. Always 0. 134 + x: i32 = 0, 135 + /// Vertical layer offset in tiles. Always 0. 136 + y: i32 = 0, 137 + 138 + pub const DrawOrder = enum { topdown, index }; 139 + pub const Encoding = enum { csv, base64 }; 140 + pub const Type = enum { tilelayer, objectgroup, imagelayer, group }; 141 + 142 + pub const Compression = enum { 143 + none, 144 + zlib, 145 + gzip, 146 + zstd, 147 + 148 + pub fn jsonParseFromValue(_: Allocator, source: Value, _: ParseOptions) !@This() { 149 + return switch (source) { 150 + .string, .number_string => |value| cmp: { 151 + if (value.len == 0) { 152 + break :cmp .none; 153 + } else { 154 + break :cmp std.meta.stringToEnum(Compression, value) orelse .none; 155 + } 156 + }, 157 + else => .none, 158 + }; 159 + } 160 + }; 161 + 162 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 163 + var layer = try jsonParser(@This(), allocator, source, options); 164 + 165 + if (source.object.get("properties")) |props| { 166 + const properties = try std.json.innerParseFromValue([]Property, allocator, props, .{}); 167 + layer.properties = std.StringHashMapUnmanaged(Property).empty; 168 + for (properties) |property| { 169 + try layer.properties.?.put(allocator, property.name, property); 170 + } 171 + } 172 + 173 + if (layer.type == .tilelayer) { 174 + if (layer.chunks) |chunks| { 175 + for (chunks) |*chunk| { 176 + const chunk_size: usize = chunk.width * chunk.height; 177 + if (layer.encoding == .base64) { 178 + chunk.data = .{ .csv = parseBase64Data(allocator, chunk.data.base64, chunk_size, layer.compression orelse .none) }; 179 + } 180 + } 181 + } 182 + if (source.object.get("data")) |data| { 183 + if (layer.encoding == .csv) { 184 + layer.layer_data = try std.json.parseFromValueLeaky([]u32, allocator, data, options); 185 + } else { 186 + const base64_data = try std.json.parseFromValueLeaky([]const u8, allocator, data, options); 187 + const layer_size: usize = (layer.width orelse 0) * (layer.height orelse 0); 188 + 189 + layer.layer_data = parseBase64Data(allocator, base64_data, layer_size, layer.compression orelse .none); 190 + } 191 + } 192 + } 193 + return layer; 194 + } 195 + }; 196 + }; 197 + 198 + pub const LayerContent = union(enum) { 199 + tile_layer: TileLayer, 200 + object_group: ObjectGroup, 201 + image_layer: ImageLayer, 202 + group: Group, 203 + 204 + pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !LayerContent { 205 + switch (json_layer.type) { 206 + .tilelayer => { 207 + return .{ 208 + .tile_layer = try TileLayer.fromJson(allocator, json_layer), 209 + }; 210 + }, 211 + else => return .{ .object_group = try ObjectGroup.fromJson(allocator, json_layer) }, 212 + } 213 + } 214 + 215 + pub fn deinit(self: *LayerContent, allocator: Allocator) void { 216 + switch (self.*) { 217 + .tile_layer => |*layer| { 218 + layer.deinit(allocator); 219 + }, 220 + .object_group => |*group| { 221 + group.deinit(allocator); 222 + }, 223 + else => {}, 224 + } 225 + } 226 + }; 227 + 228 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk 229 + pub const Chunk = struct { 230 + /// Array of unsigned int (GIDs) or base64-encoded data 231 + data: EncodedData, 232 + height: u32, 233 + width: u32, 234 + x: u32, 235 + y: u32, 236 + 237 + const EncodedData = union(Layer.JsonLayer.Encoding) { 238 + csv: []const u32, 239 + base64: []const u8, 240 + }; 241 + 242 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 243 + var chunk = try jsonParser(@This(), allocator, source, options); 244 + 245 + if (source.object.get("data")) |data| { 246 + switch (data) { 247 + .array => { 248 + chunk.data = .{ .csv = try std.json.parseFromValueLeaky([]const u32, allocator, data, options) }; 249 + }, 250 + .string => { 251 + chunk.data = .{ .base64 = try std.json.parseFromValueLeaky([]const u8, allocator, data, options) }; 252 + }, 253 + else => return error.UnexpectedToken, 254 + } 255 + } 256 + return chunk; 257 + } 258 + }; 259 + 260 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#object 261 + pub const Object = struct { 262 + gid: ?u32 = null, 263 + height: f32, 264 + id: u32, 265 + name: []const u8, 266 + point: ?bool = null, 267 + polygon: ?[]Point = null, 268 + polyline: ?[]Point = null, 269 + properties: ?[]Property = null, 270 + rotation: f32, 271 + template: ?[]const u8 = null, 272 + text: ?Text = null, 273 + class: ?[]const u8 = null, 274 + visible: bool, 275 + width: f32, 276 + x: f32, 277 + y: f32, 278 + 279 + type: Type, 280 + 281 + pub const Type = enum { 282 + rectangle, 283 + ellipse, 284 + polygon, 285 + polyline, 286 + tile, 287 + text, 288 + }; 289 + 290 + pub fn fromJson(allocator: Allocator, json_object: JsonObject) !Object { 291 + const object: Object = .{ 292 + .gid = json_object.gid, 293 + .height = json_object.height, 294 + .id = json_object.id, 295 + .name = try allocator.dupe(u8, json_object.name), 296 + .point = json_object.point, 297 + .class = if (json_object.type) |class| try allocator.dupe(u8, class) else null, 298 + .rotation = json_object.rotation, 299 + .visible = json_object.visible, 300 + .width = json_object.width, 301 + .x = json_object.x, 302 + .y = json_object.y, 303 + 304 + .type = set_type: { 305 + if (json_object.gid) |_| { 306 + break :set_type .tile; 307 + } 308 + if (json_object.ellipse) |_| { 309 + break :set_type .ellipse; 310 + } 311 + if (json_object.polygon) |_| { 312 + break :set_type .polygon; 313 + } 314 + if (json_object.polyline) |_| { 315 + break :set_type .polyline; 316 + } 317 + if (json_object.text) |_| { 318 + break :set_type .text; 319 + } 320 + break :set_type .rectangle; 321 + }, 322 + }; 323 + return object; 324 + } 325 + 326 + pub fn deinit(self: *Object, allocator: Allocator) void { 327 + allocator.free(self.name); 328 + if (self.class) |class| allocator.free(class); 329 + } 330 + 331 + const JsonObject = struct { 332 + ellipse: ?bool = null, 333 + gid: ?u32 = null, 334 + height: f32, 335 + id: u32, 336 + name: []const u8, 337 + point: ?bool = null, 338 + polygon: ?[]Point = null, 339 + polyline: ?[]Point = null, 340 + properties: ?[]Property = null, 341 + rotation: f32, 342 + template: ?[]const u8 = null, 343 + text: ?Text = null, 344 + type: ?[]const u8 = null, 345 + visible: bool, 346 + width: f32, 347 + x: f32, 348 + y: f32, 349 + }; 350 + }; 351 + 352 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#point 353 + pub const Point = struct { 354 + x: f32, 355 + y: f32, 356 + }; 357 + 358 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#text 359 + pub const Text = struct { 360 + bold: bool, 361 + color: []const u8, 362 + font_family: []const u8 = "sans-serif", 363 + h_align: enum { center, right, justify, left } = .left, 364 + italic: bool = false, 365 + kerning: bool = true, 366 + pixel_size: usize = 16, 367 + strikeout: bool = false, 368 + text: []const u8, 369 + underline: bool = false, 370 + v_align: enum { center, bottom, top } = .top, 371 + wrap: bool = false, 372 + 373 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 374 + return try jsonParser(@This(), allocator, source, options); 375 + } 376 + }; 377 + 378 + // Decode base64 data (and optionally decompress) into a slice of u32 Global Tile Ids allocated on the heap, caller owns slice 379 + fn parseBase64Data(allocator: Allocator, base64_data: []const u8, size: usize, compression: Layer.JsonLayer.Compression) []u32 { 380 + const decoded_size = base64_decoder.calcSizeForSlice(base64_data) catch @panic("Unable to decode base64 data"); 381 + var decoded = allocator.alloc(u8, decoded_size) catch @panic("OOM"); 382 + defer allocator.free(decoded); 383 + 384 + base64_decoder.decode(decoded, base64_data) catch @panic("Unable to decode base64 data"); 385 + 386 + const data = allocator.alloc(u32, size) catch @panic("OOM"); 387 + 388 + const alignment = @alignOf(u32); 389 + 390 + if (compression != .none) 391 + decoded = decompress(allocator, decoded, size, compression); 392 + 393 + if (size * alignment != decoded.len) 394 + @panic("data size does not match Layer dimensions"); 395 + 396 + for (data, 0..) |*tile, i| { 397 + const tile_index = i * alignment; 398 + const end = tile_index + alignment; 399 + tile.* = std.mem.readInt(u32, decoded[tile_index..end][0..alignment], .little); 400 + } 401 + 402 + return data; 403 + } 404 + 405 + // caller owns returned slice 406 + fn decompress(allocator: Allocator, compressed: []const u8, size: usize, compression: Layer.JsonLayer.Compression) []u8 { 407 + const decompressed = allocator.alloc(u8, size * @alignOf(u32)) catch @panic("OOM"); 408 + var decompressed_buf = std.io.fixedBufferStream(decompressed); 409 + var compressed_buf = std.io.fixedBufferStream(compressed); 410 + 411 + return switch (compression) { 412 + .gzip => { 413 + std.compress.gzip.decompress(compressed_buf.reader(), decompressed_buf.writer()) catch @panic("Unable to decompress gzip"); 414 + return decompressed; 415 + }, 416 + .zlib => { 417 + std.compress.zlib.decompress(compressed_buf.reader(), decompressed_buf.writer()) catch @panic("Unable to decompress zlib"); 418 + return decompressed; 419 + }, 420 + .zstd => { 421 + const window_buffer = allocator.alloc(u8, std.compress.zstd.DecompressorOptions.default_window_buffer_len) catch @panic("OOM"); 422 + defer allocator.free(window_buffer); 423 + 424 + var zstd_stream = std.compress.zstd.decompressor(compressed_buf.reader(), .{ .window_buffer = window_buffer }); 425 + _ = zstd_stream.reader().readAll(decompressed) catch @panic("Unable to decompress zstd"); 426 + 427 + return decompressed; 428 + }, 429 + .none => unreachable, 430 + }; 431 + } 432 + 433 + test "Layer" { 434 + const allocator = std.testing.allocator; 435 + 436 + const json = @embedFile("test/object_layer.json"); 437 + 438 + const parsed_layer = try std.json.parseFromSlice(Value, allocator, json, .{ .ignore_unknown_fields = true }); 439 + defer parsed_layer.deinit(); 440 + const managed_layer = try std.json.parseFromValue(Layer.JsonLayer, allocator, parsed_layer.value, .{ .ignore_unknown_fields = true }); 441 + defer managed_layer.deinit(); 442 + const layer = managed_layer.value; 443 + 444 + const properties = layer.properties.?; 445 + var iterator = properties.iterator(); 446 + while (iterator.next()) |entry| { 447 + try std.testing.expectEqualStrings("custom", entry.value_ptr.name); 448 + } 449 + 450 + try expectEqual(Layer.JsonLayer.DrawOrder.topdown, layer.draw_order); 451 + } 452 + 453 + const Property = @import("property.zig").Property; 454 + const tmz = @import("tmz.zig"); 455 + const Color = tmz.Color; 456 + const jsonParser = tmz.jsonParser; 457 + 458 + const std = @import("std"); 459 + const base64_decoder = std.base64.standard.Decoder; 460 + const ParseOptions = std.json.ParseOptions; 461 + const Value = std.json.Value; 462 + const Allocator = std.mem.Allocator; 463 + const expectEqual = std.testing.expectEqual;
+77
src/property.zig
··· 1 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#property 2 + pub const Property = struct { 3 + name: []const u8, 4 + property_type: ?[]const u8, 5 + type: enum { string, int, float, bool, color, file } = .string, 6 + value: union(enum) { 7 + string: []const u8, 8 + int: u32, 9 + float: f32, 10 + bool: bool, 11 + color: Color, 12 + file: []const u8, 13 + }, 14 + 15 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !Property { 16 + var property = try tmz.jsonParser(Property, allocator, source, options); 17 + const value = source.object.get("value") orelse return error.UnexpectedToken; 18 + property.value = switch (property.type) { 19 + .string => .{ .string = try std.json.innerParseFromValue([]const u8, allocator, value, options) }, 20 + .int => .{ .int = try std.json.innerParseFromValue(u32, allocator, value, options) }, 21 + .float => .{ .float = try std.json.innerParseFromValue(f32, allocator, value, options) }, 22 + .bool => .{ .bool = try std.json.innerParseFromValue(bool, allocator, value, options) }, 23 + .color => .{ .color = try std.json.innerParseFromValue(Color, allocator, value, options) }, 24 + .file => .{ .file = try std.json.innerParseFromValue([]const u8, allocator, value, options) }, 25 + }; 26 + 27 + return property; 28 + } 29 + }; 30 + 31 + test "Property is parsed correctly" { 32 + const properties_json = @embedFile("test/properties.json"); 33 + 34 + const parsed_value = try std.json.parseFromSlice(Value, std.testing.allocator, properties_json, .{ .ignore_unknown_fields = true }); 35 + defer parsed_value.deinit(); 36 + const managed_properties = try std.json.parseFromValue([]Property, std.testing.allocator, parsed_value.value, .{ .ignore_unknown_fields = true }); 37 + defer managed_properties.deinit(); 38 + 39 + const properties = managed_properties.value; 40 + const string_prop = properties[0]; 41 + try std.testing.expectEqualStrings("name", string_prop.name); 42 + try std.testing.expectEqual(.string, string_prop.type); 43 + try std.testing.expectEqualStrings("game", string_prop.value.string); 44 + 45 + const int_prop = properties[1]; 46 + try std.testing.expectEqualStrings("width", int_prop.name); 47 + try std.testing.expectEqual(.int, int_prop.type); 48 + try std.testing.expectEqual(640, int_prop.value.int); 49 + 50 + const float_prop = properties[2]; 51 + try std.testing.expectEqualStrings("scale", float_prop.name); 52 + try std.testing.expectEqual(.float, float_prop.type); 53 + try std.testing.expectEqual(1.5, float_prop.value.float); 54 + 55 + const color_prop = properties[3]; 56 + try std.testing.expectEqualStrings("bg", color_prop.name); 57 + try std.testing.expectEqual(.color, color_prop.type); 58 + try std.testing.expectEqual(Color{ .a = 0xff, .r = 0xa0, .g = 0xb0, .b = 0xc0 }, color_prop.value.color); 59 + 60 + const bool_prop = properties[4]; 61 + try std.testing.expectEqualStrings("fullscreen", bool_prop.name); 62 + try std.testing.expectEqual(.bool, bool_prop.type); 63 + try std.testing.expectEqual(true, bool_prop.value.bool); 64 + 65 + const file_prop = properties[5]; 66 + try std.testing.expectEqualStrings("splashscreen", file_prop.name); 67 + try std.testing.expectEqual(.file, file_prop.type); 68 + try std.testing.expectEqualStrings("splashscreen.png", file_prop.value.file); 69 + } 70 + 71 + const std = @import("std"); 72 + const ParseOptions = std.json.ParseOptions; 73 + const Value = std.json.Value; 74 + const Allocator = std.mem.Allocator; 75 + 76 + const tmz = @import("tmz.zig"); 77 + const Color = tmz.Color;
+24
src/test/images_tileset.tsj
··· 1 + { "columns":0, 2 + "grid": 3 + { 4 + "height":1, 5 + "orientation":"orthogonal", 6 + "width":1 7 + }, 8 + "margin":0, 9 + "name":"test", 10 + "spacing":0, 11 + "tilecount":1, 12 + "tiledversion":"1.11.2", 13 + "tileheight":16, 14 + "tiles":[ 15 + { 16 + "id":0, 17 + "image":"tile.png", 18 + "imageheight":16, 19 + "imagewidth":16 20 + }], 21 + "tilewidth":16, 22 + "type":"tileset", 23 + "version":"1.10" 24 + }
+32
src/test/object_layer.json
··· 1 + { 2 + "draworder": "topdown", 3 + "id": 2, 4 + "name": "Object Layer 1", 5 + "objects": [ 6 + { 7 + "height": 19, 8 + "id": 1, 9 + "name": "", 10 + "rotation": 0, 11 + "text": { 12 + "text": "Hello World", 13 + "wrap": true 14 + }, 15 + "type": "", 16 + "visible": true, 17 + "width": 91.4375, 18 + "x": 70, 19 + "y": 44.6353383458647 20 + } 21 + ], 22 + "opacity": 1, 23 + "properties":[{ 24 + "name":"custom", 25 + "type":"int", 26 + "value":50 27 + }], 28 + "type": "objectgroup", 29 + "visible": true, 30 + "x": 0, 31 + "y": 0 32 + }
+38
src/test/properties.json
··· 1 + [ 2 + { 3 + "name": "name", 4 + "propertytype": "", 5 + "type": "string", 6 + "value": "game" 7 + }, 8 + { 9 + "name": "width", 10 + "propertytype": "", 11 + "type": "int", 12 + "value": 640 13 + }, 14 + { 15 + "name": "scale", 16 + "propertytype": "", 17 + "type": "float", 18 + "value": 1.5 19 + }, 20 + { 21 + "name": "bg", 22 + "propertytype": "", 23 + "type": "color", 24 + "value": "#ffa0b0c0" 25 + }, 26 + { 27 + "name": "fullscreen", 28 + "propertytype": "", 29 + "type": "bool", 30 + "value": true 31 + }, 32 + { 33 + "name": "splashscreen", 34 + "propertytype": "", 35 + "type": "file", 36 + "value": "splashscreen.png" 37 + } 38 + ]
+107
src/test/source_tileset.tsj
··· 1 + { "backgroundcolor":"#ffaaff", 2 + "columns":37, 3 + "image":"tilemap.png", 4 + "imageheight":475, 5 + "imagewidth":628, 6 + "margin":0, 7 + "name":"tiles", 8 + "properties":[ 9 + { 10 + "name":"custom", 11 + "type":"int", 12 + "value":50 13 + }], 14 + "spacing":1, 15 + "tilecount":1131, 16 + "tiledversion":"1.11.2", 17 + "tileheight":16, 18 + "tiles":[ 19 + { 20 + "id":0, 21 + "objectgroup": 22 + { 23 + "draworder":"index", 24 + "id":3, 25 + "name":"", 26 + "objects":[ 27 + { 28 + "height":12, 29 + "id":2, 30 + "name":"", 31 + "rotation":0, 32 + "type":"", 33 + "visible":true, 34 + "width":12, 35 + "x":2, 36 + "y":2 37 + }], 38 + "opacity":1, 39 + "type":"objectgroup", 40 + "visible":true, 41 + "x":0, 42 + "y":0 43 + } 44 + }, 45 + { 46 + "animation":[ 47 + { 48 + "duration":100, 49 + "tileid":0 50 + }, 51 + { 52 + "duration":100, 53 + "tileid":1 54 + }, 55 + { 56 + "duration":100, 57 + "tileid":2 58 + }, 59 + { 60 + "duration":100, 61 + "tileid":3 62 + }], 63 + "id":3 64 + }], 65 + "tilewidth":16, 66 + "transformations": 67 + { 68 + "hflip":true, 69 + "preferuntransformed":true, 70 + "rotate":false, 71 + "vflip":true 72 + }, 73 + "type":"tileset", 74 + "version":"1.10", 75 + "wangsets":[ 76 + { 77 + "colors":[ 78 + { 79 + "color":"#ff0000", 80 + "name":"Dirt", 81 + "probability":1, 82 + "tile":892 83 + }, 84 + { 85 + "color":"#00ff00", 86 + "name":"Grass", 87 + "probability":1, 88 + "tile":888 89 + }], 90 + "name":"Ground", 91 + "tile":1002, 92 + "type":"mixed", 93 + "wangtiles":[ 94 + { 95 + "tileid":888, 96 + "wangid":[2, 2, 2, 2, 2, 2, 2, 2] 97 + }, 98 + { 99 + "tileid":889, 100 + "wangid":[2, 2, 2, 2, 2, 2, 2, 2] 101 + }, 102 + { 103 + "tileid":892, 104 + "wangid":[1, 1, 1, 1, 1, 1, 1, 1] 105 + }] 106 + }] 107 + }
src/test/tile.png

This is a binary file and will not be displayed.

src/test/tilemap.png

This is a binary file and will not be displayed.

+5
src/test/tileset.tsj
··· 1 + { 2 + "source": "./src/test/source_tileset.tsj", 3 + "type":"tileset", 4 + "version":"1.10" 5 + }
+162
src/tileset.zig
··· 1 + pub const Tileset = struct { 2 + columns: u32, 3 + tile_count: u32, 4 + tile_width: u32, 5 + tile_height: u32, 6 + first_gid: u32, 7 + image: ?[]const u8, 8 + 9 + tiles: std.AutoHashMapUnmanaged(u32, Tile), 10 + 11 + pub fn initFromFile(allocator: Allocator, path: []const u8) anyerror!Tileset { 12 + const file = try std.fs.cwd().openFile(path, .{}); 13 + defer file.close(); 14 + 15 + const json = try file.reader().readAllAlloc(allocator, std.math.maxInt(u32)); 16 + defer allocator.free(json); 17 + 18 + return try initFromSlice(allocator, json); 19 + } 20 + 21 + pub fn initFromSlice(allocator: Allocator, json: []const u8) !Tileset { 22 + const parsed_value = try std.json.parseFromSlice(std.json.Value, allocator, json, .{ .ignore_unknown_fields = true }); 23 + defer parsed_value.deinit(); 24 + 25 + const tileset = try std.json.parseFromValue(JsonTileset, allocator, parsed_value.value, .{ .ignore_unknown_fields = true }); 26 + defer tileset.deinit(); 27 + 28 + const json_tileset = tileset.value; 29 + 30 + return fromJson(allocator, json_tileset); 31 + } 32 + 33 + pub fn deinit(self: *Tileset, allocator: Allocator) void { 34 + if (self.image) |image| allocator.free(image); 35 + 36 + self.tiles.deinit(allocator); 37 + } 38 + 39 + pub fn fromJson(allocator: Allocator, tileset_json: JsonTileset) !Tileset { 40 + if (tileset_json.source) |source| { 41 + var tileset = try Tileset.initFromFile(allocator, source); 42 + tileset.first_gid = tileset_json.firstgid orelse 1; 43 + 44 + return tileset; 45 + } 46 + 47 + var tileset: Tileset = .{ 48 + .columns = tileset_json.columns orelse 0, 49 + .tile_count = tileset_json.tilecount orelse 0, 50 + .tile_width = tileset_json.tilewidth orelse 0, 51 + .tile_height = tileset_json.tileheight orelse 0, 52 + .first_gid = tileset_json.firstgid orelse 1, 53 + .image = if (tileset_json.image) |image| try allocator.dupe(u8, image) else null, 54 + .tiles = .empty, 55 + }; 56 + 57 + var tiles_by_id: std.AutoHashMapUnmanaged(u32, Tile.JsonTile) = .empty; 58 + defer tiles_by_id.deinit(allocator); 59 + 60 + if (tileset_json.tiles) |json_tiles| { 61 + for (json_tiles) |json_tile| { 62 + try tiles_by_id.put(allocator, json_tile.id, json_tile); 63 + } 64 + } 65 + 66 + var x: u32 = 0; 67 + var y: u32 = 0; 68 + 69 + var gid = tileset.first_gid - 1; 70 + 71 + const last_gid: u32 = gid + tileset.tile_count - 1; 72 + while (gid <= last_gid) : (gid += 1) { 73 + var tile: Tile = .{ 74 + .tileset = tileset, 75 + .image = tileset.image, 76 + .id = gid, 77 + .x = x * (tileset_json.tilewidth.? + tileset_json.spacing) + tileset_json.margin, 78 + .y = y * (tileset_json.tileheight.? + tileset_json.spacing) + tileset_json.margin, 79 + .width = tileset_json.tilewidth.?, 80 + .height = tileset_json.tileheight.?, 81 + }; 82 + 83 + if (tiles_by_id.get(gid)) |json_tile| { 84 + tile.animation = json_tile.animation; 85 + // tile = Tile.fromJson(json_tile); 86 + } 87 + try tileset.tiles.put(allocator, gid, tile); 88 + if (x >= @as(i64, @intCast(tileset.columns)) - 1) { 89 + y += 1; 90 + x = 0; 91 + } else { 92 + x += 1; 93 + } 94 + } 95 + 96 + return tileset; 97 + } 98 + 99 + pub const JsonTileset = struct { 100 + columns: ?u32 = null, 101 + tilecount: ?u32 = null, 102 + firstgid: ?u32 = 1, 103 + margin: u32 = 0, 104 + source: ?[]const u8 = null, 105 + spacing: u32 = 0, 106 + image: ?[]const u8 = null, 107 + tiles: ?[]Tile.JsonTile = null, 108 + tilewidth: ?u32 = null, 109 + tileheight: ?u32 = null, 110 + }; 111 + }; 112 + 113 + pub const Tile = struct { 114 + id: u32 = 1, 115 + x: u32, 116 + y: u32, 117 + width: u32, 118 + height: u32, 119 + tileset: Tileset, 120 + image: ?[]const u8 = null, 121 + animation: ?[]const Frame = null, 122 + 123 + // pub fn fromJson(json_tile: JsonTile) Tile { 124 + // return .{ 125 + // .id = json_tile.id, 126 + // // .x = if (json_tile.x) |x| x else 0, 127 + // // .y = if (json_tile.y) |y| y else 0, 128 + // // .width = if (json_tile.width) |width| width else 0, 129 + // // .height = if (json_tile.height) |height| height else 0, 130 + // }; 131 + // } 132 + 133 + pub const JsonTile = struct { 134 + id: u32, 135 + x: ?u32 = null, 136 + y: ?u32 = null, 137 + width: ?u32 = null, 138 + height: ?u32 = null, 139 + animation: ?[]const Frame = null, 140 + }; 141 + }; 142 + 143 + pub const Frame = struct { 144 + duration: u32, 145 + tile_id: u32, 146 + 147 + pub fn jsonParseFromValue(allocator: Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() { 148 + return try jsonParser(@This(), allocator, source, options); 149 + } 150 + }; 151 + 152 + inline fn get(value: anytype) @TypeOf(value) { 153 + return if (value) |v| return v else null; 154 + } 155 + 156 + const std = @import("std"); 157 + const Allocator = std.mem.Allocator; 158 + const expectEqualDeep = std.testing.expectEqualDeep; 159 + const expectEqual = std.testing.expectEqual; 160 + const expectEqualStrings = std.testing.expectEqualStrings; 161 + 162 + const jsonParser = @import("tmz.zig").jsonParser;
+141
src/tmz.zig
··· 1 + pub const Property = @import("property.zig").Property; 2 + 3 + pub const Tileset = tileset.Tileset; 4 + pub const Tile = tileset.Tile; 5 + pub const loadTilesetFromSlice = Tileset.initFromSlice; 6 + pub const loadTilesetFromFile = Tileset.initFromFile; 7 + 8 + pub const Color = packed struct(u32) { 9 + a: u8 = 0, 10 + r: u8 = 0, 11 + g: u8 = 0, 12 + b: u8 = 0, 13 + 14 + pub fn jsonParseFromValue(_: std.mem.Allocator, source: Value, _: ParseOptions) !@This() { 15 + return try parseColor(source.string); 16 + } 17 + }; 18 + 19 + // parses a Hex-formatted color (#RRGGBB or #AARRGGBB) string and returns 20 + // a Color. If no alpha channel value specified, defaults to 0 21 + fn parseColor(color_string: []const u8) !Color { 22 + if (color_string.len > 9 or color_string.len < 6) return error.UnexpectedToken; 23 + // buffer for the color_string stripped of '#' 24 + var hex_color: [8]u8 = @splat(0); 25 + // buffer for actual bytes parsed 26 + var color: [4]u8 = @splat(0); 27 + 28 + _ = std.mem.replace(u8, color_string, "#", "", &hex_color); 29 + // Handle strings with no alpha color defined 30 + if (color_string.len < 8) { 31 + // use same buffers for 32-bit color, but just use 6 bytes ascii and 32 + // 3 bytes for actual values 33 + _ = std.fmt.hexToBytes(color[1..4], hex_color[0..6]) catch return error.UnexpectedToken; 34 + } else { 35 + _ = std.fmt.hexToBytes(&color, &hex_color) catch return error.UnexpectedToken; 36 + } 37 + return std.mem.bytesToValue(Color, &color); 38 + } 39 + 40 + test "Color is parsed from string" { 41 + const full_color = "#ccaaffee"; 42 + const expected_full_color: Color = .{ 43 + .a = 0xcc, 44 + .r = 0xaa, 45 + .g = 0xff, 46 + .b = 0xee, 47 + }; 48 + 49 + try std.testing.expectEqual(expected_full_color, try parseColor(full_color)); 50 + 51 + const no_alpha_color = "#bbaadd"; 52 + const expected_no_alpha_color: Color = .{ 53 + .a = 0, 54 + .r = 0xbb, 55 + .g = 0xaa, 56 + .b = 0xdd, 57 + }; 58 + 59 + try std.testing.expectEqual(expected_no_alpha_color, try parseColor(no_alpha_color)); 60 + 61 + const weird_color = "#DeaDBeeF"; 62 + const expected_weird_color: Color = .{ 63 + .a = 0xde, 64 + .r = 0xad, 65 + .g = 0xbe, 66 + .b = 0xef, 67 + }; 68 + 69 + try std.testing.expectEqual(expected_weird_color, try parseColor(weird_color)); 70 + 71 + try std.testing.expectError(error.UnexpectedToken, parseColor("##bbaabbee")); 72 + } 73 + 74 + // converts masheduppropertynames to Zig style snake_case field names and 75 + // ignores data, value and properties so they can be handled later 76 + pub fn jsonParser(T: type, allocator: Allocator, source: Value, options: ParseOptions) !T { 77 + var t: T = undefined; 78 + 79 + inline for (@typeInfo(T).@"struct".fields) |field| { 80 + if (comptime eql(u8, field.name, "data") or eql(u8, field.name, "value") or eql(u8, field.name, "properties")) continue; 81 + 82 + const size = comptime std.mem.replacementSize(u8, field.name, "_", ""); 83 + var tiled_name: [size]u8 = undefined; 84 + _ = std.mem.replace(u8, field.name, "_", "", &tiled_name); 85 + 86 + const source_field = source.object.get(&tiled_name); 87 + if (source_field) |s| { 88 + @field(t, field.name) = try std.json.innerParseFromValue(field.type, allocator, s, options); 89 + } else { 90 + if (field.default_value_ptr) |val| { 91 + @field(t, field.name) = @as(*align(1) const field.type, @ptrCast(val)).*; 92 + } 93 + } 94 + } 95 + 96 + return t; 97 + } 98 + 99 + const TestJson = struct { 100 + property_id: u8 = 0, 101 + data: u8, 102 + value: u8, 103 + 104 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: std.json.ParseOptions) !@This() { 105 + return try jsonParser(@This(), allocator, source, options); 106 + } 107 + }; 108 + 109 + test "jsonParser works" { 110 + const test_json = 111 + \\{ 112 + \\ "propertyid": 9, 113 + \\ "data": 8, 114 + \\ "value": 7 115 + \\} 116 + ; 117 + const parsed_value = try std.json.parseFromSlice(Value, std.testing.allocator, test_json, .{}); 118 + defer parsed_value.deinit(); 119 + 120 + const parsed_json = try std.json.parseFromValue(TestJson, std.testing.allocator, parsed_value.value, .{}); 121 + defer parsed_json.deinit(); 122 + 123 + // Property names are "converted" to snake_case 124 + try std.testing.expectEqual(9, parsed_json.value.property_id); 125 + 126 + // `data` and `value` fields are ignored 127 + try std.testing.expectEqual(0xaa, parsed_json.value.data); 128 + try std.testing.expectEqual(0xaa, parsed_json.value.value); 129 + } 130 + test { 131 + std.testing.refAllDeclsRecursive(@This()); 132 + } 133 + 134 + const std = @import("std"); 135 + const eql = std.mem.eql; 136 + const Allocator = std.mem.Allocator; 137 + const ParseOptions = std.json.ParseOptions; 138 + const Value = std.json.Value; 139 + 140 + const tileset = @import("tileset.zig"); 141 + const layer = @import("layer.zig");