Galaxian shape extractor to json, and json to png
authorNick Downing <nick@ndcode.org>
Tue, 28 Jun 2022 13:39:06 +0000 (23:39 +1000)
committerNick Downing <nick@ndcode.org>
Tue, 28 Jun 2022 13:39:06 +0000 (23:39 +1000)
.gitignore
galaxian/Makefile
galaxian/galaxian.txt
galaxian/shape_extract.py [new file with mode: 0755]
galaxian/shape_extract_png.py [new file with mode: 0755]

index b326429..bc18aed 100644 (file)
@@ -16,6 +16,8 @@
 /emu_65c02/cg_default/*.ppm
 /galaxian/alien_typhoon.asm
 /galaxian/galaxian.asm
+/galaxian/galaxian_shape.json
+/galaxian/galaxian_shape.png
 /orig/APPLE Computer and Peripheral Card Roms Collection.zip
 /orig/Alien_Typhoon_1981_Starcraft.do
 /orig/Apple_DOS_v3.3_1980_Apple.do
index 7b59dc2..9d881d9 100644 (file)
@@ -7,9 +7,16 @@ LOAD_ADDR=0x800
 
 .PHONY: all
 all: \
+galaxian_shape.png \
 galaxian.asm \
 alien_typhoon.asm \
 
+galaxian_shape.png: galaxian_shape.json
+       ./shape_extract_png.py $^ $@
+
+galaxian_shape.json: galaxian.txt ../loader/galaxian.ihx
+       ./shape_extract.py $^ $@
+
 galaxian.asm: \
 galaxian_trace.txt \
 galaxian.txt \
@@ -34,5 +41,7 @@ clean:
 *.o \
 *.rel \
 *.rst \
+galaxian_shape.png \
+galaxian_shape.json \
 galaxian.asm \
 alien_typhoon.asm
index 08f7f26..bcc7176 100644 (file)
@@ -224,13 +224,10 @@ items
 0x3fd0,0x0028,VIDEO_LINE_BF,byte
 0x486e,0x0001,velocity_y,byte
 0x4870,0x0001,velocity_x_lo,byte
-0x7280,0x00d2,shape_ptr_lo_7280,byte
-0x7480,0x00d2,shape_ptr_lo_7480,byte
-0x7600,0x00d2,shape_ptr_hi_7600,byte
-0x7800,0x00d2,shape_ptr_hi_7800,byte
-0x7980,0x00d2,shape_width_bytes_7980,byte
-0x7b80,0x00d2,shape_width_bytes_7b80,byte
-0x7d80,0x0053,shape_height_bytes_7d80,byte
+0x7280,0x0380,shape_data_ptr_lo,byte
+0x7600,0x0380,shape_data_ptr_hi,byte
+0x7980,0x0380,shape_width_bytes,byte
+0x7d80,0x0080,shape_height,byte
 0x8cf1,0x0008,y_8cf1,byte
 0x8d01,0x0008,velocity_hi_8d01,byte
 0x8d11,0x0008,x_hi_8d11,byte
@@ -249,14 +246,14 @@ items
 0x93de,0x0001,,code_ign # accessing video_line_table before clipping y
 # returns a = quotient, y = remainder, cf = 0
 0x93f6,0x0001,div_a_by_7,code
-# the value from shape_ptr_lo/hi is relative to this value
-0x9414,0x0002,shape_ptr_base,word,byte
+# the value from shape_data_ptr_lo/hi is relative to this value
+0x9414,0x0002,shape_data_ptr_base,word,byte
 0x9416,0x0007,x2_mod7_table,byte
 0x941d,0x0007,x2_div7_table,byte
 # these contain the pointer for shift count 0, shape 0
 # the byte accessed will be base + shift count * 0x80 + shape
-0x9424,0x0002,shape_ptr_lo_base,word,byte
-0x9426,0x0002,shape_ptr_hi_base,word,byte
+0x9424,0x0002,shape_data_ptr_lo_base,word,byte
+0x9426,0x0002,shape_data_ptr_hi_base,word,byte
 0x9428,0x0002,shape_width_bytes_base,word,byte
 # takes x = 0 draw, x = 1 erase
 # reads draw_erase_(x0|y0|shape)
diff --git a/galaxian/shape_extract.py b/galaxian/shape_extract.py
new file mode 100755 (executable)
index 0000000..3ba04cc
--- /dev/null
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+
+import json
+import sys
+from intelhex import IntelHex
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+
+pixel = False
+if len(sys.argv) < 4:
+  print(f'usage: {sys.argv[0]:s} addrs.txt in.ihx out.json')
+  sys.exit(EXIT_FAILURE)
+addrs_txt = sys.argv[1]
+in_ihx = sys.argv[2]
+out_json = sys.argv[3]
+
+print('reading addrs')
+shape_tables = {}
+shape_data_ptr_base = -1
+shapes = {}
+with open(addrs_txt) as fin:
+  def get_line():
+    while True:
+      line = fin.readline()
+      if len(line) == 0:
+        return []
+      i = line.find('#')
+      if i >= 0:
+        line = line[:i]
+      fields = line.strip().split(',')
+      if fields != ['']:
+        #print('fields', fields)
+        return fields
+
+  fields = get_line()
+  while len(fields):
+    assert len(fields) == 1
+    section = fields[0]
+    print(section)
+
+    if section == 'items':
+      fields = get_line()
+      while len(fields) >= 2:
+        assert len(fields) >= 4
+        addr = int(fields[0], 0)
+        size = int(fields[1], 0)
+        name = fields[2]
+        _type = fields[3]
+        if _type == 'byte' and name[:6] == 'shape_':
+          shape_tables[name[6:]] = (addr, size // 0x80)
+        elif _type == 'word' and name == 'shape_data_ptr_base':
+          shape_data_ptr_base = addr
+        fields = get_line()
+      continue
+
+    if section == 'shapes':
+      fields = get_line()
+      while len(fields) >= 2:
+        assert len(fields) == 2
+        index = int(fields[0], 0)
+        name = fields[1]
+        shapes[index] = name
+        fields = get_line()
+      continue
+
+    # unknown section, skip
+    fields = get_line()
+    while len(fields) >= 2:
+      fields = get_line()
+assert shape_data_ptr_base != -1
+
+print('reading ihx')
+intelhex = IntelHex(in_ihx)
+segments = [j for i in intelhex.segments() for j in i]
+for i in range(0, len(segments), 2):
+  print(f'[{segments[i]:04x}, {segments[i + 1]:04x})')
+
+print('extracting')
+data = []
+for i in range(0x80):
+  name = f'shape_{i:02x}'
+  if i in shapes:
+    name += '_' + shapes[i]
+  data.append({'name': name})
+
+i = 0
+shape_tables1 = list(shape_tables.items())
+while i < len(shape_tables1):
+  if (
+    i + 2 <= len(shape_tables1) and
+      shape_tables1[i][0][-3:] == '_lo' and
+      shape_tables1[i + 1][0][-3:] == '_hi'
+  ):
+    table, (addr_lo, shifts) = shape_tables1[i]
+    _, (addr_hi, _) = shape_tables1[i + 1]
+    table = table[:-3]
+    for j in range(0x80):
+      value = [
+        intelhex[addr_lo + j + k * 0x80] |
+          (intelhex[addr_hi + j + k * 0x80] << 8)
+        for k in range(shifts)
+      ]
+      data[j][table] = [f'0x{k:04x}' for k in value]
+    i += 2
+  else:
+    table, (addr, shifts) = shape_tables1[i]
+    for j in range(0x80):
+      value = [intelhex[addr + j + k * 0x80] for k in range(shifts)]
+      data[j][table] = [str(k) for k in value]
+    i += 1
+
+for i in range(0x80):
+  height = int(data[i]['height'][0], 0)
+  addr0 = int(data[i]['data_ptr'][0], 0)
+
+  # only try to extract if pointers are sensible
+  data1 = None
+  addr = addr0
+  for j in range(6):
+    width_bytes = int(data[i]['width_bytes'][j], 0)
+    addr += width_bytes * height
+    if addr != int(data[i]['data_ptr'][j + 1], 0):
+      break
+  else:
+    # good to go
+    data1 = []
+    addr = (
+      addr0 +
+        intelhex[shape_data_ptr_base] +
+        (intelhex[shape_data_ptr_base + 1] << 8)
+    )
+    for j in range(7):
+      width_bytes = int(data[i]['width_bytes'][j], 0)
+      data1.append(
+        [
+          [
+            f'0x{intelhex[addr + k * width_bytes + l]:02x}'
+            for l in range(width_bytes)
+          ]
+          for k in range(height)
+        ]
+      )
+      addr += width_bytes * height
+  data[i]['data'] = data1
+
+print('writing json')
+with open(out_json, 'w') as fout:
+  json.dump(data, fout, indent = 2)
diff --git a/galaxian/shape_extract_png.py b/galaxian/shape_extract_png.py
new file mode 100755 (executable)
index 0000000..43ea34a
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+
+import json
+import numpy
+import sys
+import PIL.Image
+
+EXIT_SUCCESS = 0
+EXIT_FAILURE = 1
+
+PITCH_X = 64
+PITCH_Y = 32
+
+# see palette.py
+PALETTE = numpy.array(
+  [
+    [0x00, 0x00, 0x00],
+    [0xbc, 0x00, 0x89],
+    [0x00, 0x00, 0xbc],
+    [0xbc, 0x00, 0xe1],
+    [0x00, 0xbc, 0x89],
+    [0x80, 0x80, 0x80], #[0xbc, 0xbc, 0xbc],
+    [0x00, 0xbc, 0xe1],
+    [0xbc, 0xbc, 0xff],
+    [0xbc, 0xbc, 0x00],
+    [0xff, 0xbc, 0x89],
+    [0xc0, 0xc0, 0xc0], #[0xbc, 0xbc, 0xbc],
+    [0xff, 0xbc, 0xe1],
+    [0xbc, 0xff, 0x89],
+    [0xff, 0xff, 0xbc],
+    [0xbc, 0xff, 0xe1],
+    [0xff, 0xff, 0xff],
+  ],
+  numpy.uint8
+)
+
+if len(sys.argv) < 3:
+  print(f'usage: {sys.argv[0]:s} in.json out.png')
+  sys.exit(EXIT_FAILURE)
+in_json = sys.argv[1]
+out_png = sys.argv[2]
+
+print('reading json')
+with open(in_json) as fin:
+  data = json.load(fin)
+
+image_out = numpy.zeros((PITCH_Y * 16, PITCH_X * 8), numpy.uint8)
+
+for i in range(0x80):
+  j = i & 7
+  k = i >> 3
+  x = j * PITCH_X
+  y = k * PITCH_Y
+  bg = 0xa if (j ^ k) & 1 else 5
+  image_out[y:y + PITCH_Y, x:x + PITCH_X] = bg
+
+  data1 = data[i]['data']
+  if data1 is not None:
+    shape = numpy.array(
+      [[int(k, 0) for k in j] for j in data1[0]],
+      numpy.uint8
+    )
+    ys = shape.shape[0]
+    xs = shape.shape[1] * 7
+
+    # extract high bits, check consistent on each line
+    hibit = (shape >> 7).astype(bool)
+    assert numpy.all(hibit == hibit[:, :1])
+    hibit = hibit[:, 0]
+    # extract data bits
+    shape = (
+      (
+        shape[:, :, numpy.newaxis] >>
+          numpy.arange(
+            7,
+            dtype = numpy.int32
+          )[numpy.newaxis, numpy.newaxis, :]
+      ) & 1
+    ).astype(bool).reshape((ys, xs))
+
+    # check remaining (shifted) shapes are as we expect
+    for j in range(1, 7):
+      shape1 = numpy.array(
+        [[int(l, 0) for l in k] for k in data1[j]],
+        numpy.uint8
+      )
+      ys1 = shape1.shape[0]
+      assert ys1 == ys
+      xs1 = shape1.shape[1] * 7
+      assert xs1 >= xs
+
+      # extract high bits, check consistent on each line
+      hibit1 = (shape1 >> 7).astype(bool)
+      assert numpy.all(hibit1 == hibit1[:, :1])
+      hibit1 = hibit1[:, 0]
+      assert numpy.all(hibit1 == hibit)
+
+      # extract data bits
+      shape1 = (
+        (
+          shape1[:, :, numpy.newaxis] >>
+            numpy.arange(
+              7,
+              dtype = numpy.int32
+            )[numpy.newaxis, numpy.newaxis, :]
+        ) & 1
+      ).astype(bool).reshape((ys1, xs1))
+
+      # compare data bits
+      k = min(xs, xs1 - j)
+      assert not numpy.any(shape[:, k:])
+      assert not numpy.any(shape1[:, :j])
+      assert numpy.all(shape1[:, j:j + k] == shape[:, :k])
+      assert not numpy.any(shape1[:, j + k:])
+
+    # double the pixels and apply the shift by hibit value
+    shape1 = numpy.zeros((ys * 2, xs * 2 + 1), bool)
+    for j in range(ys):
+      k = int(hibit[j])
+      shape1[j * 2:j * 2 + 2, k:-1:2] = shape[j:j + 1, :]
+      shape1[j * 2:j * 2 + 2, k + 1::2] = shape[j:j + 1, :]
+    shape = shape1
+    ys = ys * 2
+    xs = xs * 2 + 1
+
+    image_out[y:y + ys, x:x + xs] = shape * 0xf
+
+print('writing png')
+image_out_pil = PIL.Image.new(
+  'P',
+  (image_out.shape[1], image_out.shape[0]),
+  None
+)
+image_out_pil.frombytes(image_out.tobytes())
+image_out_pil.putpalette(list(PALETTE.reshape((0x30,))))
+image_out_pil.save(out_png)