L'Armure Sacrée d'Antiriad

par davidnurdin · Remakes
pas encore noté
▶ Jouer Sources Éditer GitHub
games/antiriad-php / tools / extract / export.py · commit decc30a
"""Turns a memory dump of the original game (taken in game, see extract.sh) into the remake's assets.    python3 -I export.py <dsk2jumphp>/tools dump.bin <game>/assetsEvery address below is explained in README.md. Nothing here is guessed from pictures: the rooms, sprites,font, tables and texts are read where the game's own code reads them."""import jsonimport osimport syssys.path.insert(0, sys.argv[1])from cpc import Dump, HW, mode0_pixels, mode1_pixels   # noqa: E402from PIL import Image                                   # noqa: E402dump = Dump(sys.argv[2])m = dump.memOUT = sys.argv[3]ROOMS = 69EXITS = 0x9632          # 4 bytes per room: right, left, down, up (#FF = none)            #6A42LAYOUT = 0x4840         # 32 bytes per room: 8 x 4 blocks                                 #7265BLOCKS = 0x3930         # 16 bytes per block: 4 x 4 sub-tiles                             #709FSUBGFX = 0x2358         # 8 bytes per sub-tile: one byte per line, expanded by #0200/#0300  #737DCOLBITS = 0x50E0        # 2 bits per sub-tile: which colour group                          #73B7COLMASK = 0x932E        # the 4 colour groups: OR masks (pens +0, +4, +8, +12)REMAP_OF_ROOM = 0x9746  # room -> remap table (ranges of sub-tiles shifted per area)       #7255REMAP_TABLES = 0x978BPAL_OF_ROOM = 0x518F    # room -> palette (pens 1-7)                                       #7139PALETTES = 0x51D7       # 10 bytes per palette, 7 usedFIXED_PALETTE = 0x8DF3  # the 16 pens before a room changes 1-7SPRITES = 0x9432        # 4 bytes per sprite: address, width (bytes), height              #6D63SPRITE_COLOUR = 0x9332  # OR mask per spriteANIMS = 0x9021          # word per animation: [ticks per frame, frames..., command]        #6B45TYPES = 0x922A          # 4 bytes per object type (type 1 first)                           #59E0PROGRAMS = 0x97F8       # word per movement program: 4-byte steps [dirs, ticks, dx, dy]   #6BC8ENEMIES = 0x9BC7        # word per room: [count, (x, y, program, type) * count]            #7160MODULES = 0x8F14        # 4 x (room, x, y): boots, pulse beam, implosion mine, negator    #71CDCELLS = 0xA066          # 15 x (room, x, y): recharge cells                                #7202FALL = 0x8EC2           # lines per tick while falling (index grows each tick)             #65B8FONT = 0x0400           # 16 bytes per glyph, mode 1, 8 x 8                                #701EFONT_PUNCT = 0x8B3D     # 19 characters -> glyph codes at #8B50                            #6FFCMESSAGES = 0xA093       # word per message, '$' ends, 13 = new line                        #5EEAdef w16(a):    return m[a & 0xFFFF] | m[(a + 1) & 0xFFFF] << 8def remap(table, t):    """#7335: sub-tile t, shifted when it falls in one of the area's ranges (16-bit, wraps)."""    for k in range(m[table]):        lo, hi, off = m[table + 1 + 4 * k], m[table + 2 + 4 * k], w16(table + 3 + 4 * k)        if lo <= t <= hi:            return (t + off) & 0xFFFF    return tdef subtile_colour(t):    """#73B7: the OR mask of sub-tile t."""    return m[COLMASK + ((m[(COLBITS + (t >> 2)) & 0xFFFF] >> (2 * (3 - (t & 3)))) & 3)]def subtile_pens(t):    c = subtile_colour(t)    rows = []    for y in range(8):        e = m[(SUBGFX + t * 8 + y) & 0xFFFF]        rows.append(list(mode0_pixels(m[0x200 + e] | c)) + list(mode0_pixels(m[0x300 + e] | c)))    return rowsdef palette_index(room):    return m[PAL_OF_ROOM + room]def palette(index):    pal = list(m[FIXED_PALETTE:FIXED_PALETTE + 16])    pal[1:8] = m[PALETTES + index * 10:PALETTES + index * 10 + 7]    return [p & 31 for p in pal]def block_column(room, bx, table):    """4 x 16 sub-tiles of one block column of a room (with the remap table of the room drawing it)."""    col = []    for by in range(4):        blk = m[LAYOUT + room * 32 + by * 8 + bx]        for sy in range(4):            col.append([remap(table, m[BLOCKS + blk * 16 + sy * 4 + sx]) for sx in range(4)])    return coldef room_pens(room):    """The 160 x 128 pens of the playfield, as #7051 draws it: the left neighbour's last block column,    the room's 8 columns, the right neighbour's first one (each with its own remap table)."""    right, left = m[EXITS + room * 4], m[EXITS + room * 4 + 1]    cols = []    for src, bx in [(left, 7)] + [(room, i) for i in range(8)] + [(right, 0)]:        if src == 0xFF:            cols.append(None)        else:            cols.append(block_column(src, bx, w16(REMAP_TABLES + m[REMAP_OF_ROOM + src] * 2)))    pens = [[0] * 160 for _ in range(128)]    for c, col in enumerate(cols):        if col is None:            continue        for ty in range(16):            for sx in range(4):                for y, row in enumerate(subtile_pens(col[ty][sx])):                    for x, pen in enumerate(row):                        pens[ty * 8 + y][c * 16 + sx * 4 + x] = pen    return pensdef ground_rows(pens):    """#68B4: Tal stands on a line whose number is a multiple of 8 when a pixel under him has pen 3 or 7    (the test masks pen bit 2). One string of 160 '0' / '1' per sub-tile row."""    return [''.join('1' if (pens[ty * 8][x] & 0b1011) == 3 else '0' for x in range(160)) for ty in range(16)]def ceiling_rows(pens):    """#66CE / #6714: rising, Tal's head (y+7) is tested only above line #3A, the playfield's first 8 lines:    a pixel of pen 1 or 5 there stops him (the test masks pen bit 2). One string of 160 per line 0-7."""    return [''.join('1' if (pens[y][x] & 0b1011) == 1 else '0' for x in range(160)) for y in range(8)]def wall_rows(pens):    """#67B6: Tal stops at a screen byte with a pixel of pen 7 in the column he walks into. One hex string    of 80 bits (the screen bytes, bit 7 of the first digit = byte 0) per line."""    rows = []    for y in range(128):        bits = ''.join('1' if 7 in (pens[y][2 * b], pens[y][2 * b + 1]) else '0' for b in range(80))        rows.append('%020x' % int(bits, 2))    return rowsdef save_pens(pens, pal, path, width=None):    h = len(pens)    w = len(pens[0])    img = Image.new('RGB', (w * 2, h))    px = img.load()    for y in range(h):        for x in range(w):            rgb = HW[pal[pens[y][x]]]            px[x * 2, y] = rgb            px[x * 2 + 1, y] = rgb    img.save(path)MIRRORED = 0x93B2DISSOLVE = 0x8E22        # 13 masks of 13 bytes: #6E30 draws a sprite ANDed with mask #B412 (12 .. 0)def sprite_pens(n, mask=None):    """#6D3C: sprite n, one source byte = 4 pixels through #0200/#0300, ORed with its colour.    A screen byte whose two pixels are 0 in the source keeps the background: None there.    #73F0 mirrors a sprite's bytes in place when it is asked the other way round than last time and keeps    that way in #93B2 + n (bit 7): such a sprite is turned back here, every sheet faces the bit-7-clear way."""    a, cw, h = w16(SPRITES + n * 4), m[SPRITES + n * 4 + 2], m[SPRITES + n * 4 + 3]    c = m[SPRITE_COLOUR + n]    rows, k = [], 0    for y in range(h):        row = []        for x in range(cw):            e = m[(a + y * cw + x) & 0xFFFF]            for half in (0x200, 0x300):                raw = m[half + e]                if mask is not None:          # #6E9B: the expanded byte ANDed with the next mask byte                    raw &= m[DISSOLVE + mask * 13 + k % 13]                    k += 1                if raw == 0:                    row += [None, None]                else:                    row += list(mode0_pixels(raw | c))        rows.append(row[::-1] if m[MIRRORED + n] & 0x80 else row)    return rowsdef anim(i):    p = w16(ANIMS + 2 * i)    frames, k = [], p + 1    while True:        b = m[k]        k += 1        if b == 0xFC:            return {'ticks': m[p] + 1, 'frames': frames, 'end': 'loop'}        if b == 0xFD:            return {'ticks': m[p] + 1, 'frames': frames, 'end': 'hold'}        if b == 0xFE:            return {'ticks': m[p] + 1, 'frames': frames, 'end': 'next', 'next': m[k]}        if b == 0xFF:            return {'ticks': m[p] + 1, 'frames': frames, 'end': 'kill'}        frames.append(b)def program(i):    p = w16(PROGRAMS + 2 * i)    steps, k = [], p    while len(steps) < 40:        if m[k] >= 0xFC:            return {'steps': steps, 'end': {0xFC: 'loop', 0xFD: 'hold', 0xFE: 'kill', 0xFF: 'hold'}[m[k]]}        steps.append(list(m[k:k + 4]))        k += 4    return {'steps': steps, 'end': 'hold'}def text(i):    p, s = w16(MESSAGES + 2 * i), ''    while m[p] != 0x24 and len(s) < 300:        s += '\n' if m[p] == 13 else chr(m[p])        p += 1    return sdef main():    for d in ('rooms', 'sprites', 'data', 'hud'):        os.makedirs(f'{OUT}/{d}', exist_ok=True)    world = {'rooms': [], 'palettes': [palette(i) for i in range(14)], 'hw': [list(c) for c in HW]}    for r in range(ROOMS):        pens = room_pens(r)        pi = palette_index(r)        save_pens(pens, palette(pi), f'{OUT}/rooms/room-{r:02d}.png')        ex = list(m[EXITS + r * 4:EXITS + r * 4 + 4])        p = w16(ENEMIES + 2 * r)        world['rooms'].append({            'exits': {k: (None if v == 0xFF else v) for k, v in zip(('right', 'left', 'down', 'up'), ex)},            'palette': pi,            'ground': ground_rows(pens),            'walls': wall_rows(pens),            'ceiling': ceiling_rows(pens),            'objects': [list(m[p + 1 + 4 * k:p + 5 + 4 * k]) for k in range(m[p])],        })    # sprites: one sheet per palette (35 sprites take pens 0-7 of the room), rectangles in sprites.json    sprites, x, y, rowh = [], 0, 0, 0    all_pens = [sprite_pens(n) for n in range(128)]    for n, pens in enumerate(all_pens):        w, h = len(pens[0]) * 2 if pens else 0, len(pens)        if x + w > 512:            x, y, rowh = 0, y + rowh + 1, 0        sprites.append({'x': x, 'y': y, 'w': w, 'h': h, 'colour': m[SPRITE_COLOUR + n]})        x += w + 1        rowh = max(rowh, h)    sheet_h = y + rowh    # pens 13-15 are the suit's: #61BB lights them (#8E06) when Tal is inside, #61D2 puts them out (#8E03)    for pi, lit in [(pi, lit) for pi in range(14) for lit in (False, True)]:        pal = list(palette(pi))        if lit:            pal[13:16] = list(m[0x8E06:0x8E09])        img = Image.new('RGBA', (512, sheet_h), (0, 0, 0, 0))        px = img.load()        for n, pens in enumerate(all_pens):            s = sprites[n]            for yy, row in enumerate(pens):                for xx, pen in enumerate(row):                    if pen is not None:                        rgb = HW[pal[pen]]                        px[s['x'] + xx * 2, s['y'] + yy] = rgb + (255,)                        px[s['x'] + xx * 2 + 1, s['y'] + yy] = rgb + (255,)        img.save(f'{OUT}/sprites/sprites-{pi:02d}' + ('-lit' if lit else '') + '.png')    world['sprites'] = sprites    # Tal getting into the suit (#6094 -> #5D9E): his front sprite (#09) fades out through the 13 masks of    # #6E30, step 12 first; one row of 13 per palette    tal = [sprite_pens(9, step) for step in range(13)]    tw, th = len(tal[0][0]) * 2, len(tal[0])    for pi in range(14):        pal = palette(pi)        img = Image.new('RGBA', (tw * 13, th), (0, 0, 0, 0))        px = img.load()        for step, pens in enumerate(tal):            for yy, row in enumerate(pens):                for xx, pen in enumerate(row):                    if pen is not None:                        px[step * tw + xx * 2, yy] = HW[pal[pen]] + (255,)                        px[step * tw + xx * 2 + 1, yy] = HW[pal[pen]] + (255,)        img.save(f'{OUT}/sprites/dissolve-{pi:02d}.png')    world['anims'] = [anim(i) for i in range(0x40)]    world['types'] = [list(m[TYPES + 4 * (t - 1):TYPES + 4 * t]) if t else [0, 0, 0, 0] for t in range(0x40)]    world['programs'] = [program(i) for i in range(46)]    world['fall'] = list(m[FALL:FALL + 17])    world['modules'] = [list(m[MODULES + 3 * i:MODULES + 3 * i + 3]) for i in range(4)]    world['cells'] = [list(m[CELLS + 3 * i:CELLS + 3 * i + 3]) for i in range(15)]    world['armour'] = [m[0xB4F4], m[0x8F13], 0x40]   # room, x, y (#58D9, #723A)    world['start'] = [[0x38, 0x14, 0x57], [0x44, 0x24, 0x57]]   # #58DF / #7506: room 56 or 68    world['messages'] = [text(i) for i in range(22)]    # font: glyph codes 0-127, mode 1 (pen 1 = ink); characters -> codes as #6FBE does    glyphs = []    for code in range(128):        # 0-63: letters, digits, signs; 64-127: the dashboard's graphics (#6FBE)        rows = []        for yy in range(8):            a = FONT + code * 16 + yy * 2            rows.append(''.join('#' if p else '.' for p in mode1_pixels(m[a]) + mode1_pixels(m[a + 1])))        glyphs.append(rows)    chars = {chr(ord('A') + i): i + 1 for i in range(26)}    chars.update({chr(ord('0') + i): 0x22 + i for i in range(10)})    for i in range(19):        chars[chr(m[FONT_PUNCT + i])] = m[FONT_PUNCT + 19 + i]    world['font'] = {'glyphs': glyphs, 'chars': chars}    with open(f'{OUT}/data/world.json', 'w') as f:        json.dump(world, f, separators=(',', ':'))    # the dashboard (mode 1, its own palette): the screen under the playfield    hud = dump.screen().crop((0, 128, 320, 200)).convert('RGB')    hud.save(f'{OUT}/hud/hud.png')    # lit up by the suit (seen in the emulator): blue -> sky blue, bright blue -> bright cyan    lit = {(0, 0, 128): (0, 128, 255), (0, 0, 255): (0, 255, 255)}    on = hud.copy()    px = on.load()    for yy in range(on.height):        for xx in range(on.width):            px[xx, yy] = lit.get(px[xx, yy], px[xx, yy])    on.save(f'{OUT}/hud/hud-lit.png')main()