joeylib2/tests/host/tiledCheck.py

229 lines
10 KiB
Python
Executable file

#!/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 ('<tileset version="1.10" name="%s" tilewidth="8" tileheight="8" spacing="%d" margin="%d" tilecount="%d" columns="%d">\n'
' <image source="%s"/>\n</tileset>\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('<?xml version="1.0" encoding="UTF-8"?>\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 = ('<?xml version="1.0" encoding="UTF-8"?>\n'
'<map version="1.10" orientation="orthogonal" renderorder="right-down" width="%d" height="%d" tilewidth="8" tileheight="8" infinite="0">\n'
' <tileset firstgid="%d" name="a" tilewidth="8" tileheight="8" tilecount="%d" columns="%d">\n <image source="a.png"/>\n </tileset>\n'
' <tileset firstgid="%d" source="b.tsx"/>\n'
' <layer id="1" name="ground" width="%d" height="%d">\n <data encoding="csv">%s</data>\n </layer>\n'
' <layer id="2" name="%s" width="%d" height="%d">\n <data encoding="base64" compression="zlib">%s</data>\n </layer>\n'
'</map>\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("<H", open(tbk, "rb").read(), TBK_COUNT_OFFSET)[0] if bake.returncode == 0 else -1
check("sheet bakes as a tile bank", count == len(EXPECT_TILES), "%d tiles" % count)
big = open(os.path.join(work, "map.tmx")).read().replace('tilewidth="8" tileheight="8" infinite', 'tilewidth="16" tileheight="16" infinite')
with open(os.path.join(work, "big.tmx"), "w") as f:
f.write(big)
refused = run(repo, [os.path.join(work, "big.tmx"), os.path.join(work, "big.png"), os.path.join(work, "bigmaps")])
check("16x16 tiles refused", refused.returncode != 0 and "8x8" in refused.stderr, refused.stderr.strip())
Image.open(os.path.join(work, "a.png")).convert("RGB").save(os.path.join(work, "a.png"))
refused = run(repo, [os.path.join(work, "map.tmx"), os.path.join(work, "rgb.png"), os.path.join(work, "rgbmaps")])
check("RGB tileset refused", refused.returncode != 0 and "indexed" in refused.stderr, refused.stderr.strip())
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))