diff options
author | sadbeast <sadbeast@sadbeast.com> | 2024-04-15 18:08:28 -0700 |
---|---|---|
committer | sadbeast <sadbeast@sadbeast.com> | 2024-05-18 17:23:35 -0700 |
commit | a4711cad923d6c7480e596685d9dcaefa241fe3b (patch) | |
tree | f0c36edea9b32b8a1825270436868bb020689657 | |
download | tmz-main.tar.gz tmz-main.tar.bz2 |
-rw-r--r-- | .envrc | 7 | ||||
-rw-r--r-- | .gitignore | 9 | ||||
-rw-r--r-- | README.md | 92 | ||||
-rw-r--r-- | build.zig | 57 | ||||
-rw-r--r-- | build.zig.zon | 61 | ||||
-rw-r--r-- | example/build.zig | 71 | ||||
-rw-r--r-- | example/build.zig.zon | 67 | ||||
-rw-r--r-- | example/src/main.zig | 19 | ||||
-rw-r--r-- | example/src/map.tmj | 101 | ||||
-rw-r--r-- | example/src/tiles.png | bin | 0 -> 3572 bytes | |||
-rw-r--r-- | example/src/tiles.tsj | 51 | ||||
-rw-r--r-- | flake.lock | 162 | ||||
-rw-r--r-- | flake.nix | 59 | ||||
-rw-r--r-- | src/test/map-base64-gzip.tmj | 35 | ||||
-rw-r--r-- | src/test/map-base64-none.tmj | 35 | ||||
-rw-r--r-- | src/test/map-base64-zlib.tmj | 35 | ||||
-rw-r--r-- | src/test/map-base64-zstd.tmj | 35 | ||||
-rw-r--r-- | src/test/map-csv.tmj | 37 | ||||
-rw-r--r-- | src/test/map-infinite-base64-zstd.tmj | 137 | ||||
-rw-r--r-- | src/test/map-infinite-csv.tmj | 195 | ||||
-rw-r--r-- | src/test/map.tmj | 107 | ||||
-rw-r--r-- | src/test/tiles.png | bin | 0 -> 3572 bytes | |||
-rw-r--r-- | src/test/tiles.tsj | 126 | ||||
-rw-r--r-- | src/tmz.zig | 551 | ||||
-rw-r--r-- | src/tmz_test.zig | 117 |
25 files changed, 2166 insertions, 0 deletions
@@ -0,0 +1,7 @@ +#dotenv + +if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4=" +fi + +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb45df7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# direnv +.direnv + +# Zig +zig-cache +zig-out + +# kcov +kcov-output diff --git a/README.md b/README.md new file mode 100644 index 0000000..11c379f --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# tmz + +A library for parsing and loading [Tiled](https://www.mapeditor.org/) [JSON Maps](https://doc.mapeditor.org/en/stable/reference/json-map-format/#map) and [JSON Tilesets](https://doc.mapeditor.org/en/stable/reference/json-map-format/#tileset) in [Zig](https://ziglang.org/). + +```zig +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +const allocator = gpa.allocator(); + +const parsed_map = try tmz.parseMap(allocator, @embedFile("map.tmj")); +defer parsed_map.deinit(); + +const map = parsed_map.value; +std.debug.print("Map size: {}x{}, {} layers.\n", .{ map.height, map.width, map.layers.len }); +``` + +## Use in your own projects + +1. Add tmz to your `build.zig.zon` + +``` +zig fetch --save https://git.sadbeast.com/tmz/snapshot/tmz-main.tar.gz +``` +or manually: + +```zig +.dependencies = .{ + .tmz = .{ + .url = "https://git.sadbeast.com/tmz/snapshot/tmz-main.tar.gz", + .hash = "122049a7810f7bd222fc2d65204c85d0cf867ab03063bb11c8086b452092d9f1a4b5", + }, +}, +``` + +2. Add tmz to your `build.zig` + +```zig +const tmz = b.dependency("tmz", .{ + .target = target, + .optimize = optimize, +}); + +exe.root_module.addImport("tmz", tmz.module("tmz")); +``` + +2. Import module and parse a .tmj file + +```zig +const std = @import("std"); +const tmz = @import("tmz"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const parsed_map = try tmz.parseMap(allocator, @embedFile("map.tmj")); + defer parsed_map.deinit(); + + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Map: {any}\n", .{parsed_map.value}); + + try bw.flush(); +} +``` + + +## Documentation + +[Generated docs](https://sadbeast.com/tmz/docs/main) + +## Building + +There are no executables or a library yet. To test: + +`zig build test` + +`zig build test -Dcover` to generate coverage in `zig-out/coverage` + +### Dependencies + +* [Zig](https://ziglang.org/) 0.12.0 +* [kcov](http://simonkagstrom.github.io/kcov/index.html) (optional - for test coverage) + +### Nix + +If you have Nix with flakes enabled, run `nix develop` to get a development shell with above dependencies. If you also have direnv, run `direnv allow` to do this automatically. + +## Contributing + +[Email patches](https://git-send-email.io/) to [sadbeast@sadbeast.com](mailto:sadbeast@sadbeast.com) diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..15e51a4 --- /dev/null +++ b/build.zig @@ -0,0 +1,57 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + + const optimize = b.standardOptimizeOption(.{}); + + const root_source_file = b.path("src/tmz.zig"); + + _ = b.addModule("tmz", .{ .root_source_file = root_source_file }); + + const tests = b.addTest(.{ + .root_source_file = root_source_file, + .target = target, + .optimize = optimize, + }); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&b.addRunArtifact(tests).step); + + const coverage = b.option(bool, "cover", "Generate test coverage") orelse false; + + if (coverage) { + const coverage_output_dir = b.makeTempPath(); + + const args = &[_]std.Build.Step.Run.Arg{ + .{ .bytes = b.dupe("kcov") }, + .{ .bytes = b.dupe("--collect-only") }, + .{ .bytes = b.dupe("--include-pattern=src/") }, + .{ .bytes = b.dupe("--exclude-pattern=_test") }, + .{ .bytes = b.dupe(coverage_output_dir) }, + }; + + const tests_run = b.addRunArtifact(tests); + tests_run.has_side_effects = true; + + tests_run.argv.insertSlice(0, args) catch @panic("OOM"); + + const merge_step = std.Build.Step.Run.create(b, "merge kcov"); + merge_step.has_side_effects = true; + merge_step.addArgs(&.{ + "kcov", + "--merge", + b.pathJoin(&.{ coverage_output_dir, "output" }), + b.pathJoin(&.{ coverage_output_dir, "test" }), + }); + merge_step.step.dependOn(&tests_run.step); + + const install_coverage = b.addInstallDirectory(.{ + .source_dir = .{ .cwd_relative = b.pathJoin(&.{ coverage_output_dir, "output" }) }, + .install_dir = .{ .custom = "coverage" }, + .install_subdir = "", + }); + install_coverage.step.dependOn(&merge_step.step); + test_step.dependOn(&install_coverage.step); + } +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..a69f12b --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,61 @@ +.{ + .name = "tmz", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.1.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + .minimum_zig_version = "0.12.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save <url>` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + //"LICENSE", + "README.md", + }, +} diff --git a/example/build.zig b/example/build.zig new file mode 100644 index 0000000..5563386 --- /dev/null +++ b/example/build.zig @@ -0,0 +1,71 @@ +const std = @import("std"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "example", + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const tmz = b.addModule("tmz", .{ .root_source_file = b.path("../src/tmz.zig") }); + exe.root_module.addImport("tmz", tmz); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const exe_unit_tests = b.addTest(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }); + + const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_exe_unit_tests.step); +} diff --git a/example/build.zig.zon b/example/build.zig.zon new file mode 100644 index 0000000..df70c89 --- /dev/null +++ b/example/build.zig.zon @@ -0,0 +1,67 @@ +.{ + .name = "game", + // This is a [Semantic Version](https://semver.org/). + // In a future version of Zig it will be used for package deduplication. + .version = "0.0.0", + + // This field is optional. + // This is currently advisory only; Zig does not yet do anything + // with this value. + //.minimum_zig_version = "0.11.0", + + // This field is optional. + // Each dependency must either provide a `url` and `hash`, or a `path`. + // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. + // Once all dependencies are fetched, `zig build` no longer requires + // internet connectivity. + .dependencies = .{ + // See `zig fetch --save <url>` for a command-line interface for adding dependencies. + //.example = .{ + // // When updating this field to a new URL, be sure to delete the corresponding + // // `hash`, otherwise you are communicating that you expect to find the old hash at + // // the new URL. + // .url = "https://example.com/foo.tar.gz", + // + // // This is computed from the file contents of the directory of files that is + // // obtained after fetching `url` and applying the inclusion rules given by + // // `paths`. + // // + // // This field is the source of truth; packages do not come from a `url`; they + // // come from a `hash`. `url` is just one of many possible mirrors for how to + // // obtain a package matching this `hash`. + // // + // // Uses the [multihash](https://multiformats.io/multihash/) format. + // .hash = "...", + // + // // When this is provided, the package is found in a directory relative to the + // // build root. In this case the package's hash is irrelevant and therefore not + // // computed. This field and `url` are mutually exclusive. + // .path = "foo", + + // // When this is set to `true`, a package is declared to be lazily + // // fetched. This makes the dependency only get fetched if it is + // // actually used. + // .lazy = false, + //}, + }, + + // Specifies the set of files and directories that are included in this package. + // Only files and directories listed here are included in the `hash` that + // is computed for this package. + // Paths are relative to the build root. Use the empty string (`""`) to refer to + // the build root itself. + // A directory listed here means that all files within, recursively, are included. + .paths = .{ + // This makes *all* files, recursively, included in this package. It is generally + // better to explicitly list the files and directories instead, to insure that + // fetching from tarballs, file system paths, and version control all result + // in the same contents hash. + "", + // For example... + //"build.zig", + //"build.zig.zon", + //"src", + //"LICENSE", + //"README.md", + }, +} diff --git a/example/src/main.zig b/example/src/main.zig new file mode 100644 index 0000000..44fa08f --- /dev/null +++ b/example/src/main.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const tmz = @import("tmz"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + defer _ = gpa.deinit(); + + const parsed_map = try tmz.parseMap(allocator, @embedFile("map.tmj")); + defer parsed_map.deinit(); + + const stdout_file = std.io.getStdOut().writer(); + var bw = std.io.bufferedWriter(stdout_file); + const stdout = bw.writer(); + + try stdout.print("Map: {any}\n", .{parsed_map.value}); + + try bw.flush(); // don't forget to flush! +} diff --git a/example/src/map.tmj b/example/src/map.tmj new file mode 100644 index 0000000..d8e0888 --- /dev/null +++ b/example/src/map.tmj @@ -0,0 +1,101 @@ +{ "class":"bar", + "compressionlevel":-1, + "height":30, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"zstd", + "data":"KLUv\/WAADo0WAOLOIhewJekMw4AmSQqb4zwHkkxSSmknYWS3FL3tKaVakYricx+o1U0H6HiOrIpawkr20Ux75XaikAGvO8JVvSFVRWuob50v1ZNByLH9iAJCBkQmKGTQgZDD\/WGhIIdCDjO2j9CPWlvYpqHXVGnohm1sxA6qOnn4WvVpVV3oXLbp3WZeyPtZjbWonpMkkg+BU6jB6QvNAaEEyChHMWcHEZCIwJGEiChKBs0ag\/WoKoS5gp76\/p+G5cawdf2AkXsGPwUnt+jgPJ7zhrn26D9Aeus6Hyyf6BNbJaeIEfzi8uPDXHvFbzPJALzjJyR7H9sz5+4BuY89HqnnOJ6YyUnYVfZjDPgrOc29sHaf911o19ny2Yo58M+3Ee1LTfmVdy836PBu\/4kzFZMp\/4Y0PPPvTF9qH4AaMV9IHKfF7C2h4vr\/SDtjE30QBqzZ3iPh7svCD7wNB\/2ny34\/E8yAS8tQezmPVwbhvqPAQdCy4pCyFt6Tdp9r2v4Xx2CcloneN8Qv9sWDW9z+N3sUXpk9skLP2xL3T7JzZv3wBHjSlj3P3Ptr6QNxfwVWFFdkMJVZ5IPo31F8cD\/0\/vp5+nuMePDMtP1JJ\/6K\/14vYucn32yTSW8XH7kc787uXO+RwXmDw4ryMw+TVHYe\/\/3sQO2OeB0nPV0PBm1YpvDFOnri3bbBlrnF4cD\/aPaKOOeHpJ\/t5aFunfuFD57AhmcPMaptd5vEwfQL6LJOfBdHuN5czo+tQLzpP4r9aQ1dBmj0+7vxZ3IDeW4W\/uHxA0krKXi2Bj6Eo32cd0qD1yicts8yzwZU9uivOzf9+26Mwzc6j7d0dCsWYEWJXcZk8QCbKY3OH8Po\/SavNPGeOx8sxt3JgHPU20x24EAb\/MJ3YD8BjZhLnBznCu\/3v3Y\/g6PwQb+W8hNPenxROT6V3WnwE5\/PoXjgT9hhuOqtPPNXuvlotZ1rLng1VAE=", + "encoding":"base64", + "height":30, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":32, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"pool", + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":88, + "y":96 + }, + { + "height":20, + "id":2, + "name":"", + "rotation":0, + "text": + { + "color":"#241f31", + "fontfamily":"Serif", + "text":"tmz", + "wrap":true + }, + "type":"", + "visible":true, + "width":30, + "x":70, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":3, + "image":"tiles.png", + "name":"tile_image", + "offsetx":1, + "offsety":1, + "opacity":1, + "type":"imagelayer", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/tiles.tsj" + }, + { + "columns":4, + "firstgid":17, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"embedded_tiles", + "spacing":0, + "tilecount":16, + "tileheight":8, + "tilewidth":8 + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":32 +} diff --git a/example/src/tiles.png b/example/src/tiles.png Binary files differnew file mode 100644 index 0000000..499259c --- /dev/null +++ b/example/src/tiles.png diff --git a/example/src/tiles.tsj b/example/src/tiles.tsj new file mode 100644 index 0000000..4e0ca0d --- /dev/null +++ b/example/src/tiles.tsj @@ -0,0 +1,51 @@ +{ "columns":4, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":16, + "tiledversion":"1.10.2", + "tileheight":8, + "tiles":[ + { + "id":8, + "probability":3 + }, + { + "id":14, + "probability":0.25 + }], + "tilewidth":8, + "type":"tileset", + "version":"1.10", + "wangsets":[ + { + "colors":[ + { + "color":"#ff0000", + "name":"", + "probability":1, + "tile":-1 + }, + { + "color":"#00ff00", + "name":"", + "probability":1, + "tile":-1 + }], + "name":"corners", + "tile":-1, + "type":"corner", + "wangtiles":[ + { + "tileid":0, + "wangid":[0, 1, 0, 1, 0, 1, 0, 1] + }, + { + "tileid":1, + "wangid":[0, 2, 0, 2, 0, 2, 0, 2] + }] + }] +}
\ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..b27a8da --- /dev/null +++ b/flake.lock @@ -0,0 +1,162 @@ +{ + "nodes": { + "devshell": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1713532798, + "narHash": "sha256-wtBhsdMJA3Wa32Wtm1eeo84GejtI43pMrFrmwLXrsEc=", + "owner": "numtide", + "repo": "devshell", + "rev": "12e914740a25ea1891ec619bb53cf5e6ca922e40", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1714641030, + "narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1704161960, + "narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "63143ac2c9186be6d9da6035fa22620018c85932", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1714640452, + "narHash": "sha256-QBx10+k6JWz6u7VsohfSw8g8hjdBZEf8CFzXH1/1Z94=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1715266358, + "narHash": "sha256-doPgfj+7FFe9rfzWo1siAV2mVCasW+Bh8I1cToAXEE4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "f1010e0469db743d14519a1efd37e23f8513d714", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1708475490, + "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "0e74ca98a74bc7270d28838369593635a5db3260", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devshell": "devshell", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs_2", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1714058656, + "narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..92f81f5 --- /dev/null +++ b/flake.nix @@ -0,0 +1,59 @@ +{ + description = "tmz - a library for loading Tiled Maps and Tilesets"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + + devshell.url = "github:numtide/devshell"; + treefmt-nix.url = "github:numtide/treefmt-nix"; + }; + + outputs = inputs @ {flake-parts, ...}: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + inputs.devshell.flakeModule + inputs.treefmt-nix.flakeModule + ]; + systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; + perSystem = { + config, + self', + inputs', + pkgs, + system, + ... + }: { + treefmt.config = { + projectRootFile = "flake.nix"; + + flakeCheck = true; + flakeFormatter = true; + + programs.zig.enable = true; + programs.alejandra.enable = true; + }; + + devshells.default = { + commands = [ + { + name = "fmt"; + help = "format the repo"; + command = "nix fmt"; + } + { + name = "docs"; + help = "generate auto docs"; + command = "zig test -femit-docs src/tmz.zig"; + } + ]; + + packages = [ + pkgs.zig_0_12 + pkgs.kcov + ]; + }; + }; + }; +} diff --git a/src/test/map-base64-gzip.tmj b/src/test/map-base64-gzip.tmj new file mode 100644 index 0000000..3fa4154 --- /dev/null +++ b/src/test/map-base64-gzip.tmj @@ -0,0 +1,35 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"gzip", + "data":"H4sIAAAAAAAAA+NiYGDgB2JWIGZjYDgAxA6sUD5UrAGIGZiBcsxockB+AzOUDTJDAEmOFcrnBmIAadKM8GQAAAA=", + "encoding":"base64", + "height":5, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":5 +} diff --git a/src/test/map-base64-none.tmj b/src/test/map-base64-none.tmj new file mode 100644 index 0000000..a4a92d6 --- /dev/null +++ b/src/test/map-base64-none.tmj @@ -0,0 +1,35 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"", + "data":"CgAAAA8AAAAFAAAABgAAwAYAAEAFAAAABQAAAAUAAAAGAACABgAAAAMAAMADAABABQAAAAUAAAAFAAAAAwAAgAMAAAAFAAAADwAAABAAAAAFAAAABQAAAAUAAAAQAAAACwAAAA==", + "encoding":"base64", + "height":5, + "id":1, + "name":"base64-ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":5 +} diff --git a/src/test/map-base64-zlib.tmj b/src/test/map-base64-zlib.tmj new file mode 100644 index 0000000..698dabd --- /dev/null +++ b/src/test/map-base64-zlib.tmj @@ -0,0 +1,35 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"zlib", + "data":"eJzjYmBg4AdiViBmY2A4AMQOrFA+VKwBiBmYgXLMaHJAfgMzlA0yQwBJjhXK5wZiAOC8A68=", + "encoding":"base64", + "height":5, + "id":1, + "name":"base64-ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":5 +} diff --git a/src/test/map-base64-zstd.tmj b/src/test/map-base64-zstd.tmj new file mode 100644 index 0000000..70be286 --- /dev/null +++ b/src/test/map-base64-zstd.tmj @@ -0,0 +1,35 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"zstd", + "data":"KLUv\/SBkFQIA1AIKAAAADwAAAAUAAAAGAADABgAAQAWABgAAAAMAAMADAwAAgAMQEAAAAAsAAAAHACDAj92BQ3jJ9M+ONzM42WQC", + "encoding":"base64", + "height":5, + "id":1, + "name":"base64-ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":5 +} diff --git a/src/test/map-csv.tmj b/src/test/map-csv.tmj new file mode 100644 index 0000000..4241017 --- /dev/null +++ b/src/test/map-csv.tmj @@ -0,0 +1,37 @@ +{ "compressionlevel":-1, + "height":5, + "infinite":false, + "layers":[ + { + "class":"bar", + "data":[10, 15, 5, 3221225478, 1073741830, + 5, 5, 5, 2147483654, 6, + 3221225475, 1073741827, 5, 5, 5, + 2147483651, 3, 5, 15, 16, + 5, 5, 5, 16, 11], + "height":5, + "id":1, + "name":"base64-ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":5, + "x":0, + "y":0 + }], + "nextlayerid":2, + "nextobjectid":1, + "orientation":"orthogonal", + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":5 +} diff --git a/src/test/map-infinite-base64-zstd.tmj b/src/test/map-infinite-base64-zstd.tmj new file mode 100644 index 0000000..ea864c6 --- /dev/null +++ b/src/test/map-infinite-base64-zstd.tmj @@ -0,0 +1,137 @@ +{ "class":"bar", + "compressionlevel":-1, + "height":30, + "infinite":true, + "layers":[ + { + "chunks":[ + { + "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", + "height":16, + "width":16, + "x":0, + "y":0 + }, + { + "data":"KLUv\/WAAA80GAHKECw\/gaQwAIABQYJJJpjQkngI88BOQ6uvWSdXibJAo7pq11qIPts0JBskYK0OSJXlhqNC44XCCkak7DxGgiECShKgWxJY5v59BPzPfmxfuVoU9TdRy8Onzd4QOmw7JvPYnCmAY3ZAvRZcH43ZHDPllarkdzvPQg4a5IfFJc5yOEIec8Ui+mg18NPthM60tgTyyd6XeXz19Hsz0eFAHIF4XPAn3d53gj0\/2bCHcHrv7MC\/3xqNSfs4D3aTNOiAP8HstjnS45vrC+84HcMRY4rNb04PsbNJQkAE=", + "height":16, + "width":16, + "x":16, + "y":0 + }, + { + "data":"KLUv\/WAAA7UGACKECw\/gGQ4AoKAGHsqUpEw3Hyk2RxbCkZhs\/pVpXz7QjKkRbIyqLUWs7b5MgfDYRgFYqND4YlCCIURpHhIAEkBgrGEAUTCvuf5\/B5070Y5yrvUw3SjPgje6q9+wPTUdHb0F+YTtaAbG9wB0xUUOtxfrNUk70nPsnA68ilkrdudLbPKZD3DTlEGP95KHzQb+oqO9twvrmY+UwTEjHyg3iGboEx+7k5zzR34v2tHz6Cl2t\/t8Ocx1t99FTcWJ\/PfMzOi7dFT++cH9Yk8+0zsXT11wAPgMmCk=", + "height":16, + "width":16, + "x":0, + "y":16 + }, + { + "data":"KLUv\/WAAAzUHACLFDhWwJekMwwAkSTKLw58Dcsu99w5V21Itkg0GR6izwFzHX1GZFDnSivECOgrOJZIOO\/z98gOGn79+0m8BW6jh7QrtvwFwgkJTVg8SYEQEeI0QlhFJStWy\/3edmqHGgenHjtXxDFEaOO52o4Q\/TH4P4gXD5umLBjn5zpEvE+4M\/cTB5J0wq0YzgG5jY\/38IqFB7xncopxvbGDnj+5rHhDOsvza1k8aXHGBH5QXkkwc5M\/vzYcO1\/e2tPk\/8Qytk82H9osw+rbcw\/o9VKZjvvNxLTsNR7AEzRxy0VH2rQ8A+H3PnmA1", + "height":16, + "width":16, + "x":16, + "y":16 + }], + "class":"bar", + "compression":"zstd", + "encoding":"base64", + "height":32, + "id":1, + "name":"ground", + "opacity":1, + "startx":0, + "starty":0, + "type":"tilelayer", + "visible":true, + "width":32, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"pool", + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":88, + "y":96 + }, + { + "height":20, + "id":2, + "name":"", + "rotation":0, + "text": + { + "color":"#241f31", + "fontfamily":"Serif", + "text":"tmz", + "wrap":true + }, + "type":"", + "visible":true, + "width":30, + "x":70, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":3, + "image":"tiles.png", + "name":"tile_image", + "offsetx":1, + "offsety":1, + "opacity":1, + "type":"imagelayer", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"beautiful", + "type":"bool", + "value":true + }], + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }, + { + "columns":4, + "firstgid":17, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"embedded_tiles", + "spacing":0, + "tilecount":16, + "tileheight":8, + "tilewidth":8 + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":32 +} diff --git a/src/test/map-infinite-csv.tmj b/src/test/map-infinite-csv.tmj new file mode 100644 index 0000000..1889fee --- /dev/null +++ b/src/test/map-infinite-csv.tmj @@ -0,0 +1,195 @@ +{ "class":"bar", + "compressionlevel":-1, + "height":30, + "infinite":true, + "layers":[ + { + "chunks":[ + { + "data":[16, 11, 16, 16, 16, 16, 16, 16, 10, 10, 16, 14, 16, 16, 10, 16, + 16, 11, 11, 13, 14, 10, 11, 12, 16, 11, 11, 10, 13, 14, 13, 16, + 16, 14, 13, 10, 12, 16, 16, 11, 10, 16, 9, 13, 11, 11, 10, 16, + 10, 16, 16, 9, 13, 9, 15, 9, 14, 15, 9, 12, 13, 11, 13, 14, + 11, 9, 14, 9, 9, 9, 13, 12, 9, 13, 10, 16, 9, 16, 9, 12, + 13, 16, 12, 9, 12, 9, 12, 9, 9, 9, 12, 11, 16, 9, 16, 15, + 12, 11, 14, 9, 16, 11, 10, 16, 12, 16, 9, 16, 12, 14, 16, 11, + 14, 11, 14, 15, 11, 9, 9, 13, 9, 13, 16, 16, 14, 16, 13, 16, + 9, 11, 9, 10, 14, 9, 9, 11, 13, 9, 13, 13, 16, 16, 9, 12, + 12, 9, 10, 10, 11, 16, 16, 10, 9, 10, 10, 12, 9, 13, 10, 11, + 11, 16, 12, 16, 14, 13, 16, 1073741834, 9, 15, 9, 12, 16, 12, 9, 9, + 11, 11, 15, 16, 13, 9, 9, 13, 11, 13, 5, 5, 5, 5, 10, 13, + 16, 11, 9, 13, 10, 10, 16, 14, 10, 15, 5, 3221225478, 1073741830, 5, 13, 10, + 16, 13, 14, 13, 16, 9, 14, 5, 5, 5, 5, 2147483654, 6, 5, 10, 12, + 16, 9, 9, 14, 16, 9, 14, 5, 3221225475, 1073741827, 5, 5, 5, 5, 12, 11, + 16, 9, 14, 11, 9, 13, 10, 5, 2147483651, 3, 5, 15, 16, 10, 12, 13], + "height":16, + "width":16, + "x":0, + "y":0 + }, + { + "data":[16, 16, 16, 16, 9, 10, 16, 16, 16, 11, 11, 10, 12, 16, 11, 9, + 16, 16, 16, 11, 15, 12, 16, 16, 9, 16, 3221225487, 3221225487, 16, 16, 16, 9, + 9, 16, 11, 16, 13, 15, 15, 15, 9, 3221225487, 3221225487, 16, 3221225487, 11, 16, 16, + 14, 10, 13, 15, 14, 11, 14, 3221225487, 14, 16, 16, 10, 10, 12, 16, 16, + 11, 14, 12, 9, 12, 9, 13, 16, 16, 10, 14, 16, 9, 9, 11, 16, + 13, 12, 13, 16, 12, 14, 9, 9, 12, 2, 2, 2, 9, 9, 14, 16, + 12, 9, 13, 12, 2, 2, 2, 2, 2, 2, 16, 16, 2, 9, 16, 9, + 10, 12, 11, 13, 2, 14, 12, 2, 2, 11, 16, 11, 2, 9, 15, 9, + 12, 16, 14, 2, 2, 9, 10, 2, 2, 16, 10, 13, 2, 12, 15, 16, + 10, 10, 11, 14, 2, 16, 9, 14, 13, 14, 14, 2, 2, 14, 9, 16, + 9, 12, 16, 13, 2, 14, 9, 9, 11, 11, 16, 2, 9, 9, 11, 16, + 9, 16, 14, 15, 2, 2, 13, 12, 14, 16, 2, 2, 16, 14, 9, 11, + 14, 11, 10, 10, 11, 2, 9, 13, 16, 2, 2, 9, 14, 14, 9, 16, + 9, 13, 12, 9, 9, 2, 2, 9, 9, 2, 10, 13, 9, 16, 15, 12, + 13, 10, 16, 2147483663, 11, 15, 2, 2, 2, 2, 13, 16, 16, 12, 16, 14, + 16, 9, 16, 2147483663, 16, 9, 11, 2, 2, 11, 15, 12, 14, 9, 16, 9], + "height":16, + "width":16, + "x":16, + "y":0 + }, + { + "data":[16, 16, 9, 9, 15, 3221225487, 16, 5, 5, 5, 5, 16, 11, 11, 13, 15, + 12, 12, 15, 13, 14, 3221225487, 2147483663, 16, 16, 12, 10, 16, 16, 2147483663, 12, 9, + 14, 10, 14, 9, 12, 12, 16, 2147483663, 11, 13, 14, 12, 2147483663, 9, 16, 1073741834, + 12, 10, 12, 12, 9, 16, 9, 2147483663, 15, 16, 10, 14, 10, 11, 16, 16, + 16, 12, 16, 10, 16, 16, 9, 12, 14, 12, 14, 9, 14, 10, 14, 9, + 13, 9, 16, 9, 16, 16, 9, 13, 11, 11, 16, 14, 10, 3221225483, 15, 9, + 13, 10, 10, 9, 16, 9, 16, 11, 10, 13, 12, 16, 2147483663, 11, 15, 11, + 16, 11, 9, 9, 12, 14, 11, 16, 9, 16, 9, 10, 13, 10, 13, 13, + 16, 11, 12, 11, 10, 13, 13, 10, 10, 9, 12, 15, 16, 12, 9, 11, + 16, 11, 10, 9, 11, 15, 10, 15, 10, 11, 16, 10, 13, 15, 14, 3221225487, + 16, 9, 13, 11, 9, 11, 14, 14, 9, 16, 16, 16, 16, 12, 11, 14, + 16, 11, 9, 12, 16, 16, 9, 9, 14, 16, 16, 14, 15, 10, 10, 15, + 12, 15, 11, 16, 9, 13, 16, 15, 14, 15, 12, 13, 13, 13, 14, 10, + 16, 16, 16, 10, 12, 16, 16, 10, 13, 10, 14, 10, 10, 14, 13, 16, + 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, 0, 0], + "height":16, + "width":16, + "x":0, + "y":16 + }, + { + "data":[13, 11, 11, 13, 14, 12, 2147483658, 2, 9, 2147483663, 2147483663, 11, 16, 9, 15, 9, + 14, 9, 10, 11, 16, 11, 9, 12, 14, 9, 9, 11, 11, 11, 16, 9, + 12, 14, 11, 12, 11, 16, 10, 2147483658, 2147483658, 9, 15, 10, 16, 9, 14, 16, + 34, 34, 34, 34, 34, 14, 10, 2147483663, 2147483658, 14, 11, 10, 11, 9, 15, 9, + 34, 3221225511, 3221225511, 3221225511, 34, 16, 2147483663, 10, 2147483658, 16, 11, 16, 16, 16, 11, 9, + 34, 3221225511, 34, 3221225511, 34, 9, 9, 10, 9, 11, 16, 16, 16, 16, 9, 10, + 34, 3221225511, 3221225511, 3221225511, 34, 14, 16, 9, 14, 14, 16, 16, 16, 11, 12, 13, + 34, 34, 34, 34, 34, 9, 13, 13, 9, 9, 12, 9, 13, 16, 12, 11, + 16, 10, 11, 14, 11, 15, 10, 12, 16, 16, 16, 3221225487, 3221225487, 16, 13, 15, + 13, 15, 12, 9, 13, 10, 16, 10, 9, 9, 16, 8, 8, 8, 8, 8, + 13, 14, 13, 12, 15, 13, 12, 12, 16, 16, 9, 8, 1, 1, 1, 1, + 15, 10, 13, 12, 14, 15, 16, 16, 3221225483, 3221225483, 3221225483, 8, 1, 1073741825, 1073741825, 1, + 9, 15, 9, 13, 11, 13, 9, 10, 3221225487, 3221225487, 11, 8, 1, 1073741825, 1073741825, 1, + 14, 9, 14, 12, 16, 13, 16, 16, 14, 9, 9, 8, 1, 1073741825, 1073741825, 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, 0, 0, 0], + "height":16, + "width":16, + "x":16, + "y":16 + }], + "class":"bar", + "height":32, + "id":1, + "name":"ground", + "opacity":1, + "startx":0, + "starty":0, + "type":"tilelayer", + "visible":true, + "width":32, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"pool", + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":88, + "y":96 + }, + { + "height":20, + "id":2, + "name":"", + "rotation":0, + "text": + { + "color":"#241f31", + "fontfamily":"Serif", + "text":"tmz", + "wrap":true + }, + "type":"", + "visible":true, + "width":30, + "x":70, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":3, + "image":"tiles.png", + "name":"tile_image", + "offsetx":1, + "offsety":1, + "opacity":1, + "type":"imagelayer", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"beautiful", + "type":"bool", + "value":true + }], + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }, + { + "columns":4, + "firstgid":17, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"embedded_tiles", + "spacing":0, + "tilecount":16, + "tileheight":8, + "tilewidth":8 + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":32 +} diff --git a/src/test/map.tmj b/src/test/map.tmj new file mode 100644 index 0000000..4414387 --- /dev/null +++ b/src/test/map.tmj @@ -0,0 +1,107 @@ +{ "class":"bar", + "compressionlevel":-1, + "height":30, + "infinite":false, + "layers":[ + { + "class":"bar", + "compression":"zstd", + "data":"KLUv\/WAADo0WAOLOIhewJekMw4AmSQqb4zwHkkxSSmknYWS3FL3tKaVakYricx+o1U0H6HiOrIpawkr20Ux75XaikAGvO8JVvSFVRWuob50v1ZNByLH9iAJCBkQmKGTQgZDD\/WGhIIdCDjO2j9CPWlvYpqHXVGnohm1sxA6qOnn4WvVpVV3oXLbp3WZeyPtZjbWonpMkkg+BU6jB6QvNAaEEyChHMWcHEZCIwJGEiChKBs0ag\/WoKoS5gp76\/p+G5cawdf2AkXsGPwUnt+jgPJ7zhrn26D9Aeus6Hyyf6BNbJaeIEfzi8uPDXHvFbzPJALzjJyR7H9sz5+4BuY89HqnnOJ6YyUnYVfZjDPgrOc29sHaf911o19ny2Yo58M+3Ee1LTfmVdy836PBu\/4kzFZMp\/4Y0PPPvTF9qH4AaMV9IHKfF7C2h4vr\/SDtjE30QBqzZ3iPh7svCD7wNB\/2ny34\/E8yAS8tQezmPVwbhvqPAQdCy4pCyFt6Tdp9r2v4Xx2CcloneN8Qv9sWDW9z+N3sUXpk9skLP2xL3T7JzZv3wBHjSlj3P3Ptr6QNxfwVWFFdkMJVZ5IPo31F8cD\/0\/vp5+nuMePDMtP1JJ\/6K\/14vYucn32yTSW8XH7kc787uXO+RwXmDw4ryMw+TVHYe\/\/3sQO2OeB0nPV0PBm1YpvDFOnri3bbBlrnF4cD\/aPaKOOeHpJ\/t5aFunfuFD57AhmcPMaptd5vEwfQL6LJOfBdHuN5czo+tQLzpP4r9aQ1dBmj0+7vxZ3IDeW4W\/uHxA0krKXi2Bj6Eo32cd0qD1yicts8yzwZU9uivOzf9+26Mwzc6j7d0dCsWYEWJXcZk8QCbKY3OH8Po\/SavNPGeOx8sxt3JgHPU20x24EAb\/MJ3YD8BjZhLnBznCu\/3v3Y\/g6PwQb+W8hNPenxROT6V3WnwE5\/PoXjgT9hhuOqtPPNXuvlotZ1rLng1VAE=", + "encoding":"base64", + "height":30, + "id":1, + "name":"ground", + "opacity":1, + "type":"tilelayer", + "visible":true, + "width":32, + "x":0, + "y":0 + }, + { + "draworder":"topdown", + "id":2, + "name":"objects", + "objects":[ + { + "height":16, + "id":1, + "name":"pool", + "rotation":0, + "type":"", + "visible":true, + "width":16, + "x":88, + "y":96 + }, + { + "height":20, + "id":2, + "name":"", + "rotation":0, + "text": + { + "color":"#241f31", + "fontfamily":"Serif", + "text":"tmz", + "wrap":true + }, + "type":"", + "visible":true, + "width":30, + "x":70, + "y":16 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + }, + { + "id":3, + "image":"tiles.png", + "name":"tile_image", + "offsetx":1, + "offsety":1, + "opacity":1, + "type":"imagelayer", + "visible":true, + "x":0, + "y":0 + }], + "nextlayerid":4, + "nextobjectid":3, + "orientation":"orthogonal", + "properties":[ + { + "name":"beautiful", + "type":"bool", + "value":true + }], + "renderorder":"right-down", + "tiledversion":"1.10.2", + "tileheight":8, + "tilesets":[ + { + "firstgid":1, + "source":"src/test/tiles.tsj" + }, + { + "columns":4, + "firstgid":17, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"embedded_tiles", + "spacing":0, + "tilecount":16, + "tileheight":8, + "tilewidth":8 + }], + "tilewidth":8, + "type":"map", + "version":"1.10", + "width":32 +} diff --git a/src/test/tiles.png b/src/test/tiles.png Binary files differnew file mode 100644 index 0000000..499259c --- /dev/null +++ b/src/test/tiles.png diff --git a/src/test/tiles.tsj b/src/test/tiles.tsj new file mode 100644 index 0000000..53b9476 --- /dev/null +++ b/src/test/tiles.tsj @@ -0,0 +1,126 @@ +{ "columns":4, + "image":"tiles.png", + "imageheight":32, + "imagewidth":32, + "margin":0, + "name":"tiles", + "spacing":0, + "tilecount":16, + "tiledversion":"1.10.2", + "tileheight":8, + "tiles":[ + { + "id":6, + "objectgroup": + { + "draworder":"index", + "name":"", + "objects":[ + { + "height":7, + "id":1, + "name":"", + "rotation":0, + "type":"", + "visible":true, + "width":7, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + } + }, + { + "id":7, + "objectgroup": + { + "draworder":"index", + "name":"", + "objects":[ + { + "height":8, + "id":1, + "name":"", + "rotation":0, + "type":"", + "visible":true, + "width":8, + "x":0, + "y":0 + }], + "opacity":1, + "type":"objectgroup", + "visible":true, + "x":0, + "y":0 + } + }, + { + "animation":[ + { + "duration":200, + "tileid":8 + }, + { + "duration":200, + "tileid":10 + }, + { + "duration":200, + "tileid":9 + }, + { + "duration":200, + "tileid":11 + }], + "id":8 + }], + "tilewidth":8, + "transformations": + { + "hflip":true, + "preferuntransformed":false, + "rotate":true, + "vflip":false + }, + "type":"tileset", + "version":"1.10", + "wangsets":[ + { + "colors":[ + { + "color":"#ff0000", + "name":"", + "probability":1, + "tile":-1 + }], + "name":"grass", + "tile":-1, + "type":"corner", + "wangtiles":[ + { + "tileid":5, + "wangid":[0, 0, 0, 1, 0, 1, 0, 1] + }, + { + "tileid":9, + "wangid":[0, 0, 0, 0, 0, 1, 0, 1] + }, + { + "tileid":10, + "wangid":[0, 1, 0, 1, 0, 0, 0, 1] + }, + { + "tileid":13, + "wangid":[0, 1, 0, 0, 0, 1, 0, 1] + }, + { + "tileid":14, + "wangid":[0, 0, 0, 0, 0, 0, 0, 1] + }] + }] +}
\ No newline at end of file diff --git a/src/tmz.zig b/src/tmz.zig new file mode 100644 index 0000000..b743e58 --- /dev/null +++ b/src/tmz.zig @@ -0,0 +1,551 @@ +//! Tiled Map parser +//! +//! This is essentially a thin wrapper around std.json to parse the contents of JSON Map files (.tmj). +//! +//! Supports both CSV and Base64 encoded Layer and Chunk data. If Layer is compressed, data will be +//! automatically decompressed, using all supported compression methods in Tiled (as of 1.10.2): +//! - gzip +//! - zlib +//! - zstd +//! +//! Use `parseMap` to parse Tiled JSON Map string +//! +//! Use `parseTileset` to parse Tiled JSON Tileset string + +const std = @import("std"); +const base64_decoder = std.base64.standard.Decoder; +const equals = std.testing.expectEqual; +const stringEquals = std.testing.expectEqualStrings; +const fixedBufferStream = std.io.fixedBufferStream; +const ParseOptions = std.json.ParseOptions; +const Value = std.json.Value; +const Allocator = std.mem.Allocator; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#map +pub const Map = struct { + /// Hex-formatted color (#RRGGBB or #AARRGGBB) + background_color: ?[]const u8 = null, + class: ?[]const u8 = null, + /// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default) + // TODO: actually support this? + compression_level: i32 = -1, + /// Number of tile rows + height: u32, + /// Length of the side of a hex tile in pixels (hexagonal maps only) + hex_side_length: ?u32 = null, + infinite: bool, + layers: []Layer, + /// Auto-increments for each layer + next_layer_id: u32, + /// Auto-increments for each placed object + next_object_id: u32, + orientation: Orientation, + parallax_origin_x: ?f32 = 0, + parallax_origin_y: ?f32 = 0, + properties: ?[]const Property = null, + /// currently only supported for orthogonal maps + render_order: RenderOrder, + /// staggered / hexagonal maps only + stagger_axis: ?StaggerAxis = null, + /// staggered / hexagonal maps only + stagger_index: ?StaggerIndex = null, + tiled_version: []const u8, + /// Map grid height + tile_height: u32, + tilesets: []Tileset, + /// Map grid width + tile_width: u32, + version: []const u8, + /// Number of tile columns + width: u32, + + pub const Orientation = enum { orthogonal, isometric, staggered, hexagonal }; + pub const RenderOrder = enum { @"right-down", @"right-up", @"left-down", @"left-up" }; + pub const StaggerAxis = enum { x, y }; + pub const StaggerIndex = enum { odd, even }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer +pub const Layer = struct { + chunks: ?[]Chunk = null, + class: ?[]const u8 = null, + compression: ?Compression = null, + data: ?[]u32 = null, + draw_order: ?DrawOrder = .topdown, + encoding: ?Encoding = .csv, + height: ?u32 = null, + id: u32, + image: ?[]const u8 = null, + layers: ?[]Layer = null, + locked: bool = false, + name: []const u8, + objects: ?[]Object = null, + offset_x: f32 = 0, + offset_y: f32 = 0, + opacity: f32, + parallax_x: f32 = 1, + parallax_y: f32 = 1, + properties: ?[]Property = null, + repeat_x: ?bool = null, + repeat_y: ?bool = null, + start_x: ?i32 = null, + start_y: ?i32 = null, + tint_color: ?[]const u8 = null, + transparent_color: ?[]const u8 = null, + type: Type, + visible: bool, + width: ?u32 = null, + x: i32, + y: i32, + + pub const DrawOrder = enum { topdown, index }; + pub const Encoding = enum { csv, base64 }; + pub const Type = enum { tilelayer, objectgroup, imagelayer, group }; + + pub const Compression = enum { + none, + zlib, + gzip, + zstd, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + _ = allocator; + _ = options; + return switch (source) { + .string, .number_string => |value| cmp: { + if (value.len == 0) { + break :cmp .none; + } else { + break :cmp std.meta.stringToEnum(Compression, value) orelse .none; + } + }, + else => .none, + }; + } + }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + var layer = try jsonParser(@This(), allocator, source, options); + + if (layer.type == .tilelayer) { + if (source.object.get("data")) |data| { + if (layer.encoding == .csv) { + layer.data = try std.json.parseFromValueLeaky([]u32, allocator, data, options); + if (source.object.get("chunks")) |chunks| { + layer.chunks = try std.json.parseFromValueLeaky([]Chunk, allocator, chunks, options); + } + } else { + const base64_data = try std.json.parseFromValueLeaky([]const u8, allocator, data, options); + const layer_size: usize = (layer.width orelse 0) * (layer.height orelse 0); + + layer.data = parseBase64(allocator, base64_data, layer_size, layer.compression orelse .none); + + if (layer.chunks) |chunks| { + for (chunks) |*chunk| { + const chunk_size: usize = chunk.width * chunk.height; + chunk.tiles = parseBase64(allocator, chunk.data.base64, chunk_size, layer.compression orelse .none); + } + } + } + } + } + return layer; + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk +pub const Chunk = struct { + data: DataEncoding, + tiles: []u32, + height: u32, + width: u32, + x: u32, + y: u32, + + const DataEncoding = union(enum) { + csv: []const u32, + base64: []const u8, + }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + var chunk = try jsonParser(@This(), allocator, source, options); + + if (source.object.get("data")) |data| { + switch (data) { + .array => { + chunk.data = .{ .csv = try std.json.parseFromValueLeaky([]const u32, allocator, data, options) }; + }, + .string => { + chunk.data = .{ .base64 = try std.json.parseFromValueLeaky([]const u8, allocator, data, options) }; + }, + else => return error.UnexpectedToken, + } + } + return chunk; + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#object +pub const Object = struct { + ellipse: ?bool = null, + gid: ?u32 = null, + height: f32, + id: u32, + name: []const u8, + point: ?bool = null, + polygon: ?[]Point = null, + polyline: ?[]Point = null, + properties: ?[]Property = null, + rotation: f32, + template: ?[]const u8 = null, + text: ?Text = null, + type: ?[]const u8 = null, + visible: bool, + width: f32, + x: f32, + y: f32, +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#point +pub const Point = struct { + x: f32, + y: f32, +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#text +pub const Text = struct { + bold: bool, + color: []const u8, + font_family: []const u8 = "sans-serif", + h_align: enum { center, right, justify, left } = .left, + italic: bool = false, + kerning: bool = true, + pixel_size: usize = 16, + strikeout: bool = false, + text: []const u8, + underline: bool = false, + v_align: enum { center, bottom, top } = .top, + wrap: bool = false, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#properties +pub const Property = struct { + name: []const u8, + type: Type = .string, + property_type: ?[]const u8 = null, + + pub const Type = enum { + string, + int, + float, + bool, + color, + file, + object, + class, + }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#tileset +pub const Tileset = struct { + background_color: ?[]const u8 = null, + class: ?[]const u8 = null, + columns: ?u32 = null, + fill_mode: ?FillMode = .stretch, + first_gid: u32, + grid: ?Grid = null, + image: ?[]const u8 = null, + image_height: ?u32 = null, + image_width: ?u32 = null, + margin: ?u32 = null, + name: ?[]const u8 = null, + object_alignment: ?ObjectAlignment = .unspecified, + properties: ?[]Property = null, + source: ?[]const u8 = null, + spacing: ?i32 = null, + terrains: ?[]Terrain = null, + tile_count: ?u32 = null, + tile_version: ?[]const u8 = null, + tile_height: ?u32 = null, + tile_offset: ?Point = null, + tile_render_size: ?TileRenderSize = .tile, + tiles: ?[]Tile = null, + tile_width: ?u32 = null, + transformations: ?Transformations = null, + transparent_color: ?[]const u8 = null, + type: ?Type = .tileset, + version: ?[]const u8 = null, + wang_sets: ?[]WangSet = null, + + pub const FillMode = enum { stretch, @"preserve-aspect-fit" }; + pub const TileRenderSize = enum { tile, grid }; + pub const Type = enum { tileset }; + + pub const ObjectAlignment = enum { + unspecified, + topleft, + top, + topright, + left, + center, + right, + bottomleft, + bottom, + bottomright, + }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + const tileset = try jsonParser(@This(), allocator, source, options); + + if (tileset.source) |s| { + const file = std.fs.cwd().openFile(s, .{}) catch @panic("couldn't open tileset"); + defer file.close(); + const tileset_bytes = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch @panic("couldn't read tileset file "); + return parseJsonLeaky(Tileset, allocator, tileset_bytes) catch @panic("could not parse tileset"); + } + return tileset; + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#grid +pub const Grid = struct { + orientation: enum { orthogonal, isometric } = .orthogonal, + height: i32, + width: i32, +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#terrain +pub const Terrain = struct { + name: []const u8, + properties: []Property, + tile: i32, +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#tile-definition +pub const Tile = struct { + animation: []Frame, + id: i32, + name: ?[]const u8, + image_height: u32, + image_width: u32, + x: i32, + y: i32, + width: u32, + height: u32, + objectgroup: ?Layer, + probability: f32, + properties: []Property, + terrain: []i32, + type: []const u8, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#frame +pub const Frame = struct { + duration: i32, + tile_id: i32, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#transformations +pub const Transformations = struct { + h_flip: bool, + v_flip: bool, + rotate: bool, + prefer_untransformed: bool, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#wang-set +pub const WangSet = struct { + class: ?[]const u8 = null, + colors: ?[]WangColor = null, + name: []const u8, + properties: ?[]Property = null, + tile: i32, + type: Type, + wang_tiles: []WangTile, + + pub const Type = enum { corner, edge, mixed }; + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#wang-color +pub const WangColor = struct { + class: ?[]const u8 = null, + color: []const u8, + name: []const u8, + probability: f32, + properties: ?[]Property = null, + tile: i32, +}; + +/// https://doc.mapeditor.org/en/stable/reference/json-map-format/#wang-tile +pub const WangTile = struct { + tile_id: i32, + wang_id: [8]u8, + + pub fn jsonParseFromValue(allocator: Allocator, source: Value, options: ParseOptions) !@This() { + return try jsonParser(@This(), allocator, source, options); + } +}; + +/// From https://www.openmymind.net/Zigs-std-json-Parsed/ +pub fn Managed(comptime T: type) type { + return struct { + value: T, + arena: *std.heap.ArenaAllocator, + + const Self = @This(); + + pub fn fromJson(parsed: std.json.Parsed(T)) Self { + return .{ + .arena = parsed.arena, + .value = parsed.value, + }; + } + + pub fn deinit(self: Self) void { + const arena = self.arena; + const allocator = arena.child_allocator; + arena.deinit(); + allocator.destroy(arena); + } + }; +} + +/// Parse a JSON Map string +/// You must call `deinit()` on the returned `Managed(Map)` to clean up allocated resources. +/// If you are using a `std.heap.ArenaAllocator` or similar, consider calling `parseMapLeaky` instead. +pub fn parseMap(allocator: Allocator, bytes: []const u8) !Managed(Map) { + return Managed(Map).fromJson(try parse(Map, allocator, bytes)); +} + +pub fn parseMapLeaky(allocator: Allocator, bytes: []const u8) !Map { + return try parseJsonLeaky(Map, allocator, bytes); +} + +/// Parse a JSON Tileset string +/// You must call `deinit()` on the returned `Managed(Tileset)` to clean up allocated resources. +/// If you are using a `std.heap.ArenaAllocator` or similar, consider calling `parseTilesetLeaky` instead. +pub fn parseTileset(allocator: Allocator, bytes: []const u8) !Managed(Tileset) { + return Managed(Tileset).fromJson(try parse(Tileset, allocator, bytes)); +} + +pub fn parseTilesetLeaky(allocator: Allocator, bytes: []const u8) !Tileset { + return try parseJsonLeaky(Tileset, allocator, bytes); +} + +fn parse(comptime T: type, allocator: Allocator, bytes: []const u8) !std.json.Parsed(T) { + var parsed = std.json.Parsed(T){ + .arena = try allocator.create(std.heap.ArenaAllocator), + .value = undefined, + }; + errdefer allocator.destroy(parsed.arena); + + parsed.arena.* = std.heap.ArenaAllocator.init(allocator); + errdefer parsed.arena.deinit(); + + parsed.value = try parseJsonLeaky(T, parsed.arena.allocator(), bytes); + + return parsed; +} + +fn parseJsonLeaky(comptime T: type, allocator: Allocator, bytes: []const u8) !T { + const json_value = try std.json.parseFromSliceLeaky(Value, allocator, bytes, .{}); + return try std.json.parseFromValueLeaky(T, allocator, json_value, .{ .ignore_unknown_fields = true }); +} + +// converts masheduppropertynames to Zig style snake_case field names and ignores data +fn jsonParser(T: type, allocator: Allocator, source: Value, options: ParseOptions) !T { + var t: T = undefined; + + inline for (@typeInfo(T).Struct.fields) |field| { + if (comptime std.mem.eql(u8, field.name, "data")) continue; + const size = comptime std.mem.replacementSize(u8, field.name, "_", ""); + var tiled_name: [size]u8 = undefined; + _ = std.mem.replace(u8, field.name, "_", "", &tiled_name); + + const source_field = source.object.get(&tiled_name); + if (source_field) |s| { + @field(t, field.name) = try std.json.innerParseFromValue(field.type, allocator, s, options); + } else { + if (field.default_value) |val| { + @field(t, field.name) = @as(*align(1) const field.type, @ptrCast(val)).*; + } + } + } + + return t; +} + +fn parseBase64(allocator: Allocator, base64_data: []const u8, chunk_size: usize, compression: Layer.Compression) []u32 { + const size = base64_decoder.calcSizeForSlice(base64_data) catch @panic("Unable to decode base64 data"); + + var decoded = allocator.alloc(u8, size) catch @panic("OOM"); + defer allocator.free(decoded); + base64_decoder.decode(decoded, base64_data) catch @panic("Unable to decode base64 data"); + + const data = allocator.alloc(u32, chunk_size) catch @panic("OOM"); + const alignment = @alignOf(u32); + const buf = allocator.alloc(u8, chunk_size * alignment) catch @panic("OOM"); + var decompressed = fixedBufferStream(buf); + var compressed = fixedBufferStream(decoded); + switch (compression) { + .gzip => { + std.compress.gzip.decompress(compressed.reader(), decompressed.writer()) catch @panic("Unable to decompress gzip"); + decoded = decompressed.getWritten(); + }, + .zlib => { + std.compress.zlib.decompress(compressed.reader(), decompressed.writer()) catch @panic("Unable to decompress zlib"); + decoded = decompressed.getWritten(); + }, + .zstd => { + const window_buffer = allocator.alloc(u8, 1 << 23) catch @panic("OOM"); + var zstd_stream = std.compress.zstd.decompressor(compressed.reader(), .{ .window_buffer = window_buffer }); + _ = zstd_stream.reader().readAll(buf) catch @panic("Unable to decompress zstd"); + decoded = buf; + }, + .none => {}, + } + + if (buf.len != decoded.len) { + @panic("data size does not match Layer dimensions"); + } + + var tile_index: usize = 0; + for (data) |*tile| { + const end = tile_index + alignment; + tile.* = std.mem.readInt(u32, decoded[tile_index..end][0..alignment], .little); + tile_index += alignment; + } + return data; +} + +test { + _ = @import("./tmz_test.zig"); +} diff --git a/src/tmz_test.zig b/src/tmz_test.zig new file mode 100644 index 0000000..221a871 --- /dev/null +++ b/src/tmz_test.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const equals = std.testing.expectEqual; +const stringEquals = std.testing.expectEqualStrings; +const tmz = @import("./tmz.zig"); +const Layer = tmz.Layer; +const Map = tmz.Map; + +const full_map = @embedFile("test/map.tmj"); + +test "parseMap works" { + const parsed_map = try tmz.parseMap(std.testing.allocator, full_map); + defer parsed_map.deinit(); + + const map = parsed_map.value; + try regularMapTests(map); + try equals(false, map.infinite); +} + +test "infinite map with base64 chunks" { + const infinite_map = @embedFile("test/map-infinite-base64-zstd.tmj"); + const parsed_map = try tmz.parseMap(std.testing.allocator, infinite_map); + defer parsed_map.deinit(); + + const map = parsed_map.value; + try equals(true, map.infinite); +} + +test "infinite map with csv chunks" { + const infinite_map = @embedFile("test/map-infinite-csv.tmj"); + const parsed_map = try tmz.parseMap(std.testing.allocator, infinite_map); + defer parsed_map.deinit(); + + const map = parsed_map.value; + try equals(true, map.infinite); +} + +test "parseTileset works" { + const tileset_data = @embedFile("test/tiles.tsj"); + const parsed_tileset = try tmz.parseTileset(std.testing.allocator, tileset_data); + defer parsed_tileset.deinit(); + + const tileset = parsed_tileset.value; + + try stringEquals("tiles.png", tileset.image.?); +} + +test "parseMapLeaky works" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const map = try tmz.parseMapLeaky(arena.allocator(), full_map); + try regularMapTests(map); +} + +fn regularMapTests(map: Map) !void { + try equals(null, map.background_color); + try stringEquals("bar", map.class.?); + try equals(-1, map.compression_level); + try equals(30, map.height); + try equals(null, map.hex_side_length); + try equals(false, map.infinite); + try equals(4, map.next_layer_id); + try equals(3, map.next_object_id); + try equals(.orthogonal, map.orientation); + try equals(0, map.parallax_origin_x); + try equals(0, map.parallax_origin_y); + + try equals(Map.RenderOrder.@"right-down", map.render_order); + + try stringEquals("1.10", map.version); + + try equals(3, map.layers.len); + const layer = map.layers[0]; + + try stringEquals("bar", layer.class.?); + try equals(Layer.Type.tilelayer, layer.type); + try equals(32, layer.width.?); + try equals(0, layer.x); + try equals(0, layer.y); + + try equals(16, layer.data.?[0]); + try equals(11, layer.data.?[1]); +} + +test "Map with CSV encoded layer data parses correctly" { + const parsed_map = try tmz.parseMap(std.testing.allocator, @embedFile("test/map-csv.tmj")); + defer parsed_map.deinit(); + + const map = parsed_map.value; + + try equals(1, map.layers.len); + + const layer = map.layers[0]; + try equals(null, layer.compression); + + try equals(10, layer.data.?[0]); + try equals(15, layer.data.?[1]); + try equals(5, layer.data.?[2]); +} + +test "Map with Base64 encoded layer data parses correctly" { + inline for (@typeInfo(Layer.Compression).Enum.fields) |field| { + const map_file = @embedFile("test/map-base64-" ++ field.name ++ ".tmj"); + const parsed_map = try tmz.parseMap(std.testing.allocator, map_file); + defer parsed_map.deinit(); + + const map = parsed_map.value; + + try equals(1, map.layers.len); + + const layer = map.layers[0]; + try equals(@as(Layer.Compression, @enumFromInt(field.value)), layer.compression); + + try equals(10, layer.data.?[0]); + try equals(15, layer.data.?[1]); + try equals(5, layer.data.?[2]); + } +} |