A library for parsing Tiled maps.
0
fork

Configure Feed

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

Update to Zig 0.15.1

- restructures repo to move tests to a separate module, so consumers
don't have to download a bunch of test files
- removes comptime based jsonParser for now

+1602 -1585
+2 -4
.envrc
··· 1 - if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then 2 - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" 1 + if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then 2 + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM=" 3 3 fi 4 4 5 5 use flake 6 - 7 - nix_direnv_manual_reload
+8 -8
.github/workflows/main.yml
··· 15 15 os: [ubuntu-latest, macos-latest] 16 16 include: 17 17 - os: ubuntu-latest 18 - target: x86_64-linux-musl 18 + target: x86_64-linux 19 19 - os: macos-latest 20 20 target: aarch64-macos 21 21 runs-on: ${{ matrix.os }} ··· 33 33 34 34 - run: nix develop -c zig env 35 35 36 - - name: Run tests 36 + - name: Run tests 37 37 run: nix develop -c zig build test -Dtarget=${{ matrix.target }} --summary all 38 38 39 39 build-windows: ··· 42 42 - name: Checkout code 43 43 uses: actions/checkout@v4 44 44 45 - - uses: mlugg/setup-zig@v1 45 + - uses: mlugg/setup-zig@v2 46 46 with: 47 - version: "0.14.0" 47 + version: "0.15.1" 48 48 49 49 - run: zig env 50 50 51 - - name: Run tests 52 - run: zig build test -Dtarget=x86_64-windows-gnu --summary all 51 + - name: Run tests 52 + run: zig build test --summary all 53 53 54 54 coverage: 55 55 runs-on: ubuntu-latest ··· 68 68 - run: nix develop -c zig env 69 69 70 70 - name: Run tests with coverage 71 - run: nix develop -c zig build test -Dcoverage -Dtarget=x86_64-linux-musl --summary all 71 + run: nix develop -c zig build test -Dcoverage -Dtarget=x86_64-linux --summary all 72 72 73 73 - name: Upload to Codecov 74 74 uses: codecov/codecov-action@v5 75 75 with: 76 - directory: zig-out/coverage/kcov-merged 76 + directory: zig-out/coverage 77 77 fail_ci_if_error: true 78 78 verbose: true
+4 -7
README.md
··· 29 29 1. Add `tmz` as a dependency in your `build.zig.zon`: 30 30 31 31 ```bash 32 - zig fetch --save git+https://github.com/coat/tmz.zig#main 32 + zig fetch --save git+https://github.com/coat/tmz.git 33 33 ``` 34 34 35 35 2. Add module to `build.zig`: ··· 55 55 std.debug.info("Player position: {d},{d}\n", .{ player.x, player.y }); 56 56 } 57 57 58 - const ground_layer = map.getLayer("ground"); 58 + const ground_layer = map.layers.get("ground"); 59 59 if (ground_layer) |layer| { 60 60 for (layer.content.data.items) |gid| { 61 - if (gid == 0) continue; 62 61 const tile = map.getTile(gid); 63 62 if (tile) |t| { 64 - drawTile(tile.image, tile.x, tile.y); 63 + drawTile(tile.image, tile.x, tile.y, tile.orientation); 65 64 } 66 65 } 67 66 } ··· 89 88 ## Building 90 89 91 90 Building the library requires [Zig 92 - 0.14.0](https://ziglang.org/download/#release-0.14.0). To build and run the 93 - examples (TODO), [SDL 3.2](https://github.com/libsdl-org/SDL/releases/latest) 94 - is also required. 91 + 0.15.1](https://ziglang.org/download/#release-0.15.1). 95 92 96 93 `zig build install` will build the full library and output a FHS-compatible 97 94 directory in zig-out. You can customize the output directory with the --prefix
+36 -25
build.zig
··· 1 - const std = @import("std"); 2 - 3 - // Although this function looks imperative, note that its job is to 4 - // declaratively construct a build graph that will be executed by an external 5 - // runner. 6 1 pub fn build(b: *std.Build) void { 7 2 const target = b.standardTargetOptions(.{}); 8 3 const optimize = b.standardOptimizeOption(.{}); 9 4 10 5 const coverage = b.option(bool, "coverage", "Generate a coverage report with kcov") orelse false; 11 6 12 - const lib_mod = b.createModule(.{ 13 - .root_source_file = b.path("src/tmz.zig"), 7 + const mod = b.addModule("tmz", .{ 8 + .root_source_file = b.path("src/root.zig"), 14 9 .target = target, 15 10 .optimize = optimize, 16 11 }); 17 12 18 - const lib = b.addLibrary(.{ 19 - .linkage = .static, 20 - .name = "tmz", 21 - .root_module = lib_mod, 13 + const mod_tests = b.addTest(.{ 14 + .root_module = mod, 15 + .use_llvm = coverage, 22 16 }); 17 + const run_mod_tests = b.addRunArtifact(mod_tests); 23 18 24 - b.installArtifact(lib); 25 - 26 - const unit_tests = b.addTest(.{ 27 - .root_module = lib_mod, 19 + const tests = b.addTest(.{ 20 + .root_module = b.createModule(.{ 21 + .root_source_file = b.path("test/tests.zig"), 22 + .target = target, 23 + .optimize = optimize, 24 + .imports = &.{ 25 + .{ .name = "tmz", .module = mod }, 26 + }, 27 + }), 28 + .use_llvm = coverage, 28 29 }); 29 - 30 - const run_tests = b.addRunArtifact(unit_tests); 30 + const run_tests = b.addRunArtifact(tests); 31 31 32 - const test_step = b.step("test", "Run unit tests"); 32 + const test_step = b.step("test", "Run tests"); 33 + test_step.dependOn(&run_mod_tests.step); 33 34 test_step.dependOn(&run_tests.step); 34 35 35 36 if (coverage) { 37 + var run_test_steps: std.ArrayList(*std.Build.Step.Run) = .empty; 38 + run_test_steps.append(b.allocator, run_mod_tests) catch @panic("OOM"); 39 + run_test_steps.append(b.allocator, run_tests) catch @panic("OOM"); 40 + 36 41 const kcov_bin = b.findProgram(&.{"kcov"}, &.{}) catch "kcov"; 42 + 37 43 const merge_step = std.Build.Step.Run.create(b, "merge coverage"); 38 44 merge_step.addArgs(&.{ kcov_bin, "--merge" }); 39 45 merge_step.rename_step_with_output_arg = false; 40 46 const merged_coverage_output = merge_step.addOutputFileArg("."); 41 47 42 - run_tests.setName(b.fmt("{s} (collect coverage)", .{run_tests.step.name})); 43 - // prepend the kcov exec args 44 - const argv = run_tests.argv.toOwnedSlice(b.allocator) catch @panic("OOM"); 45 - run_tests.addArgs(&.{ kcov_bin, "--collect-only" }); 46 - run_tests.addPrefixedDirectoryArg("--include-pattern=", b.path("src")); 47 - merge_step.addDirectoryArg(run_tests.addOutputFileArg(run_tests.producer.?.name)); 48 - run_tests.argv.appendSlice(b.allocator, argv) catch @panic("OOM"); 48 + for (run_test_steps.items) |step| { 49 + step.setName(b.fmt("{s} (collect coverage)", .{step.step.name})); 50 + 51 + // prepend the kcov exec args 52 + const argv = step.argv.toOwnedSlice(b.allocator) catch @panic("OOM"); 53 + step.addArgs(&.{ kcov_bin, "--collect-only" }); 54 + step.addPrefixedDirectoryArg("--include-pattern=", b.path("src")); 55 + merge_step.addDirectoryArg(step.addOutputFileArg(step.producer.?.name)); 56 + step.argv.appendSlice(b.allocator, argv) catch @panic("OOM"); 57 + } 49 58 50 59 const install_coverage = b.addInstallDirectory(.{ 51 60 .source_dir = merged_coverage_output, ··· 55 64 test_step.dependOn(&install_coverage.step); 56 65 } 57 66 } 67 + 68 + const std = @import("std");
+3 -21
build.zig.zon
··· 1 1 .{ 2 2 .name = .tmz, 3 - 4 - .version = "0.1.0", 5 - 6 - // Together with name, this represents a globally unique package 7 - // identifier. This field is generated by the Zig toolchain when the 8 - // package is first created, and then *never changes*. This allows 9 - // unambiguous detection of one package being an updated version of 10 - // another. 11 - // 12 - // When forking a Zig project, this id should be regenerated (delete the 13 - // field and run `zig build`) if the upstream project is still maintained. 14 - // Otherwise, the fork is *hostile*, attempting to take control over the 15 - // original project's identity. Thus it is recommended to leave the comment 16 - // on the following line intact, so that it shows up in code reviews that 17 - // modify the field. 18 - .fingerprint = 0xcc3a94562c7802a6, // Changing this has security and trust implications 19 - 20 - .minimum_zig_version = "0.14.0", 21 - 3 + .version = "0.2.0", 4 + .fingerprint = 0xcc3a94566dc4ab5d, // Changing this has security and trust implications. 5 + .minimum_zig_version = "0.15.1", 22 6 .dependencies = .{}, 23 - 24 7 .paths = .{ 25 8 "build.zig", 26 9 "build.zig.zon", 27 10 "src", 28 - "LICENSE", 29 11 }, 30 12 }
+8 -82
flake.lock
··· 1 1 { 2 2 "nodes": { 3 - "flake-compat": { 4 - "flake": false, 5 - "locked": { 6 - "lastModified": 1696426674, 7 - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 - "owner": "edolstra", 9 - "repo": "flake-compat", 10 - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 - "type": "github" 12 - }, 13 - "original": { 14 - "owner": "edolstra", 15 - "repo": "flake-compat", 16 - "type": "github" 17 - } 18 - }, 19 - "flake-utils": { 20 - "inputs": { 21 - "systems": "systems" 22 - }, 23 - "locked": { 24 - "lastModified": 1705309234, 25 - "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 26 - "owner": "numtide", 27 - "repo": "flake-utils", 28 - "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 29 - "type": "github" 30 - }, 31 - "original": { 32 - "owner": "numtide", 33 - "repo": "flake-utils", 34 - "type": "github" 35 - } 36 - }, 37 3 "nixpkgs": { 38 4 "locked": { 39 - "lastModified": 1745290202, 40 - "narHash": "sha256-FzIbHoNIbpjPyXBm331LXHYUd1xz+cP6LQYGaW/urYg=", 41 - "owner": "nixos", 42 - "repo": "nixpkgs", 43 - "rev": "47457869d5b12bdd72303d6d2ba4bfcc26fe8531", 44 - "type": "github" 5 + "lastModified": 1759441831, 6 + "narHash": "sha256-1yCvOeEMQX/0WqpVtxtKFI2rlfq9W/bxOTGJtRuQv7g=", 7 + "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", 8 + "type": "tarball", 9 + "url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre870157.7df7ff7d8e00/nixexprs.tar.xz" 45 10 }, 46 11 "original": { 47 - "owner": "nixos", 48 - "repo": "nixpkgs", 49 - "type": "github" 12 + "type": "tarball", 13 + "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz" 50 14 } 51 15 }, 52 16 "root": { 53 17 "inputs": { 54 - "nixpkgs": "nixpkgs", 55 - "zig-overlay": "zig-overlay" 56 - } 57 - }, 58 - "systems": { 59 - "locked": { 60 - "lastModified": 1681028828, 61 - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 62 - "owner": "nix-systems", 63 - "repo": "default", 64 - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 65 - "type": "github" 66 - }, 67 - "original": { 68 - "owner": "nix-systems", 69 - "repo": "default", 70 - "type": "github" 71 - } 72 - }, 73 - "zig-overlay": { 74 - "inputs": { 75 - "flake-compat": "flake-compat", 76 - "flake-utils": "flake-utils", 77 - "nixpkgs": [ 78 - "nixpkgs" 79 - ] 80 - }, 81 - "locked": { 82 - "lastModified": 1744158734, 83 - "narHash": "sha256-cxyivkJnd1HlagmMMIykobGllm/Su1zUtdL4AoMU8h0=", 84 - "owner": "mitchellh", 85 - "repo": "zig-overlay", 86 - "rev": "bb954184eac1e2fc9054281c9276fefc22792c35", 87 - "type": "github" 88 - }, 89 - "original": { 90 - "owner": "mitchellh", 91 - "repo": "zig-overlay", 92 - "type": "github" 18 + "nixpkgs": "nixpkgs" 93 19 } 94 20 } 95 21 },
+14 -20
flake.nix
··· 2 2 description = "A library for parsing Tiled maps"; 3 3 4 4 inputs = { 5 - nixpkgs.url = "github:nixos/nixpkgs"; 6 - 7 - zig-overlay.url = "github:mitchellh/zig-overlay"; 8 - zig-overlay.inputs.nixpkgs.follows = "nixpkgs"; 5 + nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; 9 6 }; 10 7 11 - outputs = { 12 - nixpkgs, 13 - zig-overlay, 14 - ... 15 - }: 8 + outputs = {nixpkgs, ...}: let 9 + systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; 10 + in 16 11 builtins.foldl' nixpkgs.lib.recursiveUpdate {} ( 17 12 builtins.map ( 18 13 system: let 19 - pkgs-stable = nixpkgs.legacyPackages.${system}; 20 - zig = zig-overlay.packages.${system}."0.14.0"; 14 + pkgs = nixpkgs.legacyPackages.${system}; 21 15 in { 22 - devShells.${system}.default = pkgs-stable.mkShell { 23 - nativeBuildInputs = with pkgs-stable; [ 24 - sdl3 25 - sdl3-image 26 - zig 27 - ] 28 - ++ (pkgs.lib.optionals pkgs.stdenv.isLinux [kcov]); 16 + devShells.${system}.default = pkgs.mkShell { 17 + packages = with pkgs; 18 + [ 19 + zig_0_15 20 + ] 21 + ++ (pkgs.lib.optionals pkgs.stdenv.isLinux [kcov]); 29 22 }; 30 23 31 - formatter.${system} = pkgs-stable.alejandra; 24 + formatter.${system} = pkgs.alejandra; 32 25 } 33 - ) (builtins.attrNames zig-overlay.packages) 26 + ) 27 + systems 34 28 ); 35 29 }
+196
src/Map.zig
··· 1 + height: u32, 2 + width: u32, 3 + tile_width: u32, 4 + tile_height: u32, 5 + infinite: bool, 6 + orientation: Orientation, 7 + 8 + layers: std.StringHashMapUnmanaged(Layer), 9 + tilesets: std.ArrayList(Tileset), 10 + 11 + background_color: ?Color = null, 12 + class: ?[]const u8, 13 + 14 + pub const Orientation = enum { orthogonal, isometric, staggered, hexagonal }; 15 + 16 + pub fn initFromSlice(alloc: Allocator, json: []const u8) !Map { 17 + var arena = std.heap.ArenaAllocator.init(alloc); 18 + defer arena.deinit(); 19 + const arena_allocator = arena.allocator(); 20 + 21 + const parsed_value = try std.json.parseFromSliceLeaky(std.json.Value, arena_allocator, json, .{ .ignore_unknown_fields = true }); 22 + 23 + const json_map = try std.json.parseFromValueLeaky(JsonMap, arena_allocator, parsed_value, .{ .ignore_unknown_fields = true }); 24 + 25 + var map: Map = .{ 26 + .height = json_map.height, 27 + .width = json_map.width, 28 + .tile_width = json_map.tilewidth, 29 + .tile_height = json_map.tileheight, 30 + .infinite = json_map.infinite, 31 + .orientation = json_map.orientation, 32 + 33 + .background_color = if (json_map.backgroundcolor) |color| color else null, 34 + .class = if (json_map.class) |c| try alloc.dupe(u8, c) else null, 35 + .tilesets = .empty, 36 + .layers = .empty, 37 + }; 38 + 39 + if (json_map.tilesets) |json_tilesets| { 40 + for (json_tilesets) |json_tileset| { 41 + const tileset = try Tileset.fromJson(alloc, json_tileset); 42 + try map.tilesets.append(alloc, tileset); 43 + } 44 + } 45 + 46 + if (json_map.layers) |json_layers| { 47 + for (json_layers) |json_layer| { 48 + const layer = try Layer.fromJson(alloc, json_layer); 49 + try map.layers.put(alloc, layer.name, layer); 50 + } 51 + } 52 + 53 + return map; 54 + } 55 + 56 + pub fn initFromFile(allocator: Allocator, path: []const u8) !Map { 57 + const file = try std.fs.cwd().openFile(path, .{}); 58 + defer file.close(); 59 + 60 + var reader: std.fs.File.Reader = .initStreaming(file, &.{}); 61 + 62 + var out: std.Io.Writer.Allocating = .init(allocator); 63 + defer out.deinit(); 64 + 65 + _ = try reader.interface.streamRemaining(&out.writer); 66 + 67 + const json = try out.toOwnedSlice(); 68 + defer allocator.free(json); 69 + 70 + return initFromSlice(allocator, json); 71 + } 72 + 73 + pub fn deinit(self: *Map, allocator: Allocator) void { 74 + if (self.class) |class| allocator.free(class); 75 + 76 + var layers_it = self.layers.valueIterator(); 77 + while (layers_it.next()) |value_ptr| { 78 + value_ptr.*.deinit(allocator); 79 + } 80 + self.layers.deinit(allocator); 81 + 82 + for (self.tilesets.items) |*tileset| { 83 + tileset.deinit(allocator); 84 + } 85 + self.tilesets.deinit(allocator); 86 + 87 + // var properties_it = self.properties.valueIterator(); 88 + // while (properties_it.next()) |value_ptr| { 89 + // value_ptr.*.deinit(allocator); 90 + // } 91 + // self.properties.deinit(allocator); 92 + } 93 + 94 + pub fn getTile(self: Map, gid: u32) ?Tile { 95 + if (gid == 0) return null; 96 + 97 + var i = self.tilesets.items.len - 1; 98 + while (i >= 0) : (i -= 1) { 99 + const tileset = self.tilesets.items[i]; 100 + if (tileset.first_gid <= gid) { 101 + return tileset.tiles.get(gid - tileset.first_gid); 102 + } 103 + } 104 + return null; 105 + } 106 + 107 + /// Finds first object 108 + pub fn findObjectByClass(self: Map, class: []const u8) ?Object { 109 + var layer_it = self.layers.valueIterator(); 110 + while (layer_it.next()) |layer| { 111 + if (layer.content == .object_group) { 112 + if (layer.content.object_group.getByClass(class)) |object| { 113 + return object; 114 + } 115 + } 116 + } 117 + return null; 118 + } 119 + 120 + /// Finds first object by name 121 + pub fn findObject(self: Map, name: []const u8) ?Object { 122 + var layer_it = self.layers.valueIterator(); 123 + while (layer_it.next()) |layer| { 124 + if (layer.content == .object_group) { 125 + if (layer.content.object_group.get(name)) |object| { 126 + return object; 127 + } 128 + } 129 + } 130 + return null; 131 + } 132 + 133 + pub fn pixelWidth(self: Map) u32 { 134 + return self.width * self.tile_width; 135 + } 136 + 137 + pub fn pixelHeight(self: Map) u32 { 138 + return self.height * self.tile_height; 139 + } 140 + 141 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#map 142 + const JsonMap = struct { 143 + /// Hex-formatted color (#RRGGBB or #AARRGGBB) 144 + backgroundcolor: ?tmz.Color = null, 145 + class: ?[]const u8 = null, 146 + /// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) 147 + // TODO: actually support this? 148 + compression_level: i32 = -1, 149 + /// Number of tile rows 150 + height: u32, 151 + /// Length of the side of a hex tile in pixels (hexagonal maps only) 152 + hexsidelength: ?u32 = null, 153 + infinite: bool, 154 + layers: ?[]Layer.JsonLayer = null, 155 + /// Auto-increments for each layer 156 + nextlayerid: u32, 157 + /// Auto-increments for each placed object 158 + // next_object_id: u32, 159 + orientation: Orientation, 160 + parallaxoriginx: ?f32 = 0, 161 + parallaxoriginy: ?f32 = 0, 162 + properties: ?[]Property = null, 163 + /// currently only supported for orthogonal maps 164 + renderorder: ?RenderOrder = null, 165 + /// staggered / hexagonal maps only 166 + staggeraxis: ?StaggerAxis = null, 167 + /// staggered / hexagonal maps only 168 + staggerindex: ?StaggerIndex = null, 169 + tiledversion: []const u8, 170 + /// Map grid height 171 + tileheight: u32, 172 + tilesets: ?[]Tileset.JsonTileset = null, 173 + /// Map grid width 174 + tilewidth: u32, 175 + version: []const u8, 176 + /// Number of tile columns 177 + width: u32, 178 + 179 + pub const RenderOrder = enum { @"right-down", @"right-up", @"left-down", @"left-up" }; 180 + pub const StaggerAxis = enum { x, y }; 181 + pub const StaggerIndex = enum { odd, even }; 182 + }; 183 + 184 + const Map = @This(); 185 + 186 + const tmz = @import("root.zig"); 187 + const Layer = tmz.Layer; 188 + const Object = tmz.Object; 189 + const Color = tmz.Color; 190 + const Tileset = tmz.Tileset; 191 + const Tile = tmz.Tile; 192 + const Property = tmz.Property; 193 + 194 + const std = @import("std"); 195 + const innerParseFromValue = std.json.innerParseFromValue; 196 + const Allocator = std.mem.Allocator;
+71
src/Property.zig
··· 1 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#property 2 + name: []const u8, 3 + property_type: ?[]const u8, 4 + type: Type = .string, 5 + value: Value, 6 + 7 + const Type = enum { string, int, float, bool, color, file }; 8 + const Value = union(enum) { 9 + string: []const u8, 10 + int: u32, 11 + float: f32, 12 + bool: bool, 13 + color: Color, 14 + file: []const u8, 15 + }; 16 + 17 + pub fn fromJson(allocator: Allocator, property: Property) !Property { 18 + const value: Value = switch (property.value) { 19 + .string => .{ .string = try allocator.dupe(u8, property.value.string) }, 20 + .file => .{ .file = try allocator.dupe(u8, property.value.file) }, 21 + else => property.value, 22 + }; 23 + 24 + return .{ 25 + .name = try allocator.dupe(u8, property.name), 26 + .property_type = if (property.property_type) |pt| try allocator.dupe(u8, pt) else null, 27 + .type = property.type, 28 + .value = value, 29 + }; 30 + } 31 + 32 + pub fn deinit(self: *Property, allocator: Allocator) void { 33 + allocator.free(self.name); 34 + if (self.property_type) |pt| allocator.free(pt); 35 + switch (self.type) { 36 + .string => allocator.free(self.value.string), 37 + .file => allocator.free(self.value.file), 38 + else => {}, 39 + } 40 + } 41 + 42 + pub fn jsonParseFromValue(allocator: Allocator, source: std.json.Value, options: ParseOptions) !Property { 43 + const property_type = try innerParseFromValue(Type, allocator, source.object.get("type").?, options); 44 + 45 + const value = source.object.get("value") orelse return error.UnexpectedToken; 46 + return .{ 47 + .name = try innerParseFromValue([]const u8, allocator, source.object.get("name").?, options), 48 + .property_type = if (source.object.get("propertytype")) |s| try innerParseFromValue([]const u8, allocator, s, options) else null, 49 + 50 + .type = property_type, 51 + 52 + .value = switch (property_type) { 53 + .string => .{ .string = try innerParseFromValue([]const u8, allocator, value, options) }, 54 + .int => .{ .int = try innerParseFromValue(u32, allocator, value, options) }, 55 + .float => .{ .float = try innerParseFromValue(f32, allocator, value, options) }, 56 + .bool => .{ .bool = try innerParseFromValue(bool, allocator, value, options) }, 57 + .color => .{ .color = try innerParseFromValue(Color, allocator, value, options) }, 58 + .file => .{ .file = try innerParseFromValue([]const u8, allocator, value, options) }, 59 + }, 60 + }; 61 + } 62 + 63 + const Property = @This(); 64 + 65 + const std = @import("std"); 66 + const ParseOptions = std.json.ParseOptions; 67 + const innerParseFromValue = std.json.innerParseFromValue; 68 + const Allocator = std.mem.Allocator; 69 + 70 + const tmz = @import("root.zig"); 71 + const Color = tmz.Color;
+244 -198
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 - const object = try Object.fromJson(allocator, json_object); 30 - try object_group.objects.append(allocator, object); 31 - } 32 - } 33 - 34 - return object_group; 35 - } 36 - 37 - pub fn deinit(self: *ObjectGroup, allocator: Allocator) void { 38 - for (self.objects.items) |*object| { 39 - object.deinit(allocator); 40 - } 41 - self.objects.deinit(allocator); 42 - } 43 - 44 - pub fn getByClass(self: ObjectGroup, class: []const u8) ?Object { 45 - for (self.objects.items) |object| { 46 - if (object.class) |object_class| { 47 - if (std.mem.eql(u8, object_class, class)) { 48 - return object; 49 - } 50 - } 51 - } 52 - return null; 53 - } 54 - 55 - pub fn get(self: ObjectGroup, name: []const u8) ?Object { 56 - for (self.objects.items) |object| { 57 - if (std.mem.eql(u8, object.name, name)) { 58 - return object; 59 - } 60 - } 61 - return null; 62 - } 63 - }; 64 - pub const ImageLayer = struct {}; 65 - pub const Group = struct {}; 66 - 67 1 pub const Layer = struct { 68 2 id: u32, 69 - class: ?[]const u8 = null, 3 + name: []const u8, 70 4 content: LayerContent, 71 5 visible: bool, 6 + class: ?[]const u8 = null, 72 7 73 8 pub fn fromJson(allocator: Allocator, json_layer: JsonLayer) !Layer { 74 9 return .{ 75 10 .class = if (json_layer.class) |class| try allocator.dupe(u8, class) else null, 76 11 .id = json_layer.id, 12 + .name = try allocator.dupe(u8, json_layer.name), 77 13 .visible = json_layer.visible, 78 14 .content = try LayerContent.fromJson(allocator, json_layer), 79 15 }; ··· 81 17 82 18 pub fn deinit(self: *Layer, allocator: Allocator) void { 83 19 if (self.class) |class| allocator.free(class); 20 + allocator.free(self.name); 84 21 self.content.deinit(allocator); 85 22 } 86 23 87 24 /// https://doc.mapeditor.org/en/stable/reference/jsonk-map-format/#layer 88 25 pub const JsonLayer = struct { 89 26 /// `tilelayer` only. 90 - chunks: ?[]Chunk = null, 27 + chunks: ?[]Chunk.JsonChunk = null, 91 28 class: ?[]const u8 = null, 92 29 /// `tilelayer` only. 93 30 compression: ?Compression = null, ··· 113 50 /// `objectgroup` only. 114 51 objects: ?[]Object.JsonObject = null, 115 52 /// Horizontal layer offset in pixels 116 - offset_x: f32 = 0, 53 + offsetx: f32 = 0, 117 54 /// Vertical layer offset in pixels 118 - offset_y: f32 = 0, 55 + offsety: f32 = 0, 119 56 opacity: f32, 120 - parallax_x: f32 = 1, 121 - parallax_y: f32 = 1, 122 - properties: ?std.StringHashMapUnmanaged(Property) = null, 57 + parallaxx: f32 = 1, 58 + parallaxy: f32 = 1, 59 + properties: ?[]Property = null, 123 60 /// `imagelayer` only 124 - repeat_x: ?bool = null, 61 + repeatx: ?bool = null, 125 62 /// `imagelayer` only 126 - repeat_y: ?bool = null, 63 + repeaty: ?bool = null, 127 64 /// X coordinate where layer content starts (for infinite maps) 128 - start_x: ?i32 = null, 65 + startx: ?i32 = null, 129 66 /// Y coordinate where layer content starts (for infinite maps) 130 - start_y: ?i32 = null, 67 + starty: ?i32 = null, 131 68 /// Hex-formatted tint color (#RRGGBB or #AARRGGBB) that is multiplied with any graphics drawn by this layer or any child layers 132 - tint_color: ?[]const u8 = null, 69 + tintcolor: ?Color = null, 133 70 /// `imagelayer` only 134 - transparent_color: ?[]const u8 = null, 71 + transparentcolor: ?Color = null, 135 72 type: Type, 136 73 visible: bool, 137 74 /// Column count. Same as map width for fixed-size maps. ··· 146 83 pub const Encoding = enum { csv, base64 }; 147 84 pub const Type = enum { tilelayer, objectgroup, imagelayer, group }; 148 85 149 - pub const Compression = enum { 150 - none, 151 - zlib, 152 - gzip, 153 - zstd, 86 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !JsonLayer { 87 + var layer: JsonLayer = .{ 88 + .class = if (source.object.get("class")) |class| try innerParseFromValue([]const u8, allocator, class, options) else null, 89 + .compression = if (source.object.get("compression")) |compression| try innerParseFromValue(Compression, allocator, compression, options) else null, 90 + .chunks = if (source.object.get("chunks")) |chunks| try innerParseFromValue([]Chunk.JsonChunk, allocator, chunks, options) else null, 91 + .layer_data = if (source.object.get("layerdata")) |layerdata| try innerParseFromValue([]u32, allocator, layerdata, options) else null, 92 + .draw_order = if (source.object.get("draworder")) |draworder| try innerParseFromValue(DrawOrder, allocator, draworder, options) else .topdown, 93 + .encoding = if (source.object.get("encoding")) |encoding| try innerParseFromValue(Encoding, allocator, encoding, options) else .csv, 94 + .height = if (source.object.get("height")) |height| try innerParseFromValue(u32, allocator, height, options) else null, 154 95 155 - pub fn jsonParseFromValue(_: Allocator, source: Value, _: ParseOptions) !@This() { 156 - return switch (source) { 157 - .string, .number_string => |value| cmp: { 158 - if (value.len == 0) { 159 - break :cmp .none; 160 - } else { 161 - break :cmp std.meta.stringToEnum(Compression, value) orelse .none; 162 - } 163 - }, 164 - else => .none, 165 - }; 166 - } 167 - }; 168 - 169 - pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 170 - var layer = try jsonParser(@This(), allocator, source, options); 171 - 172 - if (source.object.get("properties")) |props| { 173 - const properties = try std.json.innerParseFromValue([]Property, allocator, props, .{}); 174 - layer.properties = std.StringHashMapUnmanaged(Property).empty; 175 - for (properties) |property| { 176 - try layer.properties.?.put(allocator, property.name, property); 177 - } 178 - } 96 + .id = try innerParseFromValue(u32, allocator, source.object.get("id").?, options), 97 + .image = if (source.object.get("image")) |image| try innerParseFromValue([]const u8, allocator, image, options) else null, 98 + .layers = if (source.object.get("layers")) |layers| try innerParseFromValue([]JsonLayer, allocator, layers, options) else null, 99 + .locked = if (source.object.get("locked")) |locked| try innerParseFromValue(bool, allocator, locked, options) else false, 100 + .name = try innerParseFromValue([]const u8, allocator, source.object.get("name").?, options), 101 + .objects = if (source.object.get("objects")) |objects| try innerParseFromValue([]Object.JsonObject, allocator, objects, options) else null, 102 + .offsetx = if (source.object.get("offsetx")) |offsetx| try innerParseFromValue(f32, allocator, offsetx, options) else 0, 103 + .offsety = if (source.object.get("offsety")) |offsety| try innerParseFromValue(f32, allocator, offsety, options) else 0, 104 + .opacity = try innerParseFromValue(f32, allocator, source.object.get("opacity").?, options), 105 + .parallaxx = if (source.object.get("parallaxx")) |parallaxx| try innerParseFromValue(f32, allocator, parallaxx, options) else 1, 106 + .parallaxy = if (source.object.get("parallaxy")) |parallaxy| try innerParseFromValue(f32, allocator, parallaxy, options) else 1, 107 + .properties = if (source.object.get("properties")) |properties| try innerParseFromValue([]Property, allocator, properties, options) else null, 108 + .repeatx = if (source.object.get("repeatx")) |repeatx| try innerParseFromValue(bool, allocator, repeatx, options) else null, 109 + .startx = if (source.object.get("startx")) |startx| try innerParseFromValue(i32, allocator, startx, options) else null, 110 + .starty = if (source.object.get("starty")) |starty| try innerParseFromValue(i32, allocator, starty, options) else null, 111 + .tintcolor = if (source.object.get("tintcolor")) |tintcolor| try innerParseFromValue(Color, allocator, tintcolor, options) else null, 112 + .transparentcolor = if (source.object.get("transparentcolor")) |transparentcolor| try innerParseFromValue(Color, allocator, transparentcolor, options) else null, 113 + .type = try innerParseFromValue(Type, allocator, source.object.get("type").?, options), 114 + .visible = try innerParseFromValue(bool, allocator, source.object.get("visible").?, options), 115 + .width = if (source.object.get("width")) |width| try innerParseFromValue(u32, allocator, width, options) else null, 116 + .x = if (source.object.get("x")) |x| try innerParseFromValue(i32, allocator, x, options) else 0, 117 + .y = if (source.object.get("y")) |y| try innerParseFromValue(i32, allocator, y, options) else 0, 118 + }; 179 119 180 120 if (layer.type == .tilelayer) { 181 - if (layer.chunks) |chunks| { 182 - for (chunks) |*chunk| { 183 - const chunk_size: usize = chunk.width * chunk.height; 184 - if (layer.encoding == .base64) { 185 - chunk.data = .{ .csv = parseBase64Data(allocator, chunk.data.base64, chunk_size, layer.compression orelse .none) }; 186 - } 187 - } 188 - } 189 121 if (source.object.get("data")) |data| { 190 122 if (layer.encoding == .csv) { 191 - layer.layer_data = try std.json.parseFromValueLeaky([]u32, allocator, data, options); 123 + layer.layer_data = try innerParseFromValue([]u32, allocator, data, options); 192 124 } else { 193 - const base64_data = try std.json.parseFromValueLeaky([]const u8, allocator, data, options); 125 + const base64_data = try innerParseFromValue([]const u8, allocator, data, options); 194 126 const layer_size: usize = (layer.width orelse 0) * (layer.height orelse 0); 195 127 196 128 layer.layer_data = parseBase64Data(allocator, base64_data, layer_size, layer.compression orelse .none); 197 129 } 198 130 } 199 131 } 132 + 200 133 return layer; 201 134 } 202 135 }; 203 136 }; 204 137 138 + pub const Compression = enum { 139 + none, 140 + zlib, 141 + gzip, 142 + zstd, 143 + 144 + pub fn jsonParseFromValue(_: Allocator, source: Value, _: ParseOptions) !@This() { 145 + return switch (source) { 146 + .string, .number_string => |value| cmp: { 147 + if (value.len == 0) { 148 + break :cmp .none; 149 + } else { 150 + break :cmp std.meta.stringToEnum(Compression, value) orelse .none; 151 + } 152 + }, 153 + else => .none, 154 + }; 155 + } 156 + }; 157 + 158 + pub const TileLayer = struct { 159 + data: std.ArrayList(u32), 160 + chunks: ?[]Chunk = null, 161 + 162 + pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !TileLayer { 163 + var layer: TileLayer = .{ 164 + .data = .empty, 165 + }; 166 + if (json_layer.layer_data) |json_data| { 167 + try layer.data.insertSlice(allocator, 0, json_data); 168 + } 169 + if (json_layer.chunks) |json_chunks| { 170 + layer.chunks = try allocator.alloc(Chunk, json_chunks.len); 171 + for (json_chunks, 0..) |json_chunk, i| { 172 + layer.chunks.?[i] = try Chunk.fromJson(allocator, json_chunk, json_layer.compression orelse .none); 173 + } 174 + } 175 + return layer; 176 + } 177 + 178 + pub fn deinit(self: *TileLayer, allocator: Allocator) void { 179 + if (self.chunks) |chunks| { 180 + for (chunks) |*chunk| { 181 + chunk.deinit(allocator); 182 + } 183 + allocator.free(chunks); 184 + } 185 + self.data.deinit(allocator); 186 + } 187 + }; 188 + 189 + pub const ObjectGroup = struct { 190 + objects: std.ArrayListUnmanaged(Object), 191 + 192 + pub fn fromJson(allocator: Allocator, json_layer: Layer.JsonLayer) !ObjectGroup { 193 + var object_group: ObjectGroup = .{ 194 + .objects = .empty, 195 + }; 196 + if (json_layer.objects) |json_objects| { 197 + for (json_objects) |json_object| { 198 + const object = try Object.fromJson(allocator, json_object); 199 + try object_group.objects.append(allocator, object); 200 + } 201 + } 202 + 203 + return object_group; 204 + } 205 + 206 + pub fn deinit(self: *ObjectGroup, allocator: Allocator) void { 207 + for (self.objects.items) |*object| { 208 + object.deinit(allocator); 209 + } 210 + self.objects.deinit(allocator); 211 + } 212 + 213 + pub fn getByClass(self: ObjectGroup, class: []const u8) ?Object { 214 + for (self.objects.items) |object| { 215 + if (object.class) |object_class| { 216 + if (std.mem.eql(u8, object_class, class)) { 217 + return object; 218 + } 219 + } 220 + } 221 + return null; 222 + } 223 + 224 + pub fn get(self: ObjectGroup, name: []const u8) ?Object { 225 + for (self.objects.items) |object| { 226 + if (std.mem.eql(u8, object.name, name)) { 227 + return object; 228 + } 229 + } 230 + return null; 231 + } 232 + }; 233 + pub const ImageLayer = struct {}; 234 + pub const Group = struct {}; 235 + 205 236 pub const LayerContent = union(enum) { 206 237 tile_layer: TileLayer, 207 238 object_group: ObjectGroup, ··· 234 265 235 266 /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk 236 267 pub const Chunk = struct { 237 - /// Array of unsigned int (GIDs) or base64-encoded data 238 - data: EncodedData, 268 + data: []u32, 239 269 height: u32, 240 270 width: u32, 241 271 x: u32, 242 272 y: u32, 243 273 244 - const EncodedData = union(Layer.JsonLayer.Encoding) { 245 - csv: []const u32, 246 - base64: []const u8, 247 - }; 274 + /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk 275 + pub const JsonChunk = struct { 276 + /// Array of unsigned int (GIDs) or base64-encoded data 277 + data: EncodedData, 278 + height: u32, 279 + width: u32, 280 + x: u32, 281 + y: u32, 248 282 249 - pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 250 - var chunk = try jsonParser(@This(), allocator, source, options); 283 + const EncodedData = union(Layer.JsonLayer.Encoding) { 284 + csv: []u32, 285 + base64: []const u8, 286 + }; 251 287 252 - if (source.object.get("data")) |data| { 253 - if (data == .array) { 254 - chunk.data = .{ .csv = try std.json.parseFromValueLeaky([]const u32, allocator, data, options) }; 288 + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 289 + var chunk: JsonChunk = .{ 290 + .height = try innerParseFromValue(u32, allocator, source.object.get("height").?, options), 291 + .width = try innerParseFromValue(u32, allocator, source.object.get("width").?, options), 292 + .x = try innerParseFromValue(u32, allocator, source.object.get("x").?, options), 293 + .y = try innerParseFromValue(u32, allocator, source.object.get("y").?, options), 294 + .data = undefined, 295 + }; 296 + 297 + if (source.object.get("data")) |data| { 298 + if (data == .array) { 299 + chunk.data = .{ .csv = try innerParseFromValue([]u32, allocator, data, options) }; 300 + } 301 + if (data == .string) { 302 + chunk.data = .{ .base64 = try innerParseFromValue([]const u8, allocator, data, options) }; 303 + } 255 304 } 256 - if (data == .string) { 257 - chunk.data = .{ .base64 = try std.json.parseFromValueLeaky([]const u8, allocator, data, options) }; 258 - } 305 + return chunk; 259 306 } 260 - return chunk; 307 + }; 308 + 309 + pub fn fromJson(allocator: Allocator, json_chunk: JsonChunk, compression: Compression) !Chunk { 310 + var arena_state = std.heap.ArenaAllocator.init(allocator); 311 + defer arena_state.deinit(); 312 + const arena = arena_state.allocator(); 313 + 314 + const data: []u32 = try allocator.dupe( 315 + u32, 316 + set_data: { 317 + switch (json_chunk.data) { 318 + .csv => break :set_data json_chunk.data.csv, 319 + .base64 => { 320 + const base64_data = parseBase64Data(arena, json_chunk.data.base64, json_chunk.width * json_chunk.height, compression); 321 + 322 + break :set_data base64_data; 323 + }, 324 + } 325 + }, 326 + ); 327 + 328 + return .{ 329 + .x = json_chunk.x, 330 + .y = json_chunk.y, 331 + .width = json_chunk.width, 332 + .height = json_chunk.height, 333 + 334 + .data = data, 335 + }; 336 + } 337 + 338 + pub fn deinit(self: *Chunk, allocator: Allocator) void { 339 + allocator.free(self.data); 261 340 } 262 341 }; 263 342 ··· 361 440 362 441 /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#text 363 442 pub const Text = struct { 364 - bold: bool, 365 - color: []const u8, 366 - font_family: []const u8 = "sans-serif", 367 - h_align: enum { center, right, justify, left } = .left, 443 + bold: ?bool = null, 444 + color: ?[]const u8 = null, 445 + fontfamily: []const u8 = "sans-serif", 446 + halign: enum { center, right, justify, left } = .left, 368 447 italic: bool = false, 369 448 kerning: bool = true, 370 - pixel_size: usize = 16, 449 + pixelsize: u32 = 16, 371 450 strikeout: bool = false, 372 451 text: []const u8, 373 452 underline: bool = false, 374 - v_align: enum { center, bottom, top } = .top, 453 + valign: enum { center, bottom, top } = .top, 375 454 wrap: bool = false, 376 - 377 - pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { 378 - return try jsonParser(@This(), allocator, source, options); 379 - } 380 455 }; 381 456 382 457 // Decode base64 data (and optionally decompress) into a slice of u32 Global Tile Ids allocated on the heap, caller owns slice 383 - fn parseBase64Data(allocator: Allocator, base64_data: []const u8, size: usize, compression: Layer.JsonLayer.Compression) []u32 { 458 + fn parseBase64Data(allocator: Allocator, base64_data: []const u8, size: usize, compression: Compression) []u32 { 459 + // var arena = std.heap.ArenaAllocator.init(allocator); 460 + // defer arena.deinit(); 461 + // const arena_allocator = arena.allocator(); 462 + 384 463 const decoded_size = base64_decoder.calcSizeForSlice(base64_data) catch @panic("Unable to decode base64 data"); 385 464 var decoded = allocator.alloc(u8, decoded_size) catch @panic("OOM"); 386 465 defer allocator.free(decoded); ··· 392 471 const alignment = @alignOf(u32); 393 472 394 473 if (compression != .none) 395 - decoded = decompress(allocator, decoded, size, compression); 474 + decoded = decompress(allocator, decoded, compression); 396 475 397 476 if (size * alignment != decoded.len) 398 477 @panic("data size does not match Layer dimensions"); ··· 407 486 } 408 487 409 488 // caller owns returned slice 410 - fn decompress(allocator: Allocator, compressed: []const u8, size: usize, compression: Layer.JsonLayer.Compression) []u8 { 411 - const decompressed = allocator.alloc(u8, size * @alignOf(u32)) catch @panic("OOM"); 412 - var decompressed_buf = std.io.fixedBufferStream(decompressed); 413 - var compressed_buf = std.io.fixedBufferStream(compressed); 489 + fn decompress(allocator: Allocator, compressed: []const u8, compression: Compression) []u8 { 490 + var out: std.Io.Writer.Allocating = .init(allocator); 491 + defer out.deinit(); 492 + 493 + var compressed_reader: std.Io.Reader = .fixed(compressed); 414 494 415 495 return switch (compression) { 416 496 .gzip => { 417 - std.compress.gzip.decompress(compressed_buf.reader(), decompressed_buf.writer()) catch @panic("Unable to decompress gzip"); 418 - return decompressed; 497 + var decompresser: std.compress.flate.Decompress = .init(&compressed_reader, .gzip, &.{}); 498 + _ = decompresser.reader.streamRemaining(&out.writer) catch @panic("Unable to decompress gzip"); 499 + 500 + return out.toOwnedSlice() catch @panic("OOM"); 419 501 }, 420 502 .zlib => { 421 - std.compress.zlib.decompress(compressed_buf.reader(), decompressed_buf.writer()) catch @panic("Unable to decompress zlib"); 422 - return decompressed; 503 + var decompresser: std.compress.flate.Decompress = .init(&compressed_reader, .zlib, &.{}); 504 + _ = decompresser.reader.streamRemaining(&out.writer) catch @panic("Unable to decompress zlib"); 505 + 506 + return out.toOwnedSlice() catch @panic("OOM"); 423 507 }, 424 508 .zstd => { 425 - const window_buffer = allocator.alloc(u8, std.compress.zstd.DecompressorOptions.default_window_buffer_len) catch @panic("OOM"); 426 - defer allocator.free(window_buffer); 509 + var decompresser: std.compress.zstd.Decompress = .init(&compressed_reader, &.{}, .{}); 427 510 428 - var zstd_stream = std.compress.zstd.decompressor(compressed_buf.reader(), .{ .window_buffer = window_buffer }); 429 - _ = zstd_stream.reader().readAll(decompressed) catch @panic("Unable to decompress zstd"); 511 + _ = decompresser.reader.streamRemaining(&out.writer) catch @panic("Unable to decompress zstd"); 430 512 431 - return decompressed; 513 + return out.toOwnedSlice() catch @panic("OOM"); 432 514 }, 433 - .none => return allocator.dupe(u8, compressed) catch @panic("OOM"), 515 + .none => return @constCast(compressed), 434 516 }; 435 517 } 436 518 437 - test "Layer" { 438 - const allocator = std.testing.allocator; 439 - 440 - const json = @embedFile("test/object_layer.json"); 441 - 442 - const parsed_layer = try std.json.parseFromSlice(Value, allocator, json, .{ .ignore_unknown_fields = true }); 443 - defer parsed_layer.deinit(); 444 - const managed_layer = try std.json.parseFromValue(Layer.JsonLayer, allocator, parsed_layer.value, .{ .ignore_unknown_fields = true }); 445 - defer managed_layer.deinit(); 446 - const json_layer = managed_layer.value; 447 - 448 - const properties = json_layer.properties.?; 449 - var iterator = properties.iterator(); 450 - while (iterator.next()) |entry| { 451 - try std.testing.expectEqualStrings("custom", entry.value_ptr.name); 452 - } 453 - 454 - try expectEqual(Layer.JsonLayer.DrawOrder.topdown, json_layer.draw_order); 455 - 456 - var layer = try Layer.fromJson(allocator, json_layer); 457 - defer layer.deinit(allocator); 458 - 459 - const object_group = layer.content.object_group; 460 - 461 - try expectEqual(5, object_group.objects.items.len); 462 - { 463 - const object = object_group.getByClass("hello").?; 464 - try expectEqual(1, object.id); 465 - } 466 - 467 - { 468 - const object = object_group.get("polygon").?; 469 - try expectEqual(8, object.id); 470 - } 471 - } 472 - 473 - const Property = @import("property.zig").Property; 474 - const tmz = @import("tmz.zig"); 519 + const tmz = @import("root.zig"); 475 520 const Color = tmz.Color; 476 - const jsonParser = tmz.jsonParser; 521 + const Property = tmz.Property; 477 522 478 523 const std = @import("std"); 479 524 const base64_decoder = std.base64.standard.Decoder; 480 525 const ParseOptions = std.json.ParseOptions; 481 526 const Value = std.json.Value; 482 527 const Allocator = std.mem.Allocator; 528 + const innerParseFromValue = std.json.innerParseFromValue; 483 529 const expectEqual = std.testing.expectEqual;
-300
src/map.zig
··· 1 - pub const Map = struct { 2 - background_color: ?tmz.Color = null, 3 - height: u32, 4 - width: u32, 5 - tile_width: u32, 6 - tile_height: u32, 7 - infinite: bool, 8 - orientation: Orientation, 9 - 10 - layers: std.ArrayListUnmanaged(Layer), 11 - tilesets: std.ArrayListUnmanaged(Tileset), 12 - 13 - class: ?[]const u8, 14 - 15 - pub const Orientation = enum { orthogonal, isometric, staggered, hexagonal }; 16 - 17 - pub fn initFromFile(allocator: Allocator, path: []const u8) anyerror!Map { 18 - const file = try std.fs.cwd().openFile(path, .{}); 19 - defer file.close(); 20 - 21 - const json = try file.reader().readAllAlloc(allocator, std.math.maxInt(u32)); 22 - defer allocator.free(json); 23 - 24 - return try initFromSlice(allocator, json); 25 - } 26 - 27 - pub fn initFromSlice(allocator: Allocator, json: []const u8) !Map { 28 - const parsed_value = try std.json.parseFromSlice(std.json.Value, allocator, json, .{ .ignore_unknown_fields = true }); 29 - defer parsed_value.deinit(); 30 - 31 - const map = try std.json.parseFromValue(JsonMap, allocator, parsed_value.value, .{ .ignore_unknown_fields = true }); 32 - defer map.deinit(); 33 - 34 - return try init(allocator, map); 35 - } 36 - 37 - pub fn init(allocator: std.mem.Allocator, parsed_map: std.json.Parsed(JsonMap)) !Map { 38 - const json_map = parsed_map.value; 39 - 40 - var map: Map = .{ 41 - .background_color = json_map.backgroundcolor, 42 - .height = json_map.height, 43 - .width = json_map.width, 44 - .tile_width = json_map.tilewidth, 45 - .tile_height = json_map.tileheight, 46 - .infinite = json_map.infinite, 47 - .orientation = json_map.orientation, 48 - .tilesets = .empty, 49 - .layers = .empty, 50 - 51 - .class = if (json_map.class) |class| try allocator.dupe(u8, class) else null, 52 - }; 53 - 54 - if (json_map.tilesets) |json_tilesets| { 55 - for (json_tilesets) |json_tileset| { 56 - const tileset = try Tileset.fromJson(allocator, json_tileset); 57 - try map.tilesets.append(allocator, tileset); 58 - } 59 - } 60 - 61 - if (json_map.layers) |json_layers| { 62 - for (json_layers) |json_layer| { 63 - const layer = try Layer.fromJson(allocator, json_layer); 64 - try map.layers.append(allocator, layer); 65 - } 66 - } 67 - 68 - return map; 69 - } 70 - 71 - pub fn deinit(self: *Map, allocator: std.mem.Allocator) void { 72 - if (self.class) |class| allocator.free(class); 73 - 74 - for (self.tilesets.items) |*tileset| { 75 - tileset.deinit(allocator); 76 - } 77 - self.tilesets.deinit(allocator); 78 - 79 - for (self.layers.items) |*layer| { 80 - layer.deinit(allocator); 81 - } 82 - self.layers.deinit(allocator); 83 - } 84 - 85 - pub fn getTile(self: Map, gid: u32) ?Tile { 86 - if (gid == 0) return null; 87 - 88 - const tileset_len = self.tilesets.items.len; 89 - var i = tileset_len - 1; 90 - while (i >= 0) : (i -= 1) { 91 - const tileset = self.tilesets.items[i]; 92 - if (tileset.first_gid <= gid) { 93 - return tileset.tiles.get(gid - tileset.first_gid); 94 - } 95 - } 96 - return null; 97 - } 98 - 99 - /// Finds first object 100 - pub fn getObjectByClass(self: Map, class: []const u8) ?tmz.Object { 101 - for (self.layers.items) |layer| { 102 - if (layer.content == .object_group) { 103 - if (layer.content.object_group.getByClass(class)) |object| { 104 - return object; 105 - } 106 - } 107 - } 108 - return null; 109 - } 110 - 111 - /// Finds first object by name 112 - pub fn getObject(self: Map, name: []const u8) ?tmz.Object { 113 - for (self.layers.items) |layer| { 114 - if (layer.content == .object_group) { 115 - if (layer.content.object_group.get(name)) |object| { 116 - return object; 117 - } 118 - } 119 - } 120 - return null; 121 - } 122 - 123 - pub fn pixelWidth(self: Map) u32 { 124 - return self.width * self.tile_width; 125 - } 126 - 127 - pub fn pixelHeight(self: Map) u32 { 128 - return self.height * self.tile_height; 129 - } 130 - 131 - /// https://doc.mapeditor.org/en/stable/reference/json-map-format/#map 132 - const JsonMap = struct { 133 - /// Hex-formatted color (#RRGGBB or #AARRGGBB) 134 - backgroundcolor: ?tmz.Color = null, 135 - class: ?[]const u8 = null, 136 - /// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) 137 - // TODO: actually support this? 138 - compression_level: i32 = -1, 139 - /// Number of tile rows 140 - height: u32, 141 - /// Length of the side of a hex tile in pixels (hexagonal maps only) 142 - hex_side_length: ?u32 = null, 143 - infinite: bool, 144 - layers: ?[]Layer.JsonLayer = null, 145 - /// Auto-increments for each layer 146 - nextlayerid: u32, 147 - /// Auto-increments for each placed object 148 - // next_object_id: u32, 149 - orientation: Orientation, 150 - parallax_origin_x: ?f32 = 0, 151 - parallax_origin_y: ?f32 = 0, 152 - properties: ?[]Property = null, 153 - /// currently only supported for orthogonal maps 154 - renderorder: ?RenderOrder = null, 155 - /// staggered / hexagonal maps only 156 - staggeraxis: ?StaggerAxis = null, 157 - /// staggered / hexagonal maps only 158 - staggerindex: ?StaggerIndex = null, 159 - tiledversion: []const u8, 160 - /// Map grid height 161 - tileheight: u32, 162 - tilesets: ?[]Tileset.JsonTileset = null, 163 - /// Map grid width 164 - tilewidth: u32, 165 - version: []const u8, 166 - /// Number of tile columns 167 - width: u32, 168 - 169 - pub const RenderOrder = enum { @"right-down", @"right-up", @"left-down", @"left-up" }; 170 - pub const StaggerAxis = enum { x, y }; 171 - pub const StaggerIndex = enum { odd, even }; 172 - }; 173 - }; 174 - 175 - test "initFromFile" { 176 - const allocator = std.testing.allocator; 177 - const test_maps = [_][]const u8{ 178 - "src/test/map.tmj", 179 - "src/test/map-base64-none.tmj", 180 - "src/test/map-base64-gzip.tmj", 181 - "src/test/map-base64-zlib.tmj", 182 - "src/test/map-base64-zstd.tmj", 183 - }; 184 - 185 - for (test_maps) |test_map| { 186 - var map = try Map.initFromFile(allocator, test_map); 187 - defer map.deinit(allocator); 188 - 189 - try testMap(map); 190 - } 191 - } 192 - 193 - test "initFromSlice" { 194 - const test_map = @embedFile("test/map.tmj"); 195 - 196 - const allocator = std.testing.allocator; 197 - 198 - var map = try Map.initFromSlice(allocator, test_map); 199 - defer map.deinit(allocator); 200 - 201 - try testMap(map); 202 - } 203 - 204 - test "infinite map with base64 chunks" { 205 - const test_map = @embedFile("test/map-infinite-base64-zstd.tmj"); 206 - 207 - const allocator = std.testing.allocator; 208 - 209 - var map = try Map.initFromSlice(allocator, test_map); 210 - defer map.deinit(allocator); 211 - 212 - try expectEqual(true, map.infinite); 213 - } 214 - 215 - test "infinite map with csv chunks" { 216 - const test_map = @embedFile("test/map-infinite-csv.tmj"); 217 - 218 - const allocator = std.testing.allocator; 219 - 220 - var map = try Map.initFromSlice(allocator, test_map); 221 - defer map.deinit(allocator); 222 - 223 - try expectEqual(true, map.infinite); 224 - 225 - try expectEqual(3, map.layers.items.len); 226 - 227 - // const chunk_layer = map.layers.items[0].tile_layer; 228 - // try expectEqual(4, chunk_layer.chunks.?.len); 229 - 230 - // const chunk = chunk_layer.chunks.?[0]; 231 - // try expectEqual(256, chunk.data.csv.len); 232 - // try expectEqual(16, chunk.data.csv[0]); 233 - } 234 - 235 - fn testMap(map: Map) !void { 236 - try expectEqualStrings("bar", map.class.?); 237 - try expectEqual(30, map.height); 238 - try expectEqual(30, map.width); 239 - try expectEqual(false, map.infinite); 240 - try expectEqual(.orthogonal, map.orientation); 241 - try expectEqual(32, map.tile_width); 242 - try expectEqual(32, map.tile_height); 243 - try expectEqual(2, map.tilesets.items.len); 244 - 245 - const tileset = map.tilesets.items[0]; 246 - try expectEqual(1, tileset.first_gid); 247 - try expectEqualStrings("tilemap.png", tileset.image.?); 248 - 249 - const layer = map.layers.items[0]; 250 - try expectEqual(17, layer.content.tile_layer.data.items[0]); 251 - 252 - const bad_tile = map.getTile(0); 253 - try expectEqual(null, bad_tile); 254 - 255 - const foo = map.getTile(2).?; 256 - try expectEqual(1, foo.id); 257 - try expectEqual(17, foo.x); 258 - try expectEqual(0, foo.y); 259 - 260 - const tile = map.getTile(17).?; 261 - try expectEqual(16, tile.id); 262 - try expectEqual(272, tile.x); 263 - try expectEqual(0, tile.y); 264 - try expectEqual(16, tile.width); 265 - try expectEqual(16, tile.height); 266 - 267 - try expectEqual(tileset.first_gid, tile.tileset.first_gid); 268 - 269 - try expectEqual(960, map.pixelWidth()); 270 - try expectEqual(960, map.pixelHeight()); 271 - } 272 - 273 - test "findObjectByClass" { 274 - const allocator = std.testing.allocator; 275 - const test_map = @embedFile("test/map.tmj"); 276 - 277 - var map = try Map.initFromSlice(allocator, test_map); 278 - defer map.deinit(allocator); 279 - 280 - const object = map.getObjectByClass("hello_world").?; 281 - try expectEqualStrings("hello", object.name); 282 - try expectEqual(70.0, object.x); 283 - try expectEqual(44.0, object.y); 284 - 285 - try expectEqual(null, map.getObjectByClass("non_existent")); 286 - } 287 - 288 - const tmz = @import("tmz.zig"); 289 - const Tileset = tmz.Tileset; 290 - const Tile = tmz.Tile; 291 - const Property = tmz.Property; 292 - const Layer = tmz.Layer; 293 - 294 - const std = @import("std"); 295 - const Allocator = std.mem.Allocator; 296 - const ParseOptions = std.json.ParseOptions; 297 - const Value = std.json.Value; 298 - const expectEqualDeep = std.testing.expectEqualDeep; 299 - const expectEqual = std.testing.expectEqual; 300 - const expectEqualStrings = std.testing.expectEqualStrings;
-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;
+77
src/root.zig
··· 1 + pub const Map = @import("Map.zig"); 2 + pub const Layer = layer.Layer; 3 + pub const Object = layer.Object; 4 + pub const Tileset = tileset.Tileset; 5 + pub const Tile = tileset.Tile; 6 + pub const Property = @import("Property.zig"); 7 + 8 + pub const Color = extern struct { 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: std.json.Value, _: std.json.ParseOptions) !@This() { 15 + return try parse(source.string); 16 + } 17 + 18 + // parses a Hex-formatted color (#RRGGBB or #AARRGGBB) string and returns 19 + // a Color. If no alpha channel value specified, defaults to 0 20 + pub fn parse(color_str: []const u8) !Color { 21 + if (color_str.len > 9 or color_str.len < 6) return error.UnexpectedToken; 22 + // buffer for the color_string stripped of '#' 23 + var hex_color: [8]u8 = @splat(0); 24 + // buffer for actual bytes parsed 25 + var color: [4]u8 = @splat(0); 26 + 27 + _ = std.mem.replace(u8, color_str, "#", "", &hex_color); 28 + // Handle strings with no alpha color defined 29 + if (color_str.len < 8) { 30 + // use same buffers for 32-bit color, but just use 6 bytes ascii and 31 + // 3 bytes for actual values 32 + _ = std.fmt.hexToBytes(color[1..4], hex_color[0..6]) catch return error.UnexpectedToken; 33 + } else { 34 + _ = std.fmt.hexToBytes(&color, &hex_color) catch return error.UnexpectedToken; 35 + } 36 + return std.mem.bytesToValue(Color, &color); 37 + } 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 Color.parse(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 Color.parse(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 Color.parse(weird_color)); 70 + 71 + try std.testing.expectError(error.UnexpectedToken, Color.parse("##bbaabbee")); 72 + } 73 + 74 + const layer = @import("layer.zig"); 75 + const tileset = @import("tileset.zig"); 76 + 77 + const std = @import("std");
+1 -1
src/test/images_tileset.tsj test/images_tileset.tsj
··· 21 21 "tilewidth":16, 22 22 "type":"tileset", 23 23 "version":"1.10" 24 - } 24 + }
+12 -10
src/test/map-base64-gzip.tmj test/map-base64-gzip.tmj
··· 1 - { "class":"bar", 1 + { 2 + "backgroundcolor":"#aabbccdd", 3 + "class":"bar", 2 4 "compressionlevel":-1, 3 5 "height":30, 4 6 "infinite":false, ··· 6 8 { 7 9 "class":"bar", 8 10 "compression":"gzip", 9 - "data":"H4sIAAAAAAAAA+3BMQEAAAjAoAXwsX9ZgwhsNQEAAPDNAfYOCYIQDgAA", 11 + "data":"H4sIAAAAAAAAA+3BAQEAAAABIP6fNkTVAAAA8Gh8W1o2EA4AAA==", 10 12 "encoding":"base64", 11 13 "height":30, 12 14 "id":1, ··· 26 28 { 27 29 "height":19, 28 30 "id":1, 29 - "name":"", 31 + "name":"hello", 30 32 "rotation":0, 31 33 "text": 32 34 { 33 35 "text":"Hello World", 34 36 "wrap":true 35 37 }, 36 - "type":"", 38 + "type":"hello_world", 37 39 "visible":true, 38 40 "width":91.4375, 39 41 "x":70, 40 - "y":44.6353383458647 42 + "y":44 41 43 }, 42 44 { 43 45 "height":84.2105263157895, ··· 71 73 "orientation":"orthogonal", 72 74 "renderorder":"right-down", 73 75 "tiledversion":"1.11.2", 74 - "tileheight":32, 76 + "tileheight":16, 75 77 "tilesets":[ 76 78 { 77 79 "firstgid":1, 78 - "source":"src\/test\/source_tileset.tsj" 80 + "source":"test/source_tileset.tsj" 79 81 }, 80 82 { 81 - "firstgid":1037, 82 - "source":"src\/test\/images_tileset.tsj" 83 + "firstgid":5, 84 + "source":"test/images_tileset.tsj" 83 85 }], 84 - "tilewidth":32, 86 + "tilewidth":16, 85 87 "type":"map", 86 88 "version":"1.10", 87 89 "width":30
-88
src/test/map-base64-none.tmj
··· 1 - { "class":"bar", 2 - "compressionlevel":-1, 3 - "height":30, 4 - "infinite":false, 5 - "layers":[ 6 - { 7 - "class":"bar", 8 - "compression":"", 9 - "data":"EQAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 10 - "encoding":"base64", 11 - "height":30, 12 - "id":1, 13 - "name":"Tile Layer 1", 14 - "opacity":1, 15 - "type":"tilelayer", 16 - "visible":true, 17 - "width":30, 18 - "x":0, 19 - "y":0 20 - }, 21 - { 22 - "draworder":"topdown", 23 - "id":2, 24 - "name":"Object Layer 1", 25 - "objects":[ 26 - { 27 - "height":19, 28 - "id":1, 29 - "name":"", 30 - "rotation":0, 31 - "text": 32 - { 33 - "text":"Hello World", 34 - "wrap":true 35 - }, 36 - "type":"", 37 - "visible":true, 38 - "width":91.4375, 39 - "x":70, 40 - "y":44.6353383458647 41 - }, 42 - { 43 - "height":84.2105263157895, 44 - "id":2, 45 - "name":"", 46 - "rotation":0, 47 - "type":"", 48 - "visible":true, 49 - "width":100.250626566416, 50 - "x":214.53634085213, 51 - "y":172.431077694236 52 - }], 53 - "opacity":1, 54 - "type":"objectgroup", 55 - "visible":true, 56 - "x":0, 57 - "y":0 58 - }, 59 - { 60 - "id":3, 61 - "image":"", 62 - "name":"Image Layer 1", 63 - "opacity":1, 64 - "type":"imagelayer", 65 - "visible":true, 66 - "x":0, 67 - "y":0 68 - }], 69 - "nextlayerid":4, 70 - "nextobjectid":3, 71 - "orientation":"orthogonal", 72 - "renderorder":"right-down", 73 - "tiledversion":"1.11.2", 74 - "tileheight":32, 75 - "tilesets":[ 76 - { 77 - "firstgid":1, 78 - "source":"src\/test\/source_tileset.tsj" 79 - }, 80 - { 81 - "firstgid":1037, 82 - "source":"src\/test\/images_tileset.tsj" 83 - }], 84 - "tilewidth":32, 85 - "type":"map", 86 - "version":"1.10", 87 - "width":30 88 - }
+11 -10
src/test/map-base64-zlib.tmj test/map-base64-zlib.tmj
··· 1 - { "class":"bar", 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 2 3 "compressionlevel":-1, 3 4 "height":30, 4 5 "infinite":false, ··· 6 7 { 7 8 "class":"bar", 8 9 "compression":"zlib", 9 - "data":"eJztwTEBAAAIwKAF8LF\/WYMIbDUBAADwzQGlvwAe", 10 + "data":"eJztwQEBAAAAASD+nzZE1QAAAPBoHCAAAg==", 10 11 "encoding":"base64", 11 12 "height":30, 12 13 "id":1, ··· 26 27 { 27 28 "height":19, 28 29 "id":1, 29 - "name":"", 30 + "name":"hello", 30 31 "rotation":0, 31 32 "text": 32 33 { 33 34 "text":"Hello World", 34 35 "wrap":true 35 36 }, 36 - "type":"", 37 + "type":"hello_world", 37 38 "visible":true, 38 39 "width":91.4375, 39 40 "x":70, 40 - "y":44.6353383458647 41 + "y":44 41 42 }, 42 43 { 43 44 "height":84.2105263157895, ··· 71 72 "orientation":"orthogonal", 72 73 "renderorder":"right-down", 73 74 "tiledversion":"1.11.2", 74 - "tileheight":32, 75 + "tileheight":16, 75 76 "tilesets":[ 76 77 { 77 78 "firstgid":1, 78 - "source":"src\/test\/source_tileset.tsj" 79 + "source":"test/source_tileset.tsj" 79 80 }, 80 81 { 81 - "firstgid":1037, 82 - "source":"src\/test\/images_tileset.tsj" 82 + "firstgid":5, 83 + "source":"test/images_tileset.tsj" 83 84 }], 84 - "tilewidth":32, 85 + "tilewidth":16, 85 86 "type":"map", 86 87 "version":"1.10", 87 88 "width":30
+11 -10
src/test/map-base64-zstd.tmj test/map-base64-zstd.tmj
··· 1 - { "class":"bar", 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 2 3 "compressionlevel":-1, 3 4 "height":30, 4 5 "infinite":false, ··· 6 7 { 7 8 "class":"bar", 8 9 "compression":"zstd", 9 - "data":"KLUv\/WAQDW0AADARAAAADAABAAf2QRE=", 10 + "data":"KLUv\/WAQDU0AABABAAEAC\/YBFg==", 10 11 "encoding":"base64", 11 12 "height":30, 12 13 "id":1, ··· 26 27 { 27 28 "height":19, 28 29 "id":1, 29 - "name":"", 30 + "name":"hello", 30 31 "rotation":0, 31 32 "text": 32 33 { 33 34 "text":"Hello World", 34 35 "wrap":true 35 36 }, 36 - "type":"", 37 + "type":"hello_world", 37 38 "visible":true, 38 39 "width":91.4375, 39 40 "x":70, 40 - "y":44.6353383458647 41 + "y":44 41 42 }, 42 43 { 43 44 "height":84.2105263157895, ··· 71 72 "orientation":"orthogonal", 72 73 "renderorder":"right-down", 73 74 "tiledversion":"1.11.2", 74 - "tileheight":32, 75 + "tileheight":16, 75 76 "tilesets":[ 76 77 { 77 78 "firstgid":1, 78 - "source":"src\/test\/source_tileset.tsj" 79 + "source":"test/source_tileset.tsj" 79 80 }, 80 81 { 81 - "firstgid":1037, 82 - "source":"src\/test\/images_tileset.tsj" 82 + "firstgid":5, 83 + "source":"test/images_tileset.tsj" 83 84 }], 84 - "tilewidth":32, 85 + "tilewidth":16, 85 86 "type":"map", 86 87 "version":"1.10", 87 88 "width":30
-41
src/test/map-csv.tmj
··· 1 - { "compressionlevel":-1, 2 - "height":5, 3 - "infinite":false, 4 - "layers":[ 5 - { 6 - "class":"bar", 7 - "data":[10, 15, 5, 3221225478, 1073741830, 8 - 5, 5, 5, 2147483654, 6, 9 - 3221225475, 1073741827, 5, 5, 5, 10 - 2147483651, 3, 5, 15, 16, 11 - 5, 5, 5, 16, 11], 12 - "height":5, 13 - "id":1, 14 - "name":"base64-ground", 15 - "opacity":1, 16 - "type":"tilelayer", 17 - "visible":true, 18 - "width":5, 19 - "x":0, 20 - "y":0 21 - }], 22 - "nextlayerid":2, 23 - "nextobjectid":1, 24 - "orientation":"orthogonal", 25 - "renderorder":"right-down", 26 - "tiledversion":"1.10.2", 27 - "tileheight":8, 28 - "tilesets":[ 29 - { 30 - "firstgid":1, 31 - "source":"src\/test\/source_tileset.tsj" 32 - }, 33 - { 34 - "firstgid":1037, 35 - "source":"src\/test\/images_tileset.tsj" 36 - }], 37 - "tilewidth":8, 38 - "type":"map", 39 - "version":"1.10", 40 - "width":5 41 - }
-137
src/test/map-infinite-base64-zstd.tmj
··· 1 - { "class":"bar", 2 - "compressionlevel":-1, 3 - "height":30, 4 - "infinite":true, 5 - "layers":[ 6 - { 7 - "chunks":[ 8 - { 9 - "data":"KLUv\/WAAA9UGAPLFDxPAJR3\/v+v\/N\/1v7iZkS5mLRlcKP\/0tQiBieEGG8GMICH4MPzYOmGJNQOVq5aaGbEmZttVhSeHUaB2RrH7+FVeooLhlDoCCGaoyDxGQhJYSEFVFbbUcm4S4\/zC+1IopmU\/wPW1YdH8THhx4e8zYoeYLWfd+\/yFv\/Fod+L5mxuF\/X6oPaz\/UozixnBL+ZPfLGts0fY52QV\/AjX2rjgjX+Ags9rs7voEFYm2abNBRqMnswvxgNq6UC\/co\/efu+5ldPHd43rLv+aGhPPgAz\/QczylL5tdg5gqn", 10 - "height":16, 11 - "width":16, 12 - "x":0, 13 - "y":0 14 - }, 15 - { 16 - "data":"KLUv\/WAAA80GAHKECw\/gaQwAIABQYJJJpjQkngI88BOQ6uvWSdXibJAo7pq11qIPts0JBskYK0OSJXlhqNC44XCCkak7DxGgiECShKgWxJY5v59BPzPfmxfuVoU9TdRy8Onzd4QOmw7JvPYnCmAY3ZAvRZcH43ZHDPllarkdzvPQg4a5IfFJc5yOEIec8Ui+mg18NPthM60tgTyyd6XeXz19Hsz0eFAHIF4XPAn3d53gj0\/2bCHcHrv7MC\/3xqNSfs4D3aTNOiAP8HstjnS45vrC+84HcMRY4rNb04PsbNJQkAE=", 17 - "height":16, 18 - "width":16, 19 - "x":16, 20 - "y":0 21 - }, 22 - { 23 - "data":"KLUv\/WAAA7UGACKECw\/gGQ4AoKAGHsqUpEw3Hyk2RxbCkZhs\/pVpXz7QjKkRbIyqLUWs7b5MgfDYRgFYqND4YlCCIURpHhIAEkBgrGEAUTCvuf5\/B5070Y5yrvUw3SjPgje6q9+wPTUdHb0F+YTtaAbG9wB0xUUOtxfrNUk70nPsnA68ilkrdudLbPKZD3DTlEGP95KHzQb+oqO9twvrmY+UwTEjHyg3iGboEx+7k5zzR34v2tHz6Cl2t\/t8Ocx1t99FTcWJ\/PfMzOi7dFT++cH9Yk8+0zsXT11wAPgMmCk=", 24 - "height":16, 25 - "width":16, 26 - "x":0, 27 - "y":16 28 - }, 29 - { 30 - "data":"KLUv\/WAAA10HAEJFDxawJekMAzgASZKDOAzo5JZ7791AzNoULZINBgdQZ4K5jr+iy6TIkbZjUaCjIHAukXTY4e+XPzD8\/PWTfgtbqPF5C+2\/AWCCQlNWHhKgRARsjSiUEUkKYsv+3wGdmkGHgRF9Gqs0D74bPO730c0fJr8HsZVh+fwF8Y98wTaHqT1D+3uMfSfkirM\/SDYK43DvMsLLd4lvsfVbBzPv5K\/\/Wwx0Zlv8Q2vYDaLC9GeeizrXHSrOjo2Pg78PGFr7PJEPDScnD+7LMoy9m7c9Xw\/GxIL\/9VzLX\/WhLaoJh9naWj7q4JXnt9iloAY=", 31 - "height":16, 32 - "width":16, 33 - "x":16, 34 - "y":16 35 - }], 36 - "class":"bar", 37 - "compression":"zstd", 38 - "encoding":"base64", 39 - "height":32, 40 - "id":1, 41 - "name":"ground", 42 - "opacity":1, 43 - "startx":0, 44 - "starty":0, 45 - "type":"tilelayer", 46 - "visible":true, 47 - "width":32, 48 - "x":0, 49 - "y":0 50 - }, 51 - { 52 - "draworder":"topdown", 53 - "id":2, 54 - "name":"objects", 55 - "objects":[ 56 - { 57 - "height":16, 58 - "id":1, 59 - "name":"pool", 60 - "rotation":0, 61 - "type":"", 62 - "visible":true, 63 - "width":16, 64 - "x":88, 65 - "y":96 66 - }, 67 - { 68 - "height":20, 69 - "id":2, 70 - "name":"", 71 - "rotation":0, 72 - "text": 73 - { 74 - "color":"#241f31", 75 - "fontfamily":"Serif", 76 - "text":"tmz", 77 - "wrap":true 78 - }, 79 - "type":"", 80 - "visible":true, 81 - "width":30, 82 - "x":70, 83 - "y":16 84 - }], 85 - "opacity":1, 86 - "type":"objectgroup", 87 - "visible":true, 88 - "x":0, 89 - "y":0 90 - }, 91 - { 92 - "id":3, 93 - "image":"tiles.png", 94 - "name":"tile_image", 95 - "offsetx":1, 96 - "offsety":1, 97 - "opacity":1, 98 - "type":"imagelayer", 99 - "visible":true, 100 - "x":0, 101 - "y":0 102 - }], 103 - "nextlayerid":4, 104 - "nextobjectid":3, 105 - "orientation":"orthogonal", 106 - "properties":[ 107 - { 108 - "name":"beautiful", 109 - "type":"bool", 110 - "value":true 111 - }], 112 - "renderorder":"right-down", 113 - "tiledversion":"1.11.2", 114 - "tileheight":8, 115 - "tilesets":[ 116 - { 117 - "firstgid":1, 118 - "source":"src\/test\/source_tileset.tsj" 119 - }, 120 - { 121 - "columns":78, 122 - "firstgid":1037, 123 - "image":"tilemap.png", 124 - "imageheight":475, 125 - "imagewidth":628, 126 - "margin":0, 127 - "name":"embedded_tiles", 128 - "spacing":0, 129 - "tilecount":4602, 130 - "tileheight":8, 131 - "tilewidth":8 132 - }], 133 - "tilewidth":8, 134 - "type":"map", 135 - "version":"1.10", 136 - "width":32 137 - }
-195
src/test/map-infinite-csv.tmj
··· 1 - { "class":"bar", 2 - "compressionlevel":-1, 3 - "height":30, 4 - "infinite":true, 5 - "layers":[ 6 - { 7 - "chunks":[ 8 - { 9 - "data":[16, 11, 16, 16, 16, 16, 16, 16, 10, 10, 16, 14, 16, 16, 10, 16, 10 - 16, 11, 11, 13, 14, 10, 11, 12, 16, 11, 11, 10, 13, 14, 13, 16, 11 - 16, 14, 13, 10, 12, 16, 16, 11, 10, 16, 9, 13, 11, 11, 10, 16, 12 - 10, 16, 16, 9, 13, 9, 15, 9, 14, 15, 9, 12, 13, 11, 13, 14, 13 - 11, 9, 14, 9, 9, 9, 13, 12, 9, 13, 10, 16, 9, 16, 9, 12, 14 - 13, 16, 12, 9, 12, 9, 12, 9, 9, 9, 12, 11, 16, 9, 16, 15, 15 - 12, 11, 14, 9, 16, 11, 10, 16, 12, 16, 9, 16, 12, 14, 16, 11, 16 - 14, 11, 14, 15, 11, 9, 9, 13, 9, 13, 16, 16, 14, 16, 13, 16, 17 - 9, 11, 9, 10, 14, 9, 9, 11, 13, 9, 13, 13, 16, 16, 9, 12, 18 - 12, 9, 10, 10, 11, 16, 16, 10, 9, 10, 10, 12, 9, 13, 10, 11, 19 - 11, 16, 12, 16, 14, 13, 16, 1073741834, 9, 15, 9, 12, 16, 12, 9, 9, 20 - 11, 11, 15, 16, 13, 9, 9, 13, 11, 13, 5, 5, 5, 5, 10, 13, 21 - 16, 11, 9, 13, 10, 10, 16, 14, 10, 15, 5, 3221225478, 1073741830, 5, 13, 10, 22 - 16, 13, 14, 13, 16, 9, 14, 5, 5, 5, 5, 2147483654, 6, 5, 10, 12, 23 - 16, 9, 9, 14, 16, 9, 14, 5, 3221225475, 1073741827, 5, 5, 5, 5, 12, 11, 24 - 16, 9, 14, 11, 9, 13, 10, 5, 2147483651, 3, 5, 15, 16, 10, 12, 13], 25 - "height":16, 26 - "width":16, 27 - "x":0, 28 - "y":0 29 - }, 30 - { 31 - "data":[16, 16, 16, 16, 9, 10, 16, 16, 16, 11, 11, 10, 12, 16, 11, 9, 32 - 16, 16, 16, 11, 15, 12, 16, 16, 9, 16, 3221225487, 3221225487, 16, 16, 16, 9, 33 - 9, 16, 11, 16, 13, 15, 15, 15, 9, 3221225487, 3221225487, 16, 3221225487, 11, 16, 16, 34 - 14, 10, 13, 15, 14, 11, 14, 3221225487, 14, 16, 16, 10, 10, 12, 16, 16, 35 - 11, 14, 12, 9, 12, 9, 13, 16, 16, 10, 14, 16, 9, 9, 11, 16, 36 - 13, 12, 13, 16, 12, 14, 9, 9, 12, 2, 2, 2, 9, 9, 14, 16, 37 - 12, 9, 13, 12, 2, 2, 2, 2, 2, 2, 16, 16, 2, 9, 16, 9, 38 - 10, 12, 11, 13, 2, 14, 12, 2, 2, 11, 16, 11, 2, 9, 15, 9, 39 - 12, 16, 14, 2, 2, 9, 10, 2, 2, 16, 10, 13, 2, 12, 15, 16, 40 - 10, 10, 11, 14, 2, 16, 9, 14, 13, 14, 14, 2, 2, 14, 9, 16, 41 - 9, 12, 16, 13, 2, 14, 9, 9, 11, 11, 16, 2, 9, 9, 11, 16, 42 - 9, 16, 14, 15, 2, 2, 13, 12, 14, 16, 2, 2, 16, 14, 9, 11, 43 - 14, 11, 10, 10, 11, 2, 9, 13, 16, 2, 2, 9, 14, 14, 9, 16, 44 - 9, 13, 12, 9, 9, 2, 2, 9, 9, 2, 10, 13, 9, 16, 15, 12, 45 - 13, 10, 16, 2147483663, 11, 15, 2, 2, 2, 2, 13, 16, 16, 12, 16, 14, 46 - 16, 9, 16, 2147483663, 16, 9, 11, 2, 2, 11, 15, 12, 14, 9, 16, 9], 47 - "height":16, 48 - "width":16, 49 - "x":16, 50 - "y":0 51 - }, 52 - { 53 - "data":[16, 16, 9, 9, 15, 3221225487, 16, 5, 5, 5, 5, 16, 11, 11, 13, 15, 54 - 12, 12, 15, 13, 14, 3221225487, 2147483663, 16, 16, 12, 10, 16, 16, 2147483663, 12, 9, 55 - 14, 10, 14, 9, 12, 12, 16, 2147483663, 11, 13, 14, 12, 2147483663, 9, 16, 1073741834, 56 - 12, 10, 12, 12, 9, 16, 9, 2147483663, 15, 16, 10, 14, 10, 11, 16, 16, 57 - 16, 12, 16, 10, 16, 16, 9, 12, 14, 12, 14, 9, 14, 10, 14, 9, 58 - 13, 9, 16, 9, 16, 16, 9, 13, 11, 11, 16, 14, 10, 3221225483, 15, 9, 59 - 13, 10, 10, 9, 16, 9, 16, 11, 10, 13, 12, 16, 2147483663, 11, 15, 11, 60 - 16, 11, 9, 9, 12, 14, 11, 16, 9, 16, 9, 10, 13, 10, 13, 13, 61 - 16, 11, 12, 11, 10, 13, 13, 10, 10, 9, 12, 15, 16, 12, 9, 11, 62 - 16, 11, 10, 9, 11, 15, 10, 15, 10, 11, 16, 10, 13, 15, 14, 3221225487, 63 - 16, 9, 13, 11, 9, 11, 14, 14, 9, 16, 16, 16, 16, 12, 11, 14, 64 - 16, 11, 9, 12, 16, 16, 9, 9, 14, 16, 16, 14, 15, 10, 10, 15, 65 - 12, 15, 11, 16, 9, 13, 16, 15, 14, 15, 12, 13, 13, 13, 14, 10, 66 - 16, 16, 16, 10, 12, 16, 16, 10, 13, 10, 14, 10, 10, 14, 13, 16, 67 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 69 - "height":16, 70 - "width":16, 71 - "x":0, 72 - "y":16 73 - }, 74 - { 75 - "data":[13, 11, 11, 13, 14, 12, 2147483658, 2, 9, 2147483663, 2147483663, 11, 16, 9, 15, 9, 76 - 14, 9, 10, 11, 16, 11, 9, 12, 14, 9, 9, 11, 11, 11, 16, 9, 77 - 12, 14, 11, 12, 11, 16, 10, 2147483658, 2147483658, 9, 15, 10, 16, 9, 14, 16, 78 - 34, 34, 34, 34, 34, 14, 10, 2147483663, 2147483658, 14, 11, 10, 11, 9, 15, 9, 79 - 34, 3221225511, 3221225511, 3221225511, 34, 16, 2147483663, 10, 2147483658, 16, 11, 16, 16, 16, 11, 9, 80 - 34, 3221225511, 34, 3221225511, 34, 9, 9, 10, 9, 11, 16, 16, 16, 16, 9, 10, 81 - 34, 3221225511, 3221225511, 3221225511, 34, 14, 16, 9, 14, 14, 16, 16, 16, 11, 12, 13, 82 - 34, 34, 34, 34, 34, 9, 13, 13, 9, 9, 12, 9, 13, 16, 12, 11, 83 - 16, 10, 11, 14, 11, 15, 10, 12, 16, 16, 16, 3221225487, 3221225487, 16, 13, 15, 84 - 13, 15, 12, 9, 13, 10, 16, 10, 9, 9, 16, 8, 8, 8, 8, 8, 85 - 13, 14, 13, 12, 15, 13, 12, 12, 16, 16, 9, 8, 1, 1, 1, 1, 86 - 15, 10, 13, 12, 14, 15, 16, 16, 3221225483, 3221225483, 3221225483, 8, 1, 1073741825, 1073741825, 1, 87 - 9, 15, 9, 13, 11, 13, 9, 10, 3221225487, 3221225487, 11, 8, 1, 1073741825, 1073741825, 1, 88 - 14, 9, 14, 12, 16, 13, 16, 16, 14, 9, 9, 8, 1, 1073741825, 1073741825, 1, 89 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 91 - "height":16, 92 - "width":16, 93 - "x":16, 94 - "y":16 95 - }], 96 - "class":"bar", 97 - "height":32, 98 - "id":1, 99 - "name":"ground", 100 - "opacity":1, 101 - "startx":0, 102 - "starty":0, 103 - "type":"tilelayer", 104 - "visible":true, 105 - "width":32, 106 - "x":0, 107 - "y":0 108 - }, 109 - { 110 - "draworder":"topdown", 111 - "id":2, 112 - "name":"objects", 113 - "objects":[ 114 - { 115 - "height":16, 116 - "id":1, 117 - "name":"pool", 118 - "rotation":0, 119 - "type":"", 120 - "visible":true, 121 - "width":16, 122 - "x":88, 123 - "y":96 124 - }, 125 - { 126 - "height":20, 127 - "id":2, 128 - "name":"", 129 - "rotation":0, 130 - "text": 131 - { 132 - "color":"#241f31", 133 - "fontfamily":"Serif", 134 - "text":"tmz", 135 - "wrap":true 136 - }, 137 - "type":"", 138 - "visible":true, 139 - "width":30, 140 - "x":70, 141 - "y":16 142 - }], 143 - "opacity":1, 144 - "type":"objectgroup", 145 - "visible":true, 146 - "x":0, 147 - "y":0 148 - }, 149 - { 150 - "id":3, 151 - "image":"tiles.png", 152 - "name":"tile_image", 153 - "offsetx":1, 154 - "offsety":1, 155 - "opacity":1, 156 - "type":"imagelayer", 157 - "visible":true, 158 - "x":0, 159 - "y":0 160 - }], 161 - "nextlayerid":4, 162 - "nextobjectid":3, 163 - "orientation":"orthogonal", 164 - "properties":[ 165 - { 166 - "name":"beautiful", 167 - "type":"bool", 168 - "value":true 169 - }], 170 - "renderorder":"right-down", 171 - "tiledversion":"1.10.2", 172 - "tileheight":8, 173 - "tilesets":[ 174 - { 175 - "firstgid":1, 176 - "source":"src/test/source_tileset.tsj" 177 - }, 178 - { 179 - "columns":4, 180 - "firstgid":17, 181 - "image":"tiles.png", 182 - "imageheight":32, 183 - "imagewidth":32, 184 - "margin":0, 185 - "name":"embedded_tiles", 186 - "spacing":0, 187 - "tilecount":16, 188 - "tileheight":8, 189 - "tilewidth":8 190 - }], 191 - "tilewidth":8, 192 - "type":"map", 193 - "version":"1.10", 194 - "width":32 195 - }
+8 -7
src/test/map.tmj test/map.tmj
··· 1 - { "class":"bar", 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 2 3 "compressionlevel":-1, 3 4 "height":30, 4 5 "infinite":false, 5 6 "layers":[ 6 7 { 7 8 "class":"bar", 8 - "data":[17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9 + "data":[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9 10 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10 11 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11 12 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ··· 98 99 "orientation":"orthogonal", 99 100 "renderorder":"right-down", 100 101 "tiledversion":"1.11.2", 101 - "tileheight":32, 102 + "tileheight":16, 102 103 "tilesets":[ 103 104 { 104 105 "firstgid":1, 105 - "source":"src\/test\/source_tileset.tsj" 106 + "source":"test/source_tileset.tsj" 106 107 }, 107 108 { 108 - "firstgid":1037, 109 - "source":"src\/test\/images_tileset.tsj" 109 + "firstgid":5, 110 + "source":"test/images_tileset.tsj" 110 111 }], 111 - "tilewidth":32, 112 + "tilewidth":16, 112 113 "type":"map", 113 114 "version":"1.10", 114 115 "width":30
-100
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": "hello", 16 - "visible": true, 17 - "width": 91.4375, 18 - "x": 70, 19 - "y": 44.6353383458647 20 - }, 21 - { 22 - "gid":16403, 23 - "height":48, 24 - "id":2, 25 - "name":"", 26 - "rotation":0, 27 - "type":"player", 28 - "visible":true, 29 - "width":32, 30 - "x":55.2142976793752, 31 - "y":183.914075980747 32 - }, 33 - { 34 - "height": 19, 35 - "id": 3, 36 - "ellipse": true, 37 - "name": "", 38 - "rotation": 0, 39 - "type": "", 40 - "visible": true, 41 - "width": 91.4375, 42 - "x": 70, 43 - "y": 44.6353383458647 44 - }, 45 - { 46 - "height":0, 47 - "id":8, 48 - "name":"polygon", 49 - "polygon":[ 50 - { 51 - "x":0, 52 - "y":0 53 - }, 54 - { 55 - "x":52.043665905052, 56 - "y":49.5049504950495 57 - }, 58 - { 59 - "x":77.4308200050775, 60 - "y":-8.8855039350089 61 - }], 62 - "rotation":0, 63 - "type":"", 64 - "visible":true, 65 - "width":0, 66 - "x":88.8550393500889, 67 - "y":248.794110180249 68 - }, 69 - { 70 - "height":0, 71 - "id":9, 72 - "name":"", 73 - "polyline":[ 74 - { 75 - "x":0, 76 - "y":0 77 - }, 78 - { 79 - "x":58.3904544300584, 80 - "y":77.4308200050774 81 - }], 82 - "rotation":0, 83 - "type":"", 84 - "visible":true, 85 - "width":0, 86 - "x":302.107133790302, 87 - "y":126.935770500127 88 - } 89 - ], 90 - "opacity": 1, 91 - "properties":[{ 92 - "name":"custom", 93 - "type":"int", 94 - "value":50 95 - }], 96 - "type": "objectgroup", 97 - "visible": true, 98 - "x": 0, 99 - "y": 0 100 - }
src/test/properties.json test/properties.json
+6 -7
src/test/source_tileset.tsj test/source_tileset.tsj
··· 1 1 { "backgroundcolor":"#ffaaff", 2 - "columns":37, 2 + "columns":2, 3 3 "image":"tilemap.png", 4 - "imageheight":475, 5 - "imagewidth":628, 4 + "imageheight":32, 5 + "imagewidth":32, 6 6 "margin":0, 7 7 "name":"tiles", 8 8 "properties":[ ··· 11 11 "type":"int", 12 12 "value":50 13 13 }], 14 - "spacing":1, 15 - "tilecount":1131, 14 + "spacing":0, 15 + "tilecount":4, 16 16 "tiledversion":"1.11.2", 17 17 "tileheight":16, 18 18 "tiles":[ ··· 21 21 "objectgroup": 22 22 { 23 23 "draworder":"index", 24 - "id":3, 25 24 "name":"", 26 25 "objects":[ 27 26 { ··· 104 103 "wangid":[1, 1, 1, 1, 1, 1, 1, 1] 105 104 }] 106 105 }] 107 - } 106 + }
src/test/tile.png test/tile.png
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 - }
+134 -81
src/tileset.zig
··· 1 1 pub const Tileset = struct { 2 2 columns: u32, 3 - tile_count: u32, 3 + tilecount: u32, 4 4 tile_width: u32, 5 5 tile_height: u32, 6 6 first_gid: u32, 7 - image: ?[]const u8, 8 7 9 - tiles: std.AutoHashMapUnmanaged(u32, Tile), 8 + image: ?[]const u8 = null, 10 9 11 - pub fn initFromFile(allocator: Allocator, path: []const u8) anyerror!Tileset { 12 - const file = try std.fs.cwd().openFile(path, .{}); 13 - defer file.close(); 10 + tiles: std.AutoHashMapUnmanaged(u32, Tile), 11 + properties: std.StringHashMapUnmanaged(Property), 14 12 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; 13 + pub fn initFromSlice(alloc: Allocator, json: []const u8) !Tileset { 14 + var arena = std.heap.ArenaAllocator.init(alloc); 15 + defer arena.deinit(); 16 + const arena_allocator = arena.allocator(); 29 17 30 - return fromJson(allocator, json_tileset); 31 - } 18 + const parsed_value = try std.json.parseFromSliceLeaky( 19 + std.json.Value, 20 + arena_allocator, 21 + json, 22 + .{ .ignore_unknown_fields = true }, 23 + ); 32 24 33 - pub fn deinit(self: *Tileset, allocator: Allocator) void { 34 - if (self.image) |image| allocator.free(image); 25 + const json_tileset = try std.json.parseFromValueLeaky( 26 + JsonTileset, 27 + arena_allocator, 28 + parsed_value, 29 + .{ .ignore_unknown_fields = true }, 30 + ); 35 31 36 - self.tiles.deinit(allocator); 32 + return try fromJson(alloc, json_tileset); 37 33 } 38 34 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; 35 + pub fn fromJson(allocator: Allocator, json_tileset: JsonTileset) !Tileset { 36 + if (json_tileset.source) |source| { 37 + var source_tileset = try initFromFile(allocator, source); 38 + source_tileset.first_gid = json_tileset.firstgid orelse 1; 43 39 44 - return tileset; 40 + return source_tileset; 45 41 } 46 42 47 43 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, 44 + .columns = json_tileset.columns orelse 0, 45 + .tilecount = json_tileset.tilecount orelse 0, 46 + .tile_width = json_tileset.tilewidth orelse 0, 47 + .tile_height = json_tileset.tileheight orelse 0, 48 + .first_gid = json_tileset.firstgid orelse 1, 49 + 50 + .image = if (json_tileset.image) |image| try allocator.dupe(u8, image) else null, 51 + 54 52 .tiles = .empty, 53 + .properties = .empty, 55 54 }; 56 55 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| { 56 + if (json_tileset.tiles) |json_tiles| { 61 57 for (json_tiles) |json_tile| { 62 - try tiles_by_id.put(allocator, json_tile.id, json_tile); 58 + const tile = try Tile.fromJson(allocator, json_tile, tileset); 59 + try tileset.tiles.put(allocator, tile.id, tile); 63 60 } 64 61 } 65 62 ··· 68 65 69 66 var gid = tileset.first_gid - 1; 70 67 71 - const last_gid: u32 = gid + tileset.tile_count - 1; 68 + const last_gid: u32 = gid + tileset.tilecount -| 1; 72 69 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.?, 70 + const tile_x = x * (tileset.tile_width + json_tileset.spacing) + json_tileset.margin; 71 + const tile_y = y * (tileset.tile_height + json_tileset.spacing) + json_tileset.margin; 72 + 73 + var tile: Tile = fetch_or_create: { 74 + if (tileset.tiles.getPtr(gid)) |t| { 75 + t.*.x = tile_x; 76 + t.*.y = tile_y; 77 + break :fetch_or_create t.*; 78 + } else { 79 + break :fetch_or_create .{ 80 + .id = gid, 81 + .x = tile_x, 82 + .y = tile_y, 83 + 84 + .tileset = tileset, 85 + }; 86 + } 81 87 }; 82 - 83 - if (tiles_by_id.get(gid)) |json_tile| { 84 - tile.animation = json_tile.animation; 85 - // tile = Tile.fromJson(json_tile); 86 - } 88 + tile.width = tileset.tile_width; 89 + tile.height = tileset.tile_height; 87 90 try tileset.tiles.put(allocator, gid, tile); 91 + 88 92 if (x >= @as(i64, @intCast(tileset.columns)) - 1) { 89 93 y += 1; 90 94 x = 0; ··· 93 97 } 94 98 } 95 99 100 + if (json_tileset.properties) |properties| { 101 + for (properties) |property| { 102 + const prop = try Property.fromJson(allocator, property); 103 + try tileset.properties.put(allocator, prop.name, prop); 104 + } 105 + } 106 + 96 107 return tileset; 97 108 } 98 109 110 + pub fn initFromFile(allocator: Allocator, path: []const u8) anyerror!Tileset { 111 + const file = try std.fs.cwd().openFile(path, .{}); 112 + defer file.close(); 113 + 114 + var reader: std.fs.File.Reader = .initStreaming(file, &.{}); 115 + 116 + var out: std.Io.Writer.Allocating = .init(allocator); 117 + defer out.deinit(); 118 + 119 + _ = try reader.interface.streamRemaining(&out.writer); 120 + 121 + const json = try out.toOwnedSlice(); 122 + defer allocator.free(json); 123 + 124 + return initFromSlice(allocator, json); 125 + } 126 + 127 + pub fn deinit(self: *Tileset, allocator: Allocator) void { 128 + if (self.image) |image| allocator.free(image); 129 + 130 + var tiles_it = self.tiles.valueIterator(); 131 + while (tiles_it.next()) |value_ptr| { 132 + value_ptr.*.deinit(allocator); 133 + } 134 + 135 + self.tiles.deinit(allocator); 136 + 137 + var properties_it = self.properties.valueIterator(); 138 + while (properties_it.next()) |value_ptr| { 139 + value_ptr.*.deinit(allocator); 140 + } 141 + self.properties.deinit(allocator); 142 + } 143 + 99 144 pub const JsonTileset = struct { 100 145 columns: ?u32 = null, 101 146 tilecount: ?u32 = null, ··· 107 152 tiles: ?[]Tile.JsonTile = null, 108 153 tilewidth: ?u32 = null, 109 154 tileheight: ?u32 = null, 155 + properties: ?[]Property = null, 110 156 }; 111 157 }; 112 158 113 159 pub const Tile = struct { 114 160 id: u32 = 1, 115 - x: u32, 116 - y: u32, 117 - width: u32, 118 - height: u32, 161 + x: u32 = 0, 162 + y: u32 = 0, 163 + width: u32 = 0, 164 + height: u32 = 0, 165 + animation: ?[]const Frame = null, 119 166 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 167 133 168 pub const JsonTile = struct { 134 169 id: u32, ··· 138 173 height: ?u32 = null, 139 174 animation: ?[]const Frame = null, 140 175 }; 176 + 177 + pub fn fromJson(allocator: Allocator, json_tile: JsonTile, tileset: Tileset) !Tile { 178 + return .{ 179 + .id = json_tile.id, 180 + .x = json_tile.x orelse 0, 181 + .y = json_tile.y orelse 0, 182 + .width = json_tile.width orelse tileset.tile_width, 183 + .height = json_tile.height orelse tileset.tile_height, 184 + .animation = if (json_tile.animation) |animation| try allocator.dupe(Frame, animation) else null, 185 + .tileset = tileset, 186 + }; 187 + } 188 + 189 + pub fn deinit(self: *Tile, allocator: Allocator) void { 190 + if (self.animation) |animation| allocator.free(animation); 191 + } 192 + 193 + pub fn jsonParseFromValue(allocator: Allocator, source: std.json.Value, options: std.json.ParseOptions) !Tile { 194 + return .{ 195 + .id = try innerParseFromValue(u32, allocator, source.object.get("id").?, options), 196 + .animation = if (source.object.get("animation")) |animation| try innerParseFromValue([]const Frame, allocator, animation, options) else null, 197 + .tileset = undefined, 198 + }; 199 + } 141 200 }; 142 201 143 202 pub const Frame = struct { 144 203 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 - } 204 + tileid: u32, 150 205 }; 151 206 152 207 inline fn get(value: anytype) @TypeOf(value) { 153 208 return if (value) |v| return v else null; 154 209 } 210 + 211 + const Property = @import("Property.zig"); 155 212 156 213 const std = @import("std"); 157 214 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; 215 + const innerParseFromValue = std.json.innerParseFromValue;
-151
src/tmz.zig
··· 1 - pub const Map = map.Map; 2 - pub const loadMapFromSlice = Map.initFromSlice; 3 - pub const loadMapFromFile = Map.initFromFile; 4 - 5 - pub const Tileset = tileset.Tileset; 6 - pub const Tile = tileset.Tile; 7 - pub const loadTilesetFromSlice = Tileset.initFromSlice; 8 - pub const loadTilesetFromFile = Tileset.initFromFile; 9 - 10 - pub const Layer = layer.Layer; 11 - pub const TileLayer = layer.TileLayer; 12 - pub const Object = layer.Object; 13 - pub const ObjectGroup = layer.ObjectGroup; 14 - 15 - pub const Property = @import("property.zig").Property; 16 - 17 - pub const Color = packed struct(u32) { 18 - a: u8 = 0, 19 - r: u8 = 0, 20 - g: u8 = 0, 21 - b: u8 = 0, 22 - 23 - pub fn jsonParseFromValue(_: std.mem.Allocator, source: Value, _: ParseOptions) !@This() { 24 - return try parseColor(source.string); 25 - } 26 - }; 27 - 28 - // parses a Hex-formatted color (#RRGGBB or #AARRGGBB) string and returns 29 - // a Color. If no alpha channel value specified, defaults to 0 30 - fn parseColor(color_string: []const u8) !Color { 31 - if (color_string.len > 9 or color_string.len < 6) return error.UnexpectedToken; 32 - // buffer for the color_string stripped of '#' 33 - var hex_color: [8]u8 = @splat(0); 34 - // buffer for actual bytes parsed 35 - var color: [4]u8 = @splat(0); 36 - 37 - _ = std.mem.replace(u8, color_string, "#", "", &hex_color); 38 - // Handle strings with no alpha color defined 39 - if (color_string.len < 8) { 40 - // use same buffers for 32-bit color, but just use 6 bytes ascii and 41 - // 3 bytes for actual values 42 - _ = std.fmt.hexToBytes(color[1..4], hex_color[0..6]) catch return error.UnexpectedToken; 43 - } else { 44 - _ = std.fmt.hexToBytes(&color, &hex_color) catch return error.UnexpectedToken; 45 - } 46 - return std.mem.bytesToValue(Color, &color); 47 - } 48 - 49 - test "Color is parsed from string" { 50 - const full_color = "#ccaaffee"; 51 - const expected_full_color: Color = .{ 52 - .a = 0xcc, 53 - .r = 0xaa, 54 - .g = 0xff, 55 - .b = 0xee, 56 - }; 57 - 58 - try std.testing.expectEqual(expected_full_color, try parseColor(full_color)); 59 - 60 - const no_alpha_color = "#bbaadd"; 61 - const expected_no_alpha_color: Color = .{ 62 - .a = 0, 63 - .r = 0xbb, 64 - .g = 0xaa, 65 - .b = 0xdd, 66 - }; 67 - 68 - try std.testing.expectEqual(expected_no_alpha_color, try parseColor(no_alpha_color)); 69 - 70 - const weird_color = "#DeaDBeeF"; 71 - const expected_weird_color: Color = .{ 72 - .a = 0xde, 73 - .r = 0xad, 74 - .g = 0xbe, 75 - .b = 0xef, 76 - }; 77 - 78 - try std.testing.expectEqual(expected_weird_color, try parseColor(weird_color)); 79 - 80 - try std.testing.expectError(error.UnexpectedToken, parseColor("##bbaabbee")); 81 - } 82 - 83 - // converts masheduppropertynames to Zig style snake_case field names and 84 - // ignores data, value and properties so they can be handled later 85 - pub fn jsonParser(T: type, allocator: Allocator, source: Value, options: ParseOptions) !T { 86 - var t: T = undefined; 87 - 88 - inline for (@typeInfo(T).@"struct".fields) |field| { 89 - if (comptime eql(u8, field.name, "data") or eql(u8, field.name, "value") or eql(u8, field.name, "properties")) continue; 90 - 91 - const size = comptime std.mem.replacementSize(u8, field.name, "_", ""); 92 - var tiled_name: [size]u8 = undefined; 93 - _ = std.mem.replace(u8, field.name, "_", "", &tiled_name); 94 - 95 - const source_field = source.object.get(&tiled_name); 96 - if (source_field) |s| { 97 - @field(t, field.name) = try std.json.innerParseFromValue(field.type, allocator, s, options); 98 - } else { 99 - if (field.default_value_ptr) |val| { 100 - @field(t, field.name) = @as(*align(1) const field.type, @ptrCast(val)).*; 101 - } 102 - } 103 - } 104 - 105 - return t; 106 - } 107 - 108 - const TestJson = struct { 109 - property_id: u8 = 0, 110 - data: u8, 111 - value: u8, 112 - 113 - pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: std.json.ParseOptions) !@This() { 114 - return try jsonParser(@This(), allocator, source, options); 115 - } 116 - }; 117 - 118 - test "jsonParser works" { 119 - const test_json = 120 - \\{ 121 - \\ "propertyid": 9, 122 - \\ "data": 8, 123 - \\ "value": 7 124 - \\} 125 - ; 126 - const parsed_value = try std.json.parseFromSlice(Value, std.testing.allocator, test_json, .{}); 127 - defer parsed_value.deinit(); 128 - 129 - const parsed_json = try std.json.parseFromValue(TestJson, std.testing.allocator, parsed_value.value, .{}); 130 - defer parsed_json.deinit(); 131 - 132 - // Property names are "converted" to snake_case 133 - try std.testing.expectEqual(9, parsed_json.value.property_id); 134 - 135 - // `data` and `value` fields are ignored 136 - try std.testing.expectEqual(0xaa, parsed_json.value.data); 137 - try std.testing.expectEqual(0xaa, parsed_json.value.value); 138 - } 139 - test { 140 - std.testing.refAllDeclsRecursive(@This()); 141 - } 142 - 143 - const std = @import("std"); 144 - const eql = std.mem.eql; 145 - const Allocator = std.mem.Allocator; 146 - const ParseOptions = std.json.ParseOptions; 147 - const Value = std.json.Value; 148 - 149 - const map = @import("map.zig"); 150 - const tileset = @import("tileset.zig"); 151 - const layer = @import("layer.zig");
+40
test/layers.zig
··· 1 + test "object layer" { 2 + const allocator = std.testing.allocator; 3 + 4 + const json = @embedFile("object_layer.json"); 5 + 6 + const parsed_layer = try std.json.parseFromSlice(std.json.Value, allocator, json, .{ .ignore_unknown_fields = true }); 7 + defer parsed_layer.deinit(); 8 + const managed_layer = try std.json.parseFromValue(Layer.JsonLayer, allocator, parsed_layer.value, .{ .ignore_unknown_fields = true }); 9 + defer managed_layer.deinit(); 10 + const json_layer = managed_layer.value; 11 + 12 + const properties = json_layer.properties.?; 13 + for (properties) |prop| { 14 + try std.testing.expectEqualStrings("custom", prop.name); 15 + } 16 + 17 + try expectEqual(Layer.JsonLayer.DrawOrder.topdown, json_layer.draw_order); 18 + 19 + var layer = try Layer.fromJson(allocator, json_layer); 20 + defer layer.deinit(allocator); 21 + // 22 + const object_group = layer.content.object_group; 23 + // 24 + try expectEqual(5, object_group.objects.items.len); 25 + // { 26 + // const object = object_group.getByClass("hello").?; 27 + // try expectEqual(1, object.id); 28 + // } 29 + // 30 + // { 31 + // const object = object_group.get("polygon").?; 32 + // try expectEqual(8, object.id); 33 + // } 34 + } 35 + 36 + const tmz = @import("tmz"); 37 + const Layer = tmz.Layer; 38 + 39 + const std = @import("std"); 40 + const expectEqual = std.testing.expectEqual;
+97
test/map-base64-none.tmj
··· 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 3 + "compressionlevel":-1, 4 + "editorsettings": 5 + { 6 + "export": 7 + { 8 + "format":"json", 9 + "target":"map-base64-zstd.tmj" 10 + } 11 + }, 12 + "height":30, 13 + "infinite":false, 14 + "layers":[ 15 + { 16 + "class":"bar", 17 + "compression":"zstd", 18 + "data":"KLUv\/WAQDU0AABABAAEAC\/YBFg==", 19 + "encoding":"base64", 20 + "height":30, 21 + "id":1, 22 + "name":"Tile Layer 1", 23 + "opacity":1, 24 + "type":"tilelayer", 25 + "visible":true, 26 + "width":30, 27 + "x":0, 28 + "y":0 29 + }, 30 + { 31 + "draworder":"topdown", 32 + "id":2, 33 + "name":"Object Layer 1", 34 + "objects":[ 35 + { 36 + "height":19, 37 + "id":1, 38 + "name":"hello", 39 + "rotation":0, 40 + "text": 41 + { 42 + "text":"Hello World", 43 + "wrap":true 44 + }, 45 + "type":"hello_world", 46 + "visible":true, 47 + "width":91.4375, 48 + "x":70, 49 + "y":44 50 + }, 51 + { 52 + "height":84.2105263157895, 53 + "id":2, 54 + "name":"", 55 + "rotation":0, 56 + "type":"", 57 + "visible":true, 58 + "width":100.250626566416, 59 + "x":214.53634085213, 60 + "y":172.431077694236 61 + }], 62 + "opacity":1, 63 + "type":"objectgroup", 64 + "visible":true, 65 + "x":0, 66 + "y":0 67 + }, 68 + { 69 + "id":3, 70 + "image":"", 71 + "name":"Image Layer 1", 72 + "opacity":1, 73 + "type":"imagelayer", 74 + "visible":true, 75 + "x":0, 76 + "y":0 77 + }], 78 + "nextlayerid":4, 79 + "nextobjectid":3, 80 + "orientation":"orthogonal", 81 + "renderorder":"right-down", 82 + "tiledversion":"1.11.2", 83 + "tileheight":16, 84 + "tilesets":[ 85 + { 86 + "firstgid":1, 87 + "source":"test/source_tileset.tsj" 88 + }, 89 + { 90 + "firstgid":5, 91 + "source":"test/images_tileset.tsj" 92 + }], 93 + "tilewidth":16, 94 + "type":"map", 95 + "version":"1.10", 96 + "width":30 97 + }
+127
test/map-infinite-base64-zstd.tmj
··· 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 3 + "compressionlevel":-1, 4 + "editorsettings": 5 + { 6 + "export": 7 + { 8 + "format":"json", 9 + "target":"map-infinite-base64-zstd.tmj" 10 + } 11 + }, 12 + "height":30, 13 + "infinite":true, 14 + "layers":[ 15 + { 16 + "chunks":[ 17 + { 18 + "data":"KLUv\/WAAA60BAEgHAAAABgUFBQURgHA7Bx0upICpABq3OgGHrQrwylFzR\/asmZlUAAUwCNoZZgElX0nFT5cC", 19 + "height":16, 20 + "width":16, 21 + "x":16, 22 + "y":16 23 + }], 24 + "class":"bar", 25 + "compression":"zstd", 26 + "encoding":"base64", 27 + "height":16, 28 + "id":1, 29 + "name":"ground", 30 + "opacity":1, 31 + "startx":16, 32 + "starty":16, 33 + "type":"tilelayer", 34 + "visible":true, 35 + "width":16, 36 + "x":0, 37 + "y":0 38 + }, 39 + { 40 + "draworder":"topdown", 41 + "id":2, 42 + "name":"objects", 43 + "objects":[ 44 + { 45 + "height":16, 46 + "id":1, 47 + "name":"pool", 48 + "rotation":0, 49 + "type":"", 50 + "visible":true, 51 + "width":16, 52 + "x":88, 53 + "y":96 54 + }, 55 + { 56 + "height":20, 57 + "id":2, 58 + "name":"", 59 + "rotation":0, 60 + "text": 61 + { 62 + "color":"#241f31", 63 + "fontfamily":"Serif", 64 + "text":"tmz", 65 + "wrap":true 66 + }, 67 + "type":"", 68 + "visible":true, 69 + "width":30, 70 + "x":70, 71 + "y":16 72 + }], 73 + "opacity":1, 74 + "type":"objectgroup", 75 + "visible":true, 76 + "x":0, 77 + "y":0 78 + }, 79 + { 80 + "id":3, 81 + "image":"tilemap.png", 82 + "imageheight":32, 83 + "imagewidth":32, 84 + "name":"tile_image", 85 + "offsetx":1, 86 + "offsety":1, 87 + "opacity":1, 88 + "type":"imagelayer", 89 + "visible":true, 90 + "x":0, 91 + "y":0 92 + }], 93 + "nextlayerid":4, 94 + "nextobjectid":3, 95 + "orientation":"orthogonal", 96 + "properties":[ 97 + { 98 + "name":"beautiful", 99 + "type":"bool", 100 + "value":true 101 + }], 102 + "renderorder":"right-down", 103 + "tiledversion":"1.11.2", 104 + "tileheight":16, 105 + "tilesets":[ 106 + { 107 + "firstgid":1, 108 + "source":"test/source_tileset.tsj" 109 + }, 110 + { 111 + "columns":2, 112 + "firstgid":5, 113 + "image":"tilemap.png", 114 + "imageheight":32, 115 + "imagewidth":32, 116 + "margin":0, 117 + "name":"embedded_tiles", 118 + "spacing":0, 119 + "tilecount":4, 120 + "tileheight":16, 121 + "tilewidth":16 122 + }], 123 + "tilewidth":16, 124 + "type":"map", 125 + "version":"1.10", 126 + "width":30 127 + }
+140
test/map-infinite-csv.tmj
··· 1 + { "backgroundcolor":"#aabbccdd", 2 + "class":"bar", 3 + "compressionlevel":-1, 4 + "editorsettings": 5 + { 6 + "export": 7 + { 8 + "format":"json", 9 + "target":"map-infinite-base64-zstd.tmj" 10 + } 11 + }, 12 + "height":30, 13 + "infinite":true, 14 + "layers":[ 15 + { 16 + "chunks":[ 17 + { 18 + "data":[7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 19 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 20 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 21 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 22 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 23 + 6, 6, 6, 6, 6, 5, 6, 6, 6, 6, 6, 5, 5, 6, 6, 6, 24 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 25 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 26 + 6, 6, 6, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 6, 6, 6, 27 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 28 + 6, 6, 6, 5, 5, 6, 6, 6, 6, 6, 6, 5, 5, 5, 6, 6, 29 + 6, 6, 6, 6, 5, 5, 6, 6, 5, 5, 5, 5, 6, 6, 6, 6, 30 + 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 31 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 32 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 33 + 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7], 34 + "height":16, 35 + "width":16, 36 + "x":16, 37 + "y":16 38 + }], 39 + "class":"bar", 40 + "height":16, 41 + "id":1, 42 + "name":"ground", 43 + "opacity":1, 44 + "startx":16, 45 + "starty":16, 46 + "type":"tilelayer", 47 + "visible":true, 48 + "width":16, 49 + "x":0, 50 + "y":0 51 + }, 52 + { 53 + "draworder":"topdown", 54 + "id":2, 55 + "name":"objects", 56 + "objects":[ 57 + { 58 + "height":16, 59 + "id":1, 60 + "name":"pool", 61 + "rotation":0, 62 + "type":"", 63 + "visible":true, 64 + "width":16, 65 + "x":88, 66 + "y":96 67 + }, 68 + { 69 + "height":20, 70 + "id":2, 71 + "name":"", 72 + "rotation":0, 73 + "text": 74 + { 75 + "color":"#241f31", 76 + "fontfamily":"Serif", 77 + "text":"tmz", 78 + "wrap":true 79 + }, 80 + "type":"", 81 + "visible":true, 82 + "width":30, 83 + "x":70, 84 + "y":16 85 + }], 86 + "opacity":1, 87 + "type":"objectgroup", 88 + "visible":true, 89 + "x":0, 90 + "y":0 91 + }, 92 + { 93 + "id":3, 94 + "image":"tilemap.png", 95 + "imageheight":32, 96 + "imagewidth":32, 97 + "name":"tile_image", 98 + "offsetx":1, 99 + "offsety":1, 100 + "opacity":1, 101 + "type":"imagelayer", 102 + "visible":true, 103 + "x":0, 104 + "y":0 105 + }], 106 + "nextlayerid":4, 107 + "nextobjectid":3, 108 + "orientation":"orthogonal", 109 + "properties":[ 110 + { 111 + "name":"beautiful", 112 + "type":"bool", 113 + "value":true 114 + }], 115 + "renderorder":"right-down", 116 + "tiledversion":"1.11.2", 117 + "tileheight":16, 118 + "tilesets":[ 119 + { 120 + "firstgid":1, 121 + "source":"test/source_tileset.tsj" 122 + }, 123 + { 124 + "columns":2, 125 + "firstgid":5, 126 + "image":"tilemap.png", 127 + "imageheight":32, 128 + "imagewidth":32, 129 + "margin":0, 130 + "name":"embedded_tiles", 131 + "spacing":0, 132 + "tilecount":4, 133 + "tileheight":16, 134 + "tilewidth":16 135 + }], 136 + "tilewidth":16, 137 + "type":"map", 138 + "version":"1.10", 139 + "width":30 140 + }
+125
test/maps.zig
··· 1 + test "initFromSlice" { 2 + const test_map = @embedFile("map.tmj"); 3 + 4 + const allocator = std.testing.allocator; 5 + 6 + var map = try Map.initFromSlice(allocator, test_map); 7 + defer map.deinit(allocator); 8 + 9 + try baseTests(map); 10 + } 11 + 12 + test "initFromFile" { 13 + const allocator = std.testing.allocator; 14 + 15 + const test_maps = [_][]const u8{ 16 + "test/map.tmj", 17 + "test/map-base64-none.tmj", 18 + "test/map-base64-gzip.tmj", 19 + "test/map-base64-zlib.tmj", 20 + "test/map-base64-zstd.tmj", 21 + }; 22 + 23 + for (test_maps) |test_map| { 24 + var map = try Map.initFromFile(allocator, test_map); 25 + defer map.deinit(allocator); 26 + 27 + try regularMapTests(map); 28 + } 29 + } 30 + 31 + fn regularMapTests(map: Map) !void { 32 + try baseTests(map); 33 + 34 + try expectEqual(false, map.infinite); 35 + 36 + const layer = map.layers.get("Tile Layer 1").?; 37 + try expectEqual(1, layer.content.tile_layer.data.items[0]); 38 + } 39 + 40 + test "infinite maps" { 41 + const allocator = std.testing.allocator; 42 + 43 + const test_maps = [_][]const u8{ 44 + "test/map-infinite-csv.tmj", 45 + "test/map-infinite-base64-zstd.tmj", 46 + }; 47 + 48 + for (test_maps) |test_map| { 49 + var map = try Map.initFromFile(allocator, test_map); 50 + defer map.deinit(allocator); 51 + 52 + try infiniteMapTests(map); 53 + } 54 + } 55 + 56 + fn infiniteMapTests(map: Map) !void { 57 + try baseTests(map); 58 + 59 + try expectEqual(true, map.infinite); 60 + 61 + const layer = map.layers.get("ground").?; 62 + try expectEqual(7, layer.content.tile_layer.chunks.?[0].data[0]); 63 + } 64 + 65 + fn baseTests(map: Map) !void { 66 + try expectEqual(0xaa, map.background_color.?.a); 67 + try expectEqual(0xbb, map.background_color.?.r); 68 + try expectEqual(0xcc, map.background_color.?.g); 69 + 70 + try expectEqualStrings("bar", map.class.?); 71 + try expectEqual(30, map.height); 72 + try expectEqual(30, map.width); 73 + try expectEqual(.orthogonal, map.orientation); 74 + try expectEqual(16, map.tile_width); 75 + try expectEqual(16, map.tile_height); 76 + 77 + try expectEqual(2, map.tilesets.items.len); 78 + 79 + const tileset = map.tilesets.items[0]; 80 + try expectEqual(1, tileset.first_gid); 81 + try expectEqualStrings("tilemap.png", tileset.image.?); 82 + 83 + const bad_tile = map.getTile(0); 84 + try expectEqual(null, bad_tile); 85 + 86 + const foo = map.getTile(2).?; 87 + try expectEqual(1, foo.id); 88 + try expectEqual(16, foo.x); 89 + try expectEqual(0, foo.y); 90 + 91 + const tile = map.getTile(4).?; 92 + try expectEqual(3, tile.id); 93 + try expectEqual(16, tile.x); 94 + try expectEqual(16, tile.y); 95 + try expectEqual(16, tile.width); 96 + try expectEqual(16, tile.height); 97 + 98 + try expectEqual(tileset.first_gid, tile.tileset.first_gid); 99 + 100 + try expectEqual(480, map.pixelWidth()); 101 + try expectEqual(480, map.pixelHeight()); 102 + } 103 + 104 + test "findObject" { 105 + const allocator = std.testing.allocator; 106 + const test_map = @embedFile("map.tmj"); 107 + 108 + var map = try Map.initFromSlice(allocator, test_map); 109 + defer map.deinit(allocator); 110 + 111 + const object = map.findObject("hello").?; 112 + try expectEqualStrings("hello", object.name); 113 + try expectEqual(70.0, object.x); 114 + try expectEqual(44.0, object.y); 115 + 116 + try expectEqual(null, map.findObject("non_existent")); 117 + } 118 + 119 + const tmz = @import("tmz"); 120 + const Map = tmz.Map; 121 + 122 + const std = @import("std"); 123 + const expectEqualDeep = std.testing.expectEqualDeep; 124 + const expectEqual = std.testing.expectEqual; 125 + const expectEqualStrings = std.testing.expectEqualStrings;
+104
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": "hello", 16 + "visible": true, 17 + "width": 91.4375, 18 + "x": 70, 19 + "y": 44.6353383458647 20 + }, 21 + { 22 + "gid": 16403, 23 + "height": 48, 24 + "id": 2, 25 + "name": "", 26 + "rotation": 0, 27 + "type": "player", 28 + "visible": true, 29 + "width": 32, 30 + "x": 55.2142976793752, 31 + "y": 183.914075980747 32 + }, 33 + { 34 + "height": 19, 35 + "id": 3, 36 + "ellipse": true, 37 + "name": "", 38 + "rotation": 0, 39 + "type": "", 40 + "visible": true, 41 + "width": 91.4375, 42 + "x": 70, 43 + "y": 44.6353383458647 44 + }, 45 + { 46 + "height": 0, 47 + "id": 8, 48 + "name": "polygon", 49 + "polygon": [ 50 + { 51 + "x": 0, 52 + "y": 0 53 + }, 54 + { 55 + "x": 52.043665905052, 56 + "y": 49.5049504950495 57 + }, 58 + { 59 + "x": 77.4308200050775, 60 + "y": -8.8855039350089 61 + } 62 + ], 63 + "rotation": 0, 64 + "type": "", 65 + "visible": true, 66 + "width": 0, 67 + "x": 88.8550393500889, 68 + "y": 248.794110180249 69 + }, 70 + { 71 + "height": 0, 72 + "id": 9, 73 + "name": "", 74 + "polyline": [ 75 + { 76 + "x": 0, 77 + "y": 0 78 + }, 79 + { 80 + "x": 58.3904544300584, 81 + "y": 77.4308200050774 82 + } 83 + ], 84 + "rotation": 0, 85 + "type": "", 86 + "visible": true, 87 + "width": 0, 88 + "x": 302.107133790302, 89 + "y": 126.935770500127 90 + } 91 + ], 92 + "opacity": 1, 93 + "properties": [ 94 + { 95 + "name": "custom", 96 + "type": "int", 97 + "value": 50 98 + } 99 + ], 100 + "type": "objectgroup", 101 + "visible": true, 102 + "x": 0, 103 + "y": 0 104 + }
+58
test/properties.zig
··· 1 + test "Property is parsed correctly" { 2 + const allocator = std.testing.allocator; 3 + const properties_json = @embedFile("properties.json"); 4 + 5 + const parsed_value = try std.json.parseFromSlice(std.json.Value, allocator, properties_json, .{ .ignore_unknown_fields = true }); 6 + defer parsed_value.deinit(); 7 + const managed_properties = try std.json.parseFromValue([]Property, allocator, parsed_value.value, .{ .ignore_unknown_fields = true }); 8 + defer managed_properties.deinit(); 9 + 10 + const json_properties = managed_properties.value; 11 + 12 + var properties: std.ArrayList(Property) = .empty; 13 + for (json_properties) |prop| { 14 + try properties.append(allocator, try Property.fromJson(allocator, prop)); 15 + } 16 + defer { 17 + for (properties.items) |*p| p.deinit(allocator); 18 + properties.deinit(allocator); 19 + } 20 + 21 + const string_prop = properties.items[0]; 22 + try expectEqualStrings("name", string_prop.name); 23 + try expectEqual(.string, string_prop.type); 24 + try expectEqualStrings("game", string_prop.value.string); 25 + 26 + const int_prop = properties.items[1]; 27 + try expectEqualStrings("width", int_prop.name); 28 + try expectEqual(.int, int_prop.type); 29 + try expectEqual(640, int_prop.value.int); 30 + 31 + const float_prop = properties.items[2]; 32 + try expectEqualStrings("scale", float_prop.name); 33 + try expectEqual(.float, float_prop.type); 34 + try expectEqual(1.5, float_prop.value.float); 35 + 36 + const color_prop = properties.items[3]; 37 + try expectEqualStrings("bg", color_prop.name); 38 + try expectEqual(.color, color_prop.type); 39 + try expectEqual(Color{ .a = 0xff, .r = 0xa0, .g = 0xb0, .b = 0xc0 }, color_prop.value.color); 40 + 41 + const bool_prop = properties.items[4]; 42 + try expectEqualStrings("fullscreen", bool_prop.name); 43 + try expectEqual(.bool, bool_prop.type); 44 + try expectEqual(true, bool_prop.value.bool); 45 + 46 + const file_prop = properties.items[5]; 47 + try expectEqualStrings("splashscreen", file_prop.name); 48 + try expectEqual(.file, file_prop.type); 49 + try expectEqualStrings("splashscreen.png", file_prop.value.file); 50 + } 51 + 52 + const tmz = @import("tmz"); 53 + const Property = tmz.Property; 54 + const Color = tmz.Color; 55 + 56 + const std = @import("std"); 57 + const expectEqual = std.testing.expectEqual; 58 + const expectEqualStrings = std.testing.expectEqualStrings;
+1
test/test
··· 1 + ..
+6
test/tests.zig
··· 1 + comptime { 2 + _ = @import("maps.zig"); 3 + _ = @import("layers.zig"); 4 + _ = @import("tilesets.zig"); 5 + _ = @import("properties.zig"); 6 + }
test/tilemap.png

This is a binary file and will not be displayed.

+5
test/tileset.tsj
··· 1 + { 2 + "source": "test/source_tileset.tsj", 3 + "type":"tileset", 4 + "version":"1.10" 5 + }
+53
test/tilesets.zig
··· 1 + test "initFromSlice" { 2 + const test_tileset = @embedFile("tileset.tsj"); 3 + 4 + const allocator = std.testing.allocator; 5 + 6 + var tileset = try Tileset.initFromSlice(allocator, test_tileset); 7 + defer tileset.deinit(allocator); 8 + 9 + try expectEqual(4, tileset.tilecount); 10 + try expectEqual(16, tileset.tile_width); 11 + try expectEqual(16, tileset.tile_height); 12 + try expectEqual(4, tileset.tiles.size); 13 + try expectEqual(1, tileset.first_gid); 14 + try expectEqualStrings("tilemap.png", tileset.image.?); 15 + 16 + const tile_0 = tileset.tiles.get(0).?; 17 + try expectEqual(0, tile_0.id); 18 + try expectEqual(null, tile_0.animation); 19 + try expectEqual(0, tile_0.x); 20 + try expectEqual(0, tile_0.y); 21 + try expectEqual(tileset.tile_width, 16); 22 + 23 + const tile_1 = tileset.tiles.get(1).?; 24 + try expectEqual(1, tile_1.id); 25 + try expectEqual(1 * tileset.tile_width, tile_1.x); 26 + try expectEqual(0, tile_1.y); 27 + 28 + const tile_2 = tileset.tiles.get(2).?; 29 + try expectEqual(2, tile_2.id); 30 + try expectEqual(0, tile_2.x); 31 + try expectEqual(1 * tileset.tile_height, tile_2.y); 32 + 33 + const tile_3 = tileset.tiles.get(3).?; 34 + try expectEqual(3, tile_3.id); 35 + try expectEqual(1 * tileset.tile_width, tile_3.x); 36 + try expectEqual(1 * tileset.tile_height, tile_3.y); 37 + try expectEqual(4, tile_3.animation.?.len); 38 + 39 + const properties = tileset.properties; 40 + try expectEqual(1, properties.size); 41 + 42 + const prop = properties.get("custom").?; 43 + try expectEqualStrings("custom", prop.name); 44 + try expectEqual(.int, prop.type); 45 + try expectEqual(50, prop.value.int); 46 + } 47 + 48 + const tmz = @import("tmz"); 49 + const Tileset = tmz.Tileset; 50 + 51 + const std = @import("std"); 52 + const expectEqual = std.testing.expectEqual; 53 + const expectEqualStrings = std.testing.expectEqualStrings;