"""Write the game's assets from the original disc and the emulator captures. python3 -I export.py <disk.dsk> <work dir of extract_round.py> <game folder>Everything the PHP game loads comes from here: assets/tiles/island-N.png the 128 background tiles of island N (8x8, 16 per row) assets/tiles/overlay-N.png the 128 "overlay" tiles (rainbows, fruits...) drawn over the background assets/sprites/island-N.png the 29 sprite frames of island N, and island-N.json their rectangles assets/fonts/font.png the game's font: 48 glyphs per row, one row per colour assets/sprites/loading.png the loading screen assets/maps/round-NN.json the 28 rounds: tile rows, goal, signs, enemies assets/data/islands.json names, solidity threshold and enemy kinds of the 7 islandsThe CPC draws in mode 0 (pixels twice as wide as high), so every pixel is written 2 pixels wide."""import sys, os, json, structsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))from PIL import Imagefrom cpc import HW, mode0_pixels, Diskfrom island import unpack2, blocksfrom sprite import decodefrom frameset import frames as sprite_framesISLANDS = ['INSECT ISLAND', 'COMBAT ISLAND', 'MONSTER ISLAND', 'TOY ISLAND', "DOH'S ISLAND", 'ROBOT ISLAND', 'DRAGON ISLAND']# Object type (low 7 bits of a placement) -> sprite frames it was seen drawing in the emulator# (typemap.py); types the captures never met are guessed from the frame order. Frames 12-15 (the blue spin of a hit enemy) are shared by all.ENEMY_FRAMES = { 1: {0x44: [16, 17], 0x45: [16, 17], 0x46: [18, 19], 0x48: [20], 0x4a: [21, 22], 0x4b: [23, 24], 0x4c: [25, 26]}, 2: {0x44: [16, 17], 0x46: [18, 19], 0x48: [20, 21], 0x4a: [24], 0x4c: [22, 23], 0x4e: [25, 26], 0x50: [27, 28]}, 3: {0x44: [19, 20], 0x46: [16, 17], 0x48: [21, 22], 0x4a: [18], 0x4c: [19, 20], 0x4e: [23], 0x50: [25, 26]}, 4: {0x44: [17], 0x46: [16], 0x48: [27], 0x4a: [27], 0x4c: [28], 0x4e: [18, 19, 20, 21], 0x50: [22, 23, 24, 25]}, 5: {0x44: [16], 0x46: [17, 18, 19, 20], 0x48: [21], 0x4a: [22, 23, 24, 25], 0x4c: [26, 27]}, 6: {0x44: [16, 17], 0x46: [16, 17], 0x48: [19], 0x4a: [20, 21], 0x4c: [22, 23], 0x4e: [18], 0x50: [25, 26]}, 7: {0x44: [16, 17], 0x46: [18, 19], 0x48: [18, 19], 0x4a: [18, 19], 0x4c: [20], 0x4e: [21, 22], 0x50: [25, 26]},}# Enemies seen flying (the others walk on the platforms)FLYERS = {1: [0x46, 0x4c], 2: [0x4c, 0x4e], 3: [0x44, 0x4c], 4: [0x48, 0x4a], 5: [0x44, 0x48], 6: [0x48, 0x4e], 7: [0x4c]}# The rainbow: 6 x 4 overlay tiles (overlay tiles 1-18; its legs are 2 tiles wide)RAINBOW = [[0x00, 0x01, 0x02, 0x03, 0x04, 0x00], [0x05, 0x0f, 0x06, 0x07, 0x10, 0x08], [0x09, 0x0a, 0x00, 0x00, 0x0b, 0x0c], [0x11, 0x0d, 0x00, 0x00, 0x0e, 0x12]]def rgb(pal, pen): return HW[pal[pen] & 0x1f]def tile_image(data, pal, transparent): img = Image.new('RGBA', (16 * 8, 8 * 8), (0, 0, 0, 0)) px = img.load() for t in range(128): for y in range(8): row = [p for b in data[t * 16 + y * 2:t * 16 + y * 2 + 2] for p in mode0_pixels(b)] for x, p in enumerate(row): colour = (0, 0, 0, 0) if transparent and p == 0 else rgb(pal, p) + (255,) px[(t % 16) * 8 + x * 2, (t // 16) * 8 + y] = colour px[(t % 16) * 8 + x * 2 + 1, (t // 16) * 8 + y] = colour return imgdef sprite_sheet(mem, pal): rects, x = [], 0 found = sprite_frames(mem) height = max(f[2] for f in found if f) img = Image.new('RGBA', (sum(f[1] * 4 + 2 for f in found if f), height), (0, 0, 0, 0)) px = img.load() for f in found: if not f: rects.append(None) continue base, w, h = f for yy, row in enumerate(decode(mem, base, w, h)): for xx, p in enumerate(row): if p: px[x + xx * 2, yy] = px[x + xx * 2 + 1, yy] = rgb(pal, p) + (255,) rects.append({'x': x, 'y': 0, 'w': w * 4, 'h': h}) x += w * 4 + 2 return img, rectsdef boss_frame(sheet, rects): """The boss is the biggest frame of the island's own ones (16-28). Some are stored as their left half only (the right half is drawn mirrored): their rightmost column is not empty.""" best = max((i for i in range(16, len(rects)) if rects[i]), key=lambda i: rects[i]['w'] * rects[i]['h']) r = rects[best] edge = sum(1 for y in range(r['h']) if sheet.getpixel((r['x'] + r['w'] - 1, y))[3]) return best, edge > r['h'] // 3def font_image(mem, pal): schemes = [2, 4, 5, 6, 7] # red, green, cyan, yellow, white img = Image.new('RGBA', (48 * 8, len(schemes) * 8), (0, 0, 0, 0)) px = img.load() for row, scheme in enumerate(schemes): table = mem[0x247d + scheme * 4:0x247d + scheme * 4 + 4] for ch in range(48): for y in range(8): bits = mem[0x1a30 + ch * 8 + y] for x in range(4): v = (bits >> (6 - 2 * x)) & 3 if v: c = rgb(pal, mode0_pixels(table[v])[1]) + (255,) px[ch * 8 + x * 2, row * 8 + y] = px[ch * 8 + x * 2 + 1, row * 8 + y] = c return imgdef round_lists(block): """The three lists of a round block: placements by player height, placements by camera, signs.""" offsets = struct.unpack('<3H', block[:6]) by_camera = block[offsets[1]:offsets[2]] objects, i = [], 3 while i + 2 < len(by_camera): y, x, t = by_camera[i:i + 3] i += 3 if x == 0xff: break if t & 0x80 and not x & 0x60: objects.append({'type': t & 0x7f, 'x': x & 0x1f, 'y': y - 256, 'flip': bool(x & 0x80)}) signs = block[offsets[2]:] sign_rows = [signs[k + 1] - 256 for k in range(0, len(signs) - 1, 2)] return objects, sign_rows, block[7]def main(): dsk, work, game = sys.argv[1:4] assets = os.path.join(game, 'assets') for sub in ('tiles', 'sprites', 'fonts', 'maps', 'data'): os.makedirs(os.path.join(assets, sub), exist_ok=True) disk = Disk(dsk) # the loading screen: 16 KB of mode 0 screen read by the boot loader (file 11), palette from the boot sector boot = disk.sector(0, 0) pal = list(boot[0x95:0x95 + 16]) screen = disk.read(22, 2, 32) img = Image.new('RGB', (320, 200)) px = img.load() for y in range(200): for x in range(80): b = screen[(y // 8) * 80 + (y % 8) * 0x800 + x] for k, p in enumerate(mode0_pixels(b)): px[x * 4 + k * 2, y] = px[x * 4 + k * 2 + 1, y] = rgb(pal, p) img.save(os.path.join(assets, 'sprites', 'loading.png')) islands = [] for n in range(1, 8): mem = open(f'{work}/round-{(n - 1) * 4:02d}/dump-02200.bin', 'rb').read() pal = list(mem[0x20011:0x20021]) tile_image(mem[0x7300:0x7b00], pal, False).save(f'{assets}/tiles/island-{n}.png') tile_image(mem[0x1000:0x1800], pal, True).save(f'{assets}/tiles/overlay-{n}.png') sheet, rects = sprite_sheet(mem, pal) sheet.save(f'{assets}/sprites/island-{n}.png') json.dump(rects, open(f'{assets}/sprites/island-{n}.json', 'w')) if n == 1: font_image(mem, pal).save(f'{assets}/fonts/font.png') islands.append({ 'name': ISLANDS[n - 1], 'solidFrom': mem[0xfec8], 'palette': ['#%02x%02x%02x' % rgb(pal, p) for p in range(16)], 'enemies': {str(t): {'frames': f, 'flies': t in FLYERS[n]} for t, f in ENEMY_FRAMES[n].items()}, 'bossFrame': boss_frame(sheet, rects)[0], 'bossMirrored': boss_frame(sheet, rects)[1], }) json.dump(islands, open(f'{assets}/data/islands.json', 'w'), indent=1) # where Bub's feet rest on a rainbow: the first opaque pixel of each of its 48 columns mem = open(f'{work}/round-00/dump-02200.bin', 'rb').read() profile = [] for x in range(48): col, sub = x // 8, (x % 8) // 2 top = 32 for y in range(32): t = RAINBOW[y // 8][col] if t and mode0_pixels(mem[0x1000 + t * 16 + (y % 8) * 2 + sub // 2])[sub % 2]: top = y break profile.append(top) json.dump({'tiles': RAINBOW, 'profile': profile}, open(f'{assets}/data/rainbow.json', 'w')) for r in range(28): n = r // 4 + 1 data = blocks(unpack2(disk.read(*ISLAND_FILES[n - 1]))) objects, sign_rows, hurry = round_lists(data['blocks'][2 + r % 4]) m = json.load(open(f'{work}/round-{r:02d}/map.json')) mem = open(f'{work}/round-{r:02d}/dump-02200.bin', 'rb').read() json.dump({ 'island': n, 'round': r + 1, 'top': m['top'], 'goalRow': mem[0xfee4] - 256, 'hurry': hurry, 'signs': sign_rows, 'objects': objects, 'rows': [''.join('%02x' % t for t in row) for row in m['rows']], }, open(f'{assets}/maps/round-{r + 1:02d}.json', 'w'), indent=0) print('assets written to', assets)# (track, sector, count) of the seven island files, from the loader's table at #01EFISLAND_FILES = [(25, 7, 18), (27, 7, 18), (29, 7, 18), (31, 7, 18), (33, 7, 18), (35, 7, 18), (37, 7, 18)]if __name__ == '__main__': main()