#!/usr/bin/env python3"""Build the JumPHP assets of Sorcery from the openSorcery resources folder.Source: the C++/SDL2 port of Sorcery+ (Virgin Games 1985, Amstrad CPC),https .../david/sorcery (its `resources/` folder and `assets/audio`).Everything is derived from that folder; the original .dsk is NOT needed. python3 convert.py /mnt/openspace1/david/sorceryOutputs into ../.. (the game folder): assets/tiles/tiles1.png tiles2.png 128x128 RGBA, 16x16 tiles of 8x8 (pen -> scene palette) assets/sprites/sprites1.png sprites2.png 288x192 RGBA, 12x8 frames of 24x24 (pen 0 transparent) assets/sprites/extra.png 288x48 RGBA, frames 96.. of the sprite list assets/fonts/font1.png font2.png font3.png 128x72 RGBA atlases of 8x8 glyphs (text palette, pens mask 1/2/3) assets/pictures/title.png 320x200 RGBA (the mode-1 picture rebuilt from the 160x200 sheet) assets/data/CODE.bin SMESSC.bin TISCRN.bin halloffame.bin copied as is (parsed at run time) assets/rooms/roomNN.txt copied as is assets/levels/<pack>/... copied, tiles.png converted like the tiles sheets assets/sfx/*.wav assets/music/title.mp3 copied from assets/audio"""import os, shutil, sysfrom PIL import ImageSRC = sys.argv[1] if len(sys.argv) > 1 else "/mnt/openspace1/david/sorcery"DST = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))CODE_BASE = 0x7000def fail(msg): print("convert: " + msg) sys.exit(1)# ---------------------------------------------------------------- colourdef lv_of(i): return [0, 128, 255][i]def ink_rgb(ink): """Firmware ink 0..26 -> (r, g, b). Port of Assets::inkColour (C++ writes ABGR; red is the low byte).""" ink %= 27 r = lv_of((ink // 3) % 3) g = lv_of(ink // 9) b = lv_of(ink % 3) return (r, g, b)INK2HW = [0x14, 0x04, 0x15, 0x1C, 0x18, 0x1D, 0x0C, 0x05, 0x0D, 0x16, 0x06, 0x17, 0x1E, 0x00, 0x1F, 0x0E, 0x07, 0x0F, 0x12, 0x02, 0x13, 0x1A, 0x19, 0x1B, 0x0A, 0x03, 0x0B]def hardware_rgb(hw): """Hardware colour number -> (r, g, b). Port of Assets::hardwareColour.""" hw &= 0x1F if hw == 0x01: hw = 0x00 elif hw == 0x08: hw = 0x05 elif hw == 0x09: hw = 0x03 elif hw == 0x10: hw = 0x04 elif hw == 0x11: hw = 0x02 for i in range(27): if INK2HW[i] == hw: return ink_rgb(i) return (0, 0, 0)# ---------------------------------------------------------------- CODE.binclass Code: def __init__(self, path): with open(path, "rb") as f: self.d = f.read() def at(self, a): off = a - CODE_BASE return self.d[off] if 0 <= off < len(self.d) else 0 def strings(self, a, count): out = [] for _ in range(count): s = "" while True: b = self.at(a); a += 1 s += chr(b & 0x7F) if b & 0x80: break out.append(s) return out# ---------------------------------------------------------------- indexed PNGsdef load_pen_png(path): """Indexed PNG -> list of palette indices + size. The PNG palette order IS the pen order.""" im = Image.open(path) if im.mode == "P": return im.load(), im.size if im.mode == "RGB" or im.mode == "RGBA": # map every pixel to the nearest colour of its own palette is not possible here: # the sources are always indexed; converting through RGB would need the preview palette pal = im.getpalette() rgb = im.convert("RGBA") return ("rgb", rgb.load(), pal, im.size) fail(path + ": expected an indexed PNG, got " + im.mode)def pen_at(source, x, y): if source[0] == "rgb": _, px, pal, size = source if x < 0 or y < 0 or x >= size[0] or y >= size[1]: return 0 r, g, b = px[x, y][:3] best, bi = 1 << 30, 0 for i in range(0, len(pal) // 3 if pal else 16): dr, dg, db = pal[i * 3] - r, pal[i * 3 + 1] - g, pal[i * 3 + 2] - b d = dr * dr + dg * dg + db * db if d < best: best, bi = d, i return bi px, size = source if x < 0 or y < 0 or x >= size[0] or y >= size[1]: return 0 return px[x, y]def make_dir(p): os.makedirs(p, exist_ok=True)# ---------------------------------------------------------------- maindef main(): res = os.path.join(SRC, "resources") audio = os.path.join(SRC, "assets", "audio") for p in (res, audio): if not os.path.isdir(p): fail("missing " + p) code = Code(os.path.join(res, "data", "CODE.bin")) scene_pal = [ink_rgb(code.at(0x92F8 + i)) for i in range(16)] text_pal = [hardware_rgb(code.at(0x9370 + i) & 0x1F) for i in range(4)] picture_pal = [ink_rgb(code.at(0x930C + i)) for i in range(4)] border = hardware_rgb(code.at(0x936F) & 0x1F) def emit_png(path, w, h, get): im = Image.new("RGBA", (w, h)) px = im.load() for y in range(h): for x in range(w): px[x, y] = get(x, y) im.save(path) print("== graphics") gdir = os.path.join(DST, "assets") for sub in ("tiles", "sprites", "fonts", "pictures", "data", "rooms", "sfx", "music"): make_dir(os.path.join(gdir, sub)) for set_n in (1, 2): # tiles: 64x128 pens (16x16 grid of 4x8) -> 128x128 screen pixels (each pen 2 px wide, 8 lines tall) src = load_pen_png(os.path.join(res, "graphics", "tiles%d.png" % set_n)) out = os.path.join(gdir, "tiles", "tiles%d.png" % set_n) emit_png(out, 128, 128, lambda x, y, s=src, p=scene_pal: p[pen_at(s, x // 2, y)] + (255,)) # sprites: 144x192 pens (12x8 frames of 12x24) -> 288x192 (pen 0 transparent) src = load_pen_png(os.path.join(res, "graphics", "sprites%d.png" % set_n)) out = os.path.join(gdir, "sprites", "sprites%d.png" % set_n) def sprite_pix(x, y, s=src, p=scene_pal): pen = pen_at(s, x // 2, y) return p[pen] + (255,) if pen else (0, 0, 0, 0) emit_png(out, 288, 192, sprite_pix) # extra sprites (frames 96..): 144x48 pens -> 288x48 src = load_pen_png(os.path.join(res, "graphics", "extra.png")) emit_png(os.path.join(gdir, "sprites", "extra.png"), 288, 48, lambda x, y: (lambda pen: scene_pal[pen] + (255,) if pen else (0, 0, 0, 0))(pen_at(src, x // 2, y))) # font: three atlases, one per pens mask (the colour of a pixel is text_pal[g & pens]) src = load_pen_png(os.path.join(res, "graphics", "font.png")) for pens in (1, 2, 3): emit_png(os.path.join(gdir, "fonts", "font%d.png" % pens), 128, 72, lambda x, y, m=pens: (lambda g: text_pal[g & m] + (255,) if g & m else (0, 0, 0, 0))(pen_at(src, x, y))) # title picture: the 160x200 sheet shows each mode-1 screen byte as two mode-0 pixels; rebuild the bytes, read as mode 1 src = load_pen_png(os.path.join(res, "graphics", "title.png")) pic = [[0] * 320 for _ in range(200)] for y in range(200): for bx in range(80): p0 = pen_at(src, bx * 2, y) p1 = pen_at(src, bx * 2 + 1, y) b = ((p0 & 1) << 7 | (p0 >> 1 & 1) << 3 | (p0 >> 2 & 1) << 5 | (p0 >> 3 & 1) << 1 | (p1 & 1) << 6 | (p1 >> 1 & 1) << 2 | (p1 >> 2 & 1) << 4 | (p1 >> 3 & 1)) for p in range(4): pic[y][bx * 4 + p] = ((b >> (3 - p)) & 1) | ((b >> (7 - p)) & 1) << 1 emit_png(os.path.join(gdir, "pictures", "title.png"), 320, 200, lambda x, y: picture_pal[pic[y][x]] + (255,)) print("== data") for name in ("CODE.bin", "SMESSC.bin", "TISCRN.bin", "halloffame.bin"): shutil.copyfile(os.path.join(res, "data", name), os.path.join(gdir, "data", name)) print("== rooms") for n in range(79): shutil.copyfile(os.path.join(res, "rooms", "room%02d.txt" % n), os.path.join(gdir, "rooms", "room%02d.txt" % n)) print("== levels") packs = [l.strip() for l in open(os.path.join(res, "levels", "index.txt")) if l.strip() and not l.strip().startswith("#")] for pack in packs: base = os.path.join(res, "levels", pack) dst = os.path.join(gdir, "levels", pack) make_dir(os.path.join(dst, "rooms")) shutil.copyfile(os.path.join(base, "level.txt"), os.path.join(dst, "level.txt")) if os.path.isfile(os.path.join(base, "extras.txt")): shutil.copyfile(os.path.join(base, "extras.txt"), os.path.join(dst, "extras.txt")) n = 0 while os.path.isfile(os.path.join(base, "rooms", "room%02d.txt" % n)): shutil.copyfile(os.path.join(base, "rooms", "room%02d.txt" % n), os.path.join(dst, "rooms", "room%02d.txt" % n)) n += 1 tiles_png = os.path.join(base, "tiles.png") if os.path.isfile(tiles_png): src = load_pen_png(tiles_png) emit_png(os.path.join(dst, "tiles.png"), 128, 128, lambda x, y, s=src: scene_pal[pen_at(s, x // 2, y)] + (255,)) print(" pack %-8s %2d rooms" % (pack, n)) print("== audio") sfx = {"shoot": "shoot", "rise": "rise", "click": "click", "enemy_explode": "enemy_explode", "player_death": "player_death", "pickup": "pickup", "blip": "blip", "burst": "burst", "hit": "hit", "zap": "zap", "thud": "thud", "counter": "counter", "game_over": "game_over", "level_done": "level_done", "whoosh": "whoosh"} for name in sfx: shutil.copyfile(os.path.join(audio, "sfx_%s.wav" % name), os.path.join(gdir, "sfx", "%s.wav" % name)) shutil.copyfile(os.path.join(audio, "music_title.mp3"), os.path.join(gdir, "music", "title.mp3")) print("scene palette: " + " ".join("%02d=%s" % (i, "".join("%02x" % c for c in scene_pal[i])) for i in (0, 3, 7, 15))) print("text palette: %s border %s" % (text_pal, border)) print("done -> " + DST)if __name__ == "__main__": main()