HOME/Articles/

pil example avecta (snippet)

Article Outline

Python pil example 'avecta'

Functions in program:

  • def levelsdump(fn,tg,tf,numbers=True):
  • def leveldump(fn,count,d,tg,tf,numbers):
  • def floortile(d,tf):
  • def hex(d):
  • def unpack_txt(b):
  • def unpack_4bit(g, tw, th, stride, rows):
  • def palette_color(x):

Modules used in program:

  • import PIL.ImageDraw
  • import PIL.ImageFont
  • import PIL.Image

python avecta

Python pil example: avecta

# Avecta I: Ebora is an Atari ST game published in STart Magazine, September 1989
#
# Information here:
# https://www.atarimagazines.com/startv4n2/avecta.html
#
# This program parses its data files, and generates maps from it.
# The file formats can be deduced from the program.
# Some of the data is described in comments.

import PIL.Image
import PIL.ImageFont
import PIL.ImageDraw

info = True

prg = open("AVECTA.PRG","rb").read()
grafxdat = open("GRAFX.DAT","rb").read()
filldat = open("FILL.DAT","rb").read()

font = PIL.ImageFont.truetype("ProggyTiny.ttf",16)
# Font available here: https://proggyfonts.net/download/

palette = [
    0x000,
    0x700,
    0x007,
    0x520,
    0x560,
    0x050,
    0x741,
    0x444,
    0x770,
    0x077,
    0x641,
    0x111, #0x000,
    0x707,
    0x765,
    0x505,
    0x777,
    ]

pattern = []

pattern_color_remap = [
    0x0,0xF,0x2,0x3,
    0x4,0x6,0x6,0x5,
    0x7,0x9,0x9,0xB,
    0xC,0xD,0xE,0xD,
    ] # 1,4,5,7,8,A,C,F are known. 0,2,3,6,9,B,D,E are unused/unknown.

# fill patterns derived from GFA Basic DEFFILL
fill_patterns = [
    [0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,0xFFFF,], # 0
    [0xFFFF,0xBBBB,0xFFFF,0xEEEE,0xFFFF,0xBBBB,0xFFFF,0xEEEE,0xFFFF,0xBBBB,0xFFFF,0xEEEE,0xFFFF,0xBBBB,0xFFFF,0xEEEE,], # 1
    [0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,0xFFFF,0xAAAA,], # 2
    [0x7777,0xAAAA,0xDDDD,0xAAAA,0x7777,0xAAAA,0xDDDD,0xAAAA,0x7777,0xAAAA,0xDDDD,0xAAAA,0x7777,0xAAAA,0xDDDD,0xAAAA,], # 3
    [0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,0x5555,0xAAAA,], # 4
    [0x5555,0x2222,0x5555,0x8888,0x5555,0x2222,0x5555,0x8888,0x5555,0x2222,0x5555,0x8888,0x5555,0x2222,0x5555,0x8888,], # 5
    [0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,0x5555,0x0000,], # 6
    [0x1111,0x0000,0x4444,0x0000,0x1111,0x0000,0x4444,0x0000,0x1111,0x0000,0x4444,0x0000,0x1111,0x0000,0x4444,0x0000,], # 7
    [0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,0x0000,], # 8
    [0x0000,0x7F7F,0x7F7F,0x7F7F,0x0000,0xF7F7,0xF7F7,0xF7F7,0x0000,0x7F7F,0x7F7F,0x7F7F,0x0000,0xF7F7,0xF7F7,0xF7F7,], # 9
    [0xDFDF,0xBFBF,0x7F7F,0xBEBE,0xDDDD,0xEBEB,0xF7F7,0xEFEF,0xDFDF,0xBFBF,0x7F7F,0xBEBE,0xDDDD,0xEBEB,0xF7F7,0xEFEF,], # 10
    [0xFFFF,0xFFFF,0xEFEF,0xD7D7,0xFFFF,0xFFFF,0xFEFE,0x7D7D,0xFFFF,0xFFFF,0xEFEF,0xD7D7,0xFFFF,0xFFFF,0xFEFE,0x7D7D,], # 11
    [0xFDFD,0xFDFD,0x5555,0xAFAF,0xDFDF,0xDFDF,0x5555,0xFAFA,0xFDFD,0xFDFD,0x5555,0xAFAF,0xDFDF,0xDFDF,0x5555,0xFAFA,], # 12
    [0xBFBF,0x7F7F,0xFFFF,0xF7F7,0xFBFB,0xFDFD,0xFFFF,0xDFDF,0xBFBF,0x7F7F,0xFFFF,0xF7F7,0xFBFB,0xFDFD,0xFFFF,0xDFDF,], # 13
    [0x99F9,0x3939,0x2727,0xE7E7,0x7E7E,0x724E,0xF3CC,0x9FFF,0x99F9,0x3939,0x2727,0xE7E7,0x7E7E,0x724E,0xF3CC,0x9FFF,], # 14
    [0xFFFF,0xFFFF,0xFBFF,0xFFFF,0xFFEF,0xFFFF,0x7FFF,0xFFFF,0xFFFF,0xFFFF,0xFBFF,0xFFFF,0xFFEF,0xFFFF,0x7FFF,0xFFFF,], # 15
    [0x0707,0x9393,0x3939,0x7070,0xE0E0,0xC9C9,0x9C9C,0x0E0E,0x0707,0x9393,0x3939,0x7070,0xE0E0,0xC9C9,0x9C9C,0x0E0E,], # 16
    [0x5555,0xFFFF,0x7777,0xEBEB,0xDDDD,0xBEBE,0x7777,0xFFFF,0x5555,0xFFFF,0x7777,0xEBEB,0xDDDD,0xBEBE,0x7777,0xFFFF,], # 17
    [0xF7F7,0xFFFF,0x5555,0xFFFF,0xF7F7,0xFFFF,0x7777,0xFFFF,0xF7F7,0xFFFF,0x5555,0xFFFF,0xF7F7,0xFFFF,0x7777,0xFFFF,], # 18
    [0x8888,0x6767,0x0707,0x0707,0x8888,0x7676,0x7070,0x7070,0x8888,0x6767,0x0707,0x0707,0x8888,0x7676,0x7070,0x7070,], # 19
    [0x7F7F,0x7F7F,0xBEBE,0xC1C1,0xF7F7,0xF7F7,0xEBEB,0x1C1C,0x7F7F,0x7F7F,0xBEBE,0xC1C1,0xF7F7,0xF7F7,0xEBEB,0x1C1C,], # 20
    [0x7E7E,0xBDBD,0xDBDB,0xE7E7,0xF9F9,0xFEFE,0x7F7F,0x7F7F,0x7E7E,0xBDBD,0xDBDB,0xE7E7,0xF9F9,0xFEFE,0x7F7F,0x7F7F,], # 21
    [0x0F0F,0x0F0F,0x0F0F,0x0F0F,0xF0F0,0xF0F0,0xF0F0,0xF0F0,0x0F0F,0x0F0F,0x0F0F,0x0F0F,0xF0F0,0xF0F0,0xF0F0,0xF0F0,], # 22
    [0xF7F7,0xE3E3,0xC1C1,0x8080,0x0000,0x8080,0xC1C1,0xE3E3,0xF7F7,0xE3E3,0xC1C1,0x8080,0x0000,0x8080,0xC1C1,0xE3E3,], # 23
    ]

for i in range(0,24):
    dst = PIL.Image.new("RGBA",(16,16),(255,255,255,0))
    dp = dst.load()
    for y in range(16):
        a = fill_patterns[i][y]
        for x in range(16):
            if ((a >> x) & 1) != 0:
                dp[x,y] = (0,0,0,255)
    pattern.append(dst)

def palette_color(x):
    p = palette[x]
    a = 0 if x == 0 else 255
    return (
        (((p>>8)&7) * 255) // 7,
        (((p>>4)&7) * 255) // 7,
        (((p>>0)&7) * 255) // 7,
        a )

def unpack_4bit(g, tw, th, stride, rows):
    def read16(x):
        return (g[x+0] << 8) | g[x+1]       
    cols = ((len(g) // stride) + rows - 1) // rows
    img = PIL.Image.new("RGB",(cols*tw,rows*th),(255,0,255))
    tiles = []
    for c in range(cols):
        for r in range(rows):
            od = ((c * rows) + r) * stride
            ox = c * tw
            oy = r * th
            tile = PIL.Image.new("RGBA",(tw,th),(255,0,255,0))
            pixels = tile.load()
            for y in range(th):
                for x in range(tw):
                    if (od + 8) >= len(g):
                        continue
                    p = 0
                    for plane in range(3,-1,-1):
                    #for plane in range(4):
                        m = read16(od+(plane*2))
                        p = (p << 1) | ((m >> (15-x)) & 1)
                    pixels[x,y] = palette_color(p)
                od = od + 8
            img.paste(tile,(ox,oy))
            tiles.append(tile)
    return (img, tiles)

def unpack_txt(b):
    s = ""
    addr = 0
    ss = ""
    def emit(x):
        e = ""
        if len(x) > 0:
            e += "%04X: " % addr
            e += ss + "\n"
        return e
    for i in range(len(b)):
        c = b[i]
        if c == 0:
            s += emit(ss)
            ss = ""
            addr = i+1
        else:
            pc = "*"
            if c >= 0x20 and c < 128:
                pc = chr(c)
            ss += pc
    s += emit(ss)
    return s

def hex(d):
    s = ""
    for v in d:
        s += " %02X" % v
    if len(s) > 0:
        s = s[1:]
    return s

def floortile(d,tf):
    if d[0] == 0xFF: # background tile
        return tf[d[1]]
    elif d[0] == 0x02: # pattern fill tile (colours are remapped instead of direct?)
        t = PIL.Image.new("RGBA",(16,16),palette_color(pattern_color_remap[d[2]]))
        p = pattern[d[1]]
        t.paste(p,(0,0),p)
        return t
    return tf[0] # ?

def leveldump(fn,count,d,tg,tf,numbers):   
    def read16(n):
        return (d[n+0] << 8) | d[n+1]
    # skip empty data
    if all(v==0 for v in d):
        return
    # not empty:
    iw = 256 if info else 16
    ih = 0 if info else 0
    img = PIL.Image.new("RGBA",(16*16+iw,8*16+ih),(0,0,0,0))
    # floor
    ft1 = floortile(d[0x10:0x13],tf)
    ft2 = floortile(d[0x13:0x16],tf)
    fl = [[0]*9 for x in range(16)]
    coord0 = d[0]
    c0 = coord0
    for i in range(1,16):
        # draw the walls (a cycle of nybble coordinates drawing straight lines)
        c1 = d[i]
        x = c0 & 15
        y = c0 >> 4
        xt = c1 & 15
        yt = c1 >> 4
        while (x != xt) or (y != yt):
            fl[x][y] = 1
            if x < xt:
                x += 1
            if x > xt:
                x -= 1
            if y < yt:
                y += 1
            if y > yt:
                y -= 1
        if (c1 == coord0):
            break # wall ends when it meets the starting coordinate
        c0 = c1
    for y in range(0,8):
        for x in range(0,16):
            # fill the interior
            if fl[x][y] != 0:
                continue
            x0c = 0
            x1c = 0
            y0c = 0
            y1c = 0
            for i in range(0,x):
                if fl[i][y] == 1:
                    x0c += 1
            for i in range(x+1,16):
                if fl[i][y] == 1:
                    x1c += 1
            for i in range(0,y):
                if fl[x][i] == 1:
                    y0c += 1
            for i in range(y+1,8):
                if fl[x][i] == 1:
                    y1c += 1
            if x0c < 1 or x1c < 1 or y0c < 1 or y1c < 1:
                continue
            fl[x][y] = 2
    for y in range(0,8):
        # draw floor
        for x in range(0,16):
            if fl[x][y] == 2:
                img.paste(ft2,(x*16,y*16))
            if fl[x][y] == 1:
                img.paste(ft1,(x*16,y*16))
    # items
    color = [
        # every item has 9 data bytes:
        # 0 - tile
        # 1 - ?
        # 2 - door text?
        # 3 - exit room number
        # 4 - book/sign text?
        # 5 - FF solid, FE locked, 01 filled, 00 empty ?
        # 6 - X coordinate
        # 7 - Y coordinate
        # 8 - always 01 ?
        (255,255,  0),(255,255,255),(255,255,255),
        (  0,255,  0),(255,255,255),(255,  0,  0),
        (200,128,128),(128,200,128),(  0,255,255) ]
    draw = PIL.ImageDraw.Draw(img,"RGBA")
    for i in range(14):
        item = d[31+(i*9):31+(i*9)+9]
        if all(v==0 for v in item):
            continue
        it = item[0]
        ix = item[6]
        iy = item[7]
        img.paste(tg[it],(ix*16,iy*16),tg[it])
        if info:
            #s = ("%2d: " % i) + hex(item)
            #draw.text(((16*16)+8,(i*9)+1),s,font=font)
            draw.text(((16*16)+8,(i*9)+1),"%2d:"%i,font=font)
            for j in range(9):
                draw.text(((16*16)+26+(j*20),(i*9)+1),hex(item[j:j+1]),fill=color[j],font=font)
            if numbers:
                draw.rectangle((ix*16+2,iy*16+3,ix*16+13,iy*16+11),fill=(0,0,0,64))
                draw.text((ix*16+2,iy*16+3),"%2d"%i,font=font)
            if (item[8] == 0): # hidden
                draw.rectangle((ix*16,iy*16,ix*16+15,iy*16+15),outline=color[8])
    fn = fn + "." + ("%02X" % count)
    if info:
        ip = 16*16+iw-50
        draw.text((ip, 5),hex([count]),fill=color[3],font=font)
        # wall tile
        draw.text((ip,14),hex(d[0x10:0x13]),font=font,fill=(128,128,128))
        # floor tile
        draw.text((ip,23),hex(d[0x13:0x16]),font=font,fill=(128,128,128))
        # 9 more mystery bytes, the last one seems to indicate darkness
        # but the rest probably have to do with NPCs or enemies?
        draw.text((ip,32),hex(d[0x16:0x19]),font=font)
        draw.text((ip,41),hex(d[0x19:0x1C]),font=font)
        draw.text((ip,50),hex(d[0x1C:0x1F]),font=font)
    else:
        pos = (0,0)
        for x in range(16):
            for y in range(8):
                if fl[x][y] == 1:
                    if pos[0] < x:
                        pos = (x,y)
        if not numbers:
            pos = (pos[0]-1,pos[1]+1) # shift for world map which always has a black border
        draw.text(((pos[0]+1)*16+1,pos[1]*16+0),"%02X"%count,font=font,fill=(0,255,0))
    if info:
        img.save("info/" + fn + ".png")
    else:
        img.save("out/" + fn + ".png")
    print((fn + " ($%4X)" % (count * 157)))
    return

def levelsdump(fn,tg,tf,numbers=True):
    dat = open(fn,"rb").read()
    start = 0
    stride = 157 # each room is 157 bytes
    extra = 0
    count = 0
    while (start + stride + extra) <= len(dat):
        leveldump(fn,count,dat[start:start+stride+extra],tg,tf,numbers)
        count += 1
        start += stride
        if count >= 0x50:
            # there is extra data at the end of the file, unknown purpose
            break
    return

palimg = PIL.Image.new("RGB",(128,128))
paldrw = PIL.ImageDraw.Draw(palimg,"RGBA")
for y in range(0,4):
    for x in range(0,4):
        ci = x+(y*4)
        c = palette_color(ci)
        paldrw.rectangle((x*32,y*32,x*32+31,y*32+31),fill=c)
        paldrw.rectangle((x*32,y*32,x*32+ 7,y*32+ 8),fill=(0,0,0,128))
        paldrw.text((x*32+1,y*32-1),"%X"%ci)
palimg.save("palette.png")

(grafxpng, grafxt) = unpack_4bit(grafxdat,16,16,(8*16)+2,16)
(fillpng, fillt) = unpack_4bit(filldat, 16,16,(8*16)+2,16)
grafxpng.save("GRAFX.DAT.png")
fillpng.save("FILL.DAT.png")
open("AVECTA.PRG.txt","wt").write(unpack_txt(prg))

levelsdump("CAVE.DAT",grafxt,fillt)
levelsdump("TROND.DAT",grafxt,fillt)
levelsdump("DEMON.DAT",grafxt,fillt)
# These three seem to differ only by their "mystery" bytes.
# Not sure when they are used by the game.
# Presumably they alter which characers/NPCs appear in various rooms.
#levelsdump("DCAVE.DAT",grafxt,fillt)
#levelsdump("DTROND.DAT",grafxt,fillt)
#levelsdump("DDEMON.DAT",grafxt,fillt)
levelsdump("OUTSIDE.DAT",fillt[20:],fillt,numbers=False)