229 lines
10 KiB
Python
Executable file
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))
|