Last active
December 13, 2018 08:53
-
-
Save bbbradsmith/299b92b664672e7fc8f4ee8855f816d9 to your computer and use it in GitHub Desktop.
Avecta (Atari ST) data dumper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment