#!/usr/bin/env python3 # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT """tiledCheck.py REPO WORK - checks tools/tiled2joey.py (scripts/check-tiled.sh). One 4x3 map, written as TMX (tileset A embedded, B external TSX, layers in csv and base64+zlib) and as TMJ (A embedded, B external TSJ, csv and base64+gzip). Tileset A's four tiles and tileset B's two (with a margin and spacing) each hold marker pixels, so every orientation Tiled can ask for lands its markers in a known place. Both maps must give the same sheet and maps, and those must be what is worked out here by hand: the tiles in the order they first appear, flips baked, A's colours at their own indices and B's new one in the first free slot, empty cells 0xFF. The sheet must bake as a tile bank, and a map with 16x16 tiles or an RGB tileset must be refused. Prints a line a check; exits 1 if any fails. """ import base64 import gzip import json import os import struct import subprocess import sys import zlib from PIL import Image BLACK = (0, 0, 0) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) YELLOW = (255, 255, 0) H = 0x80000000 V = 0x40000000 D = 0x20000000 SKIP = 0xFF SIDE = 8 # Tileset A: four tiles in a row, markers as (x, y, palette index). A_TILES = [ [(1, 2, 1)], [(1, 2, 2)], [(1, 2, 3)], [(1, 2, 1), (6, 6, 2)], ] A_PALETTE = [BLACK, RED, GREEN, BLUE] # Tileset B: two tiles, margin 1, spacing 1; its blue is A's, yellow new. B_TILES = [ [(0, 7, 2)], [(7, 0, 1)], ] B_PALETTE = [BLACK, BLUE, YELLOW] B_MARGIN = 1 B_SPACING = 1 WIDTH = 4 HEIGHT = 3 A_FIRST = 1 B_FIRST = 5 GROUND = [ A_FIRST, A_FIRST | H, A_FIRST | V, A_FIRST | D, A_FIRST | D | H, A_FIRST + 1, 0, A_FIRST + 2, A_FIRST + 3, (A_FIRST + 1) | H | V, 0, 0, ] TOP = [ 0, 0, 0, 0, B_FIRST, 0, B_FIRST + 1, 0, 0, (B_FIRST + 1) | H, 0, 0, ] TOP_NAME = "Top Layer!" # What the sheet must hold, worked out by hand: each tile's markers as # (x, y, sheet palette index), in first-appearance order. A's colours keep # their indices (red 1, green 2, blue 3); B's yellow takes slot 4. EXPECT_TILES = [ [(1, 2, 1)], # A0 [(6, 2, 1)], # A0 flipped across [(1, 5, 1)], # A0 flipped down [(2, 1, 1)], # A0 x and y swapped [(5, 1, 1)], # A0 swapped, then across [(1, 2, 2)], # A1 [(1, 2, 3)], # A2 [(1, 2, 1), (6, 6, 2)], # A3 [(6, 5, 2)], # A1 across and down [(0, 7, 4)], # B0 (yellow) [(7, 0, 3)], # B1 (blue) [(0, 0, 3)], # B1 across ] EXPECT_PALETTE = [BLACK, RED, GREEN, BLUE, YELLOW] EXPECT_GROUND = bytes([0, 1, 2, 3, 4, 5, SKIP, 6, 7, 8, SKIP, SKIP]) EXPECT_TOP = bytes([SKIP, SKIP, SKIP, SKIP, 9, SKIP, 10, SKIP, SKIP, 11, SKIP, SKIP]) TOP_FILE = "Top_Layer.map" TBK_COUNT_OFFSET = 6 failures = 0 def check(name, ok, detail=""): global failures print("TILED: %-28s %s%s" % (name, "PASS" if ok else "FAIL", " (" + detail + ")" if detail else "")) if not ok: failures += 1 def indexedImage(path, palette, tiles, margin, spacing): width = 2 * margin + len(tiles) * SIDE + (len(tiles) - 1) * spacing image = Image.new("P", (width, 2 * margin + SIDE)) flat = [c for rgb in palette for c in rgb] image.putpalette(flat + [0] * (3 * 256 - len(flat))) pixels = image.load() for k, markers in enumerate(tiles): for x, y, index in markers: pixels[margin + k * (SIDE + spacing) + x, margin + y] = index image.save(path) def tsx(name, image, count, margin, spacing): return ('\n' ' \n\n' % (name, spacing, margin, count, count, image)) def tilesetJson(name, image, count, margin, spacing): return {"name": name, "tilewidth": 8, "tileheight": 8, "spacing": spacing, "margin": margin, "tilecount": count, "columns": count, "image": image} def words(gids): return struct.pack("<%dI" % len(gids), *gids) def writeMaps(work): indexedImage(os.path.join(work, "a.png"), A_PALETTE, A_TILES, 0, 0) indexedImage(os.path.join(work, "b.png"), B_PALETTE, B_TILES, B_MARGIN, B_SPACING) with open(os.path.join(work, "b.tsx"), "w") as f: f.write('\n' + tsx("b", "b.png", len(B_TILES), B_MARGIN, B_SPACING)) with open(os.path.join(work, "b.tsj"), "w") as f: json.dump(tilesetJson("b", "b.png", len(B_TILES), B_MARGIN, B_SPACING), f) tmx = ('\n' '\n' ' \n \n \n' ' \n' ' \n %s\n \n' ' \n %s\n \n' '\n') % (WIDTH, HEIGHT, A_FIRST, len(A_TILES), len(A_TILES), B_FIRST, WIDTH, HEIGHT, ",".join(str(g) for g in GROUND), TOP_NAME, WIDTH, HEIGHT, base64.b64encode(zlib.compress(words(TOP))).decode()) with open(os.path.join(work, "map.tmx"), "w") as f: f.write(tmx) tmj = { "type": "map", "orientation": "orthogonal", "infinite": False, "width": WIDTH, "height": HEIGHT, "tilewidth": 8, "tileheight": 8, "tilesets": [dict(tilesetJson("a", "a.png", len(A_TILES), 0, 0), firstgid=A_FIRST), {"firstgid": B_FIRST, "source": "b.tsj"}], "layers": [ {"type": "tilelayer", "name": "ground", "width": WIDTH, "height": HEIGHT, "data": GROUND}, {"type": "group", "name": "upper", "layers": [ {"type": "tilelayer", "name": TOP_NAME, "width": WIDTH, "height": HEIGHT, "encoding": "base64", "compression": "gzip", "data": base64.b64encode(gzip.compress(words(TOP))).decode()}, ]}, ], } with open(os.path.join(work, "map.tmj"), "w") as f: json.dump(tmj, f) def run(repo, args): return subprocess.run([sys.executable, os.path.join(repo, "tools", "tiled2joey.py")] + args, capture_output=True, text=True) def sheetMatches(path): image = Image.open(path) if image.mode != "P" or image.size != (SIDE * len(EXPECT_TILES), SIDE): return False, "mode %s, size %s" % (image.mode, image.size) palette = image.getpalette() for index, rgb in enumerate(EXPECT_PALETTE): if tuple(palette[index * 3:index * 3 + 3]) != rgb: return False, "palette index %d is %s, not %s" % (index, tuple(palette[index * 3:index * 3 + 3]), rgb) pixels = image.load() for k, markers in enumerate(EXPECT_TILES): want = {(x, y): index for x, y, index in markers} for y in range(SIDE): for x in range(SIDE): got = pixels[k * SIDE + x, y] if got != want.get((x, y), 0): return False, "tile %d pixel (%d,%d) is %d, not %d" % (k, x, y, got, want.get((x, y), 0)) return True, "%d tiles" % len(EXPECT_TILES) def main(argv): repo, work = argv[1], argv[2] writeMaps(work) outputs = {} for kind in ("tmx", "tmj"): out = os.path.join(work, kind) os.makedirs(out, exist_ok=True) result = run(repo, ["--header", os.path.join(out, "map.h"), os.path.join(work, "map." + kind), os.path.join(out, "sheet.png"), os.path.join(out, "maps")]) check("%s converts" % kind, result.returncode == 0, (result.stdout + result.stderr).strip()) if result.returncode != 0: continue ok, detail = sheetMatches(os.path.join(out, "sheet.png")) check("%s sheet" % kind, ok, detail) with open(os.path.join(out, "maps", "ground.map"), "rb") as f: ground = f.read() check("%s ground layer" % kind, ground == EXPECT_GROUND, ground.hex()) topPath = os.path.join(out, "maps", TOP_FILE) top = open(topPath, "rb").read() if os.path.exists(topPath) else b"" check("%s %s layer" % (kind, TOP_FILE), top == EXPECT_TOP, top.hex()) with open(os.path.join(out, "map.h")) as f: header = f.read() check("%s header" % kind, "MAP_WIDTH 4u" in header and "MAP_HEIGHT 3u" in header and "MAP_TILES 12u" in header and TOP_FILE in header) outputs[kind] = (list(Image.open(os.path.join(out, "sheet.png")).getdata()), ground, top) if len(outputs) == 2: check("tmx and tmj agree", outputs["tmx"] == outputs["tmj"]) tbk = os.path.join(work, "sheet.tbk") bake = subprocess.run([sys.executable, os.path.join(repo, "tools", "assetbake", "assetbake.py"), "--type", "tile", "--target", "dos", os.path.join(work, "tmx", "sheet.png"), tbk], capture_output=True, text=True) count = struct.unpack_from("