Skip to content

Instantly share code, notes, and snippets.

@bbbradsmith
Last active July 19, 2023 04:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bbbradsmith/e5e9459d400d1cc6f34501429d916ae6 to your computer and use it in GitHub Desktop.
Save bbbradsmith/e5e9459d400d1cc6f34501429d916ae6 to your computer and use it in GitHub Desktop.
Milva DOS image dumper (Desafio, Kick Boxing Street) and re-compressor
# Dumps image data from Milva DOS game,
# as well as Desafio and Kick Boxing Street
# from Ediciones Manali.
#
# https://archive.org/details/msdos_Milva_1993
# https://www.old-games.ru/game/4884.html (Desafio)
# https://www.old-games.ru/game/4532.html (Kick Boxing)
#
# If you successfully use this for their other games,
# send me the dump list and I can add it.
import PIL.Image
import struct
PALETTE = [
0x00,0x00,0x00, # black
0x55,0xFF,0xFF, # CGA cyan
0xFF,0x55,0xFF, # CGA magenta
0xFF,0xFF,0xFF, # CGA white
0xFF,0xFF,0x00, # pure yellow (border)
]
# There are two file extensions to look for: SPR and PIC.
# I think these are the same format, but PIC implies a single 320x200 image.
#
# I could not find sprite size definitions in the data files.
# They may have been hard-coded in the EXE?
# I ended up using Binxelview, with 2BPP and reverse byte.
#
# If a raw (decompressed) file, start at offset 0,
# otherwise with an uncompressed file start at offset 7 to skip the header.
#
# I find a width that fits the current sprite, then find a height.
# I add it to the list, then click the down scroll arrow to move
# to the next sprite.
#
# Sometimes after I find the width for the next sprite,
# it seems misaligned. In this case, go back to the width/height setting
# of the previous sprite and click the up scroll arrow to return to it.
# Most likely it needs 1 more or 1 less line of height.
#
# Repeat until I've found them all.
#
# If I made a mistake, review the logs and the generated PNG files.
# They logs can tell you the data offsets of each packet.
#
# Compressed files I first start with an empty list,
# or maybe just (320,200,1) as a guess in case they're a full screen image,
# but either way this tool will dump a ".raw" file of the decompressed data.
# Binxelview can then be used with the decompressed raw file instead.
#
# https://github.com/bbbradsmith/binxelview
DUMP_MILVA = [ # all files uncomprssed except PANTAGAM.SPR
("PANTAGAM.SPR",322,[(320,200,3),(320,40,1)]), # contest screens (compressed)
("MILVAMAR.PIC",322,[(320,200,1)]), # gameplay frame
("MILVAPRE.PIC",322,[(320,200,1)]), # title screen
("MILBLO.SPR",201,[(24,24,40)]), # blocks used for gameplay level
("MILVA.SPR",246,[ # sprites used for gameplay
(48,50,10), # milva
(16,5,1), # bullet
(8,10,1), # upward aiming gun tip
(40,42,1), # lizard
(40,31,4), # rat
(32,60,2), # snake
(16,3,1), # bullet
(16,5,1), # ememy pellet small
(16,13,1), # enemy pellet large/round
(16,24,1), # falling arrow?
(48,44,2), # snake milva?
(72,18,2), # snake
(48,23,4), # lizard bird
(32,27,2), # explode small
(48,45,2), # explode large
(32,45,6), # grass
(40,42,2), # pit snake
(48,9,1), # fireball
(80,72,4), # xenomorph
(96,54,1), # xenomorph crouch
(128,71,1), # xenomorph queen
(40,22,1), # cannon?
]),
]
DUMP_DESAFIO = [ # all files were compressed
("DESAFIO.SPR",503,[
(72,68,2),(96,68,2),(64,68,1),(96,67,1), # player
(32,20,1), # flame swipe
(32,8,1), # fireball
(32,23,1), # "bank" money/soap?
(104,103,1), (128,103,1), (120,132,1), # player jumping
(80,59,1), # enemy with gun
(24,9,1), # poop?
(88,112,1), (120,108,1), (96,121,1), (168,74,1), # enemy flying back
(72,57,2), (72,68,2), # alien
(64,90,1), (120,23,1), (136,23,1), # dogsnake
]),
("DESENTFA.SPR",322,[
(320,200,3), # full screen alien
(64,94,2), (72,62,1), # player
(24,21,1), # splatter
(32,32,1), # 100 points star
(16,21,1), # flame
(32,20,1), (40,28,1), (40,31,1), (56,41,1), # exploding pod
]),
("DTF1.SPR",322,[(320,119,1)]),
("DTF2.SPR",322,[(320,119,1)]),
("DTF3.SPR",322,[(320,119,1)]),
("DTF4.SPR",322,[(320,119,1)]),
("DTF5.SPR",322,[(320,119,1)]),
("DTPIC.SPR",322,[(320,200,3)]),
("DTMARCO.PIC",322,[(320,200,1)]),
]
DUMP_KICK = [ # all files compressed except KPAI1-3.SPR
("KICKMAR.PIC",322,[(320,200,1)]), # gameplay frame
("KICKPRE.PIC",322,[(320,200,1)]), # title screen
("KFIN.SPR",322,[(240,136,2)]), # victory screens
("KICKYO.SPR",322,[ # player
(24,66,1), (32,66,2), (24,66,2), # walking
(40,66,1), # posing
(88,20,1), # lying down
(48,41,1), (40,58,1), (40,65,1), # knocked back
(48,65,1), (56,65,2), # punching
(64,65,1), (64,62,1), # kicking
(48,58,1), (72,53,1), # jump kick
(104,37,1), # split kick
(48,65,1), (56,62,1), # roundhouse
(48,36,1), # crouch
(24,18,1), # pow
(40,20,1), # tire
(56,28,1), # box
(16,11,1), # star
(24,7,1), # knife
(32,25,1), # tin can
(32,10,1), # AHHH
(24,8,1), # 100
(16,9,1), # 50
]),
("KMALO1.SPR",322,[ # enemy
(24,66,1), (32,66,2), (24,66,2), # walk
(40,66,1), # facing
(88,20,1), # lying
(48,41,1), (40,58,1), (40,65,1), # knocked back
(48,65,1), (56,66,1), (56,64,1), # punching
(64,65,1), (64,62,1), # kicking
]),
("KMALO2.SPR",322,[
(24,66,1), (32,66,2), (24,66,2),
(40,66,1),
(88,21,1),
(48,41,1), (48,58,1), (40,65,1),
(48,69,1), (80,75,1), (88,64,1),
(64,65,1), (72,62,1),
]),
("KMALO3.SPR",322,[
(24,66,1), (32,66,2), (24,66,2),
(40,66,1),
(88,20,1),
(48,43,1), (40,62,1), (40,65,1),
(48,65,1), (56,66,1), (56,64,1),
(64,65,1), (64,63,1),
]),
("KMALO4.SPR",367,[
(24,66,1), (32,66,2), (32,66,1), (24,66,1),
(64,66,1),
(88,20,1),
(56,46,1), (48,61,1), (48,66,1),
(56,65,1), (72,66,1), (72,64,1),
(64,66,1), (64,65,1),
]),
("KMALO5.SPR",322,[
(24,66,1), (32,66,2), (24,66,2),
(40,66,1),
(88,19,1),
(48,43,1), (40,61,1), (40,65,1),
(40,66,1), (56,67,1), (56,65,1),
(64,65,1), (64,63,1),
(56,56,1), (72,53,1), # flying kick
]),
("KMALO6.SPR",322,[
(24,66,1), (32,66,2), (24,66,2),
(40,66,1),
(88,20,1),
(48,42,1), (40,61,1), (48,65,1),
(40,66,1), (56,67,1), (56,65,1),
(56,57,1), (64,63,1),
(56,56,1), (72,53,1), # flying kick
]),
("KMALO7.SPR",377,[
(24,73,1), (40,71,1), (32,71,1), (24,72,2),
(48,72,1),
(112,25,1),
(64,44,1), (48,68,2), # knocked back
(48,70,1), (56,70,2), # punching
(64,71,1), (64,70,1), # kicking
(32,81,1), (48,72,1), # captain-kirk move
]),
("KPAI1.SPR",322,[(320,136,2)]), # streets (uncompressed)
("KPAI2.SPR",322,[(320,136,2)]),
("KPAI3.SPR",322,[(320,136,2)]),
]
DUMPLIST = DUMP_MILVA
#DUMPLIST = DUMP_DESAFIO
#DUMPLIST = DUMP_KICK
# assemble images into gallery with 1px border
def gallery(imgs, width=256, border=0):
# determine an image size that can contain everything
iw = width
ih = 1
# determine a height for the current row
rw = 1 # minimum width of row
rh = 0 # height of row
rc = 0 # count in row
for img in imgs:
nrw = rw + img.width + 1
nrh = img.height + 1
if nrw > iw and rc == 0: # expand the canvas if a single image is too wide
iw = nrw
if nrw > iw: # row break when the edge would be crossed
ih += rh # finish row
rh = nrh # start new row with this image
rw = 1 + img.width + 1
if (rw > iw):
iw = rw # expand if too wide
rc = 1
else: # fits in the current row
rw = nrw
if (nrh > rh):
rh = nrh
rc += 1
if (rc > 0): # finish last row
ih += rh
# assemble the image
gimg = PIL.Image.new(imgs[0].mode,(iw,ih),border)
rx = 1
ry = 1
rh = 0
for img in imgs:
nrx = rx + img.width + 1
nrh = img.height + 1
if nrx > iw: # break
ry += rh
rx = 1
gimg.paste(img,(rx,ry))
rx = 1 + img.width + 1
rh = nrh
else:
gimg.paste(img,(rx,ry))
rx = nrx
if (nrh > rh):
rh = nrh
return gimg
# Compressed file format. Has a 3 letter signature "AGD".
# This is an LZ77-like compression format,
# consisting of alternating commands of raw byte copy from source,
# and back-references to previously decompressed data.
#
# The data is stored in reverse order.
# The first source byte is at the end of the data,
# and bytes are decompressed to the end of the output buffer first,
# both working backwards.
#
# A control bitstream is read, bit by bit.
# The bits for this stream are stored 1 byte at a time,
# whenever there are no remaining bits in the control byte,
# a new byte is immediately read from the source data.
# Bits are read from the control bit high-bit first.
# The first byte of data (at the end of the input file)
# contains only 7 control bits, and the least significant bit
# contains a 1, which serves as a sentinel that assists the
# implementation in keeping track of the bits.
# (The control byte is constantly held in the AL register.
# Each bit is shifted into CF, and when CF=1 and AL=0,
# this indicates we should read a new control byte and
# shift again. Whenever a new byte is read, the sentinel 1
# is rotated into the least significant bit again, ensuring
# that AL!=0 until all 8 bits have been read out.
#
# Decompression works by reading commands from the control bitstream.
# There are two types of commands which alternate:
# 1. Copy raw bytes from source to destination.
# 2. Copy previously decompressed bytes from destination to itself.
# After both command types have been processed, it repeats until
# the destination is full. The back-reference commands copy from
# the last-byte first, but unfortunately an overlapped-copy is not permitted,
# because the count is always added to the offset (+dx) for back references.
# Overlapped copying would have allowed more efficient repeated byte/pattern encoding.
def decompress_milva(d,DEBUG=False):
header = struct.unpack("<BBBHH",d[0:7])
print("Compression header: %02X %02X %02X %04X %04X" % header)
d = d[7:]
if (header[0] != ord('A') or header[1] != ord('G') or header[2] != ord('D')):
print("Warning: compression header should begin with AGD (41 47 44).")
if (header[4] != len(d)):
print("Warning: compression header input length (%04X) does not match file data size (%04X)" % (header[4], len(d)))
do = bytearray([0]*header[3])
si = len(d)-1
di = header[3]-1
al = 0
control_debug_string = ""
def write_byte(b): # writes 1 byte to the output
nonlocal di, do
if (di >= 0):
if DEBUG: print(" Out: [%4X] = %02X" % (di,b))
do[di] = b
di -= 1
def write_copy(bx): # copies a decoded byte from bx bytes previous
nonlocal di, do
ci = di + bx
b = 0
if (ci < len(do)):
b = do[ci]
if DEBUG: print("Copy: [%4X] = %02X" % (ci,b))
write_byte(b)
def read_byte(): # reads 1 byte from the input
nonlocal d, si
b = 0
if (si >= 0):
b = d[si]
if DEBUG: print(" In: [%4X] = %02X" % (si,b))
si -= 1
return b
def control_bit(): # reads 1 bit of the control bitstream
nonlocal al, control_debug_string
carry = (al >> 7)
al = (al << 1) & 0xFF
if (al == 0):
al = (read_byte() << 1) | carry
carry = al >> 8
al &= 0xFF
control_debug_string += "1" if (carry != 0) else "0"
if DEBUG: print("Control bit: %d (%02X)" % (1 if (carry!=0) else 0, al))
return (carry != 0)
def control_bits(cx): # reads cx bits of the control bitsream
nonlocal control_debug_string
control_debug_string += "("
bx = 0
for c in range(cx):
bx <<= 1
bx |= 1 if control_bit() else 0
control_debug_string += ")"
return bx
def control_debug():
nonlocal control_debug_string
if DEBUG: print("Control command: " + control_debug_string)
control_debug_string = ""
# decompress the data
# t
al = read_byte()
if (al & 1) == 0:
# first control byte is 7-bits + 1 "sentinel" bit which must be 1,
#
print("Warning: first control byte missing sentinal in bit 0: %02X" % (al))
while di >= 0 and si >= 0:
# source byte copy command:
# 0 -> count = 0
# 10 -> count = 1
# 11xx -> count = xx+1 (2-4)
# 1100xx -> count = xx+4 (5-7)
# 110000xxx -> count = xxx+7 8-15
# 110000000xxxxxxxxxx -> count = xxxxxxxxxx+15 (16-1038)
count = 0
if control_bit():
count = 1
if control_bit():
count = 1 + control_bits(2)
if count <= 1:
count = 4 + control_bits(2)
if count <= 4:
count = 7 + control_bits(3)
if count <= 7:
count = 15 + control_bits(10)
control_debug()
if DEBUG: print("--> Source copy: %d bytes" % count)
for i in range(count):
write_byte(read_byte())
if (di < 0):
break
# back reference copy command:
# These commands set 3 registers to control the back reference:
# bx = base offset from current destination (di)
# cx = additional variable offset: read cx bits from control stream and add to bx
# dx = number of bytes to copy
# back reference copy prefix:
# 00 -> bx = 0, cx = 6, dx = 2 (copy 2 bytes from offset 0-63)
# 01 -> bx = 64, cx = 10, dx = 2 (copy 2 bytes from offset 64-1087)
# 10 -> dx = 3, variable bx/cx (copy 3 bytes...)
# 110x -> dx = x+4, variable bx/cx (copy 4-5 bytes...)
# 1110xx -> dx = xx+6, variable bx/cx (copy 6-9 bytes...)
# 1111xxxxxxxxxx -> dx = xxxxxxxxxx+10, variable bx/cx (copy 10-1033 bytes...)
# back reference variable bx/cx suffix:
# 0 -> bx = 0, cx = 4 (offset 0-15)
# 10 -> bx = 16 ($10), cx = 8 (offset 16-271)
# 110 -> bx = 272 ($110), cx = 12 (offset 272-4367)
# 111 -> bx = 4368 ($1110), cx = 15 (offset 4368-37135)
# back reference execution:
# source = di + bx + cx control bits + dx
# length = dx
# for length iterations:
# copy source to di
# --di, --source
bx = 0
cx = 0
dx = 0
if not control_bit(): # 00
bx = 0
cx = 6
dx = 2
if control_bit(): # 01
bx = 64
cx = 10
dx = 2
else: # 1
dx = 3 # 10
if control_bit(): # 11
if not control_bit(): # 110
dx = control_bits(1) + 4
else: # 111
if not control_bit(): # 1110
dx = control_bits(2) + 6
else: # 1111
dx = control_bits(10) + 10
# variable suffix
control_debug()
bx = 0
cx = 4 # 0
if control_bit(): # 1
bx = 0x0010
cx = 8 # 10
if control_bit(): # 11
bx = 0x0110
cx = 12 # 110
if control_bit(): # 111
bx = 0x1110
cx = 15
# apply back reference copy
control_debug()
offset = bx + dx
if (cx > 0):
offset += control_bits(cx)
control_debug()
if DEBUG: print("--> Back copy: %d bytes at %d = %d + %d (%d bits)" % (dx,offset,bx,offset-bx,cx))
for i in range(dx):
write_copy(offset)
# finished, do a few extra checks
if len(do) != header[3]:
print("Warning: decompressed data does not match expected size?");
if si >= 0:
print("Warning: %d unused bytes of input compressed data?" % (si+1))
return do
# Dump graphics from Milva data files.
# Uncompressed format is simple, basically just chunky CGA bytes,
# and no other data? Compressed is as above.
# Had to manually come up with image sizes, not sure where they're
# stored in the game... strongly suspected MILVA.TBC but couldn't
# find anything suitable in there.
# (First 2 or 3 bytes of header might be a file type indicator,
# the next 2 bytes are a mystery, and after that are the data size,
# which should match the file size + 8 bytes (7 byte header + 1 EOF byte)
def dump_milva(filename,packets,width=256):
imgs = []
print(filename)
d = open(filename,"rb").read()
header = struct.unpack("<BBBBBH",d[0:7])
print("Header: %02X %02X %02X %02X %02X %04X" % header)
eof = header[5] + 7
if len(d) != eof+1:
print("Warning: file size: %d expected: %d" % (len(d),eof+1))
if d[eof] != 0x1A:
print("Warning: expected EOF (1A) at %X" % (eof))
pos = 7
compressed = False
if header[2] == 0x60:
dc = decompress_milva(d[7:-1])
decompout = filename + ".raw"
open(decompout,"wb").write(dc)
print("Decompressed to: " + decompout)
d = dc
eof = len(d)
pos = 0
compressed = True
cancel = False
for (pw,ph,pc) in packets:
for c in range(pc):
print("%02d: %06X %d x %d" % (len(imgs),pos,pw,ph))
img = PIL.Image.new("P",(pw,ph),4)
for y in range(ph):
for x in range(pw):
dx = x // 4
db = 2 * (3 - (x % 4))
if (pos+dx >= len(d)):
print("Error: out of data, packet %d (%d,%d,%d of %d)" % (len(imgs),pw,ph,c,pc))
cancel = True
break
p = (d[pos+dx] >> db) & 3
img.putpixel((x,y),p)
if cancel: break
pos += pw // 4
if cancel: break
imgs.append(img)
if cancel: break
if pos != eof and ((not compressed) or pos != eof-1): # compressed data often has 1 extra byte
print("Warning: more %sdata after packet list at %X (%d bytes)" % ("raw " if compressed else "", pos, eof-pos))
if len(imgs) > 0:
gimg = gallery(imgs,width,4)
gimg.putpalette(PALETTE)
fileout = filename + ".png"
gimg.save(fileout)
print(fileout)
else:
print("No images decoded.")
print()
for (filename,width,packets) in DUMPLIST:
dump_milva(filename,packets,width)
# Repacks image data for Milva DOS game,
# or Desafio, Kick Boxing Street, etc.
# from Ediciones Manali.
#
# For format information, see the dumping script that does the reverse:
# https://gist.github.com/bbbradsmith/e5e9459d400d1cc6f34501429d916ae6
#
# Usage:
# Use the dump script to unpack the images.
# Edit the generated PNG images. (Don't change any sprite dimensions.)
# Use this script to re-pack the images.
import PIL.Image
import struct
import os
# uses uncompressed format if False
COMPRESS = True
# speeds up compression by ignoring long-distance matches
COMPRESS_FAST = False
PALETTE = [
0x00,0x00,0x00, # black
0x55,0xFF,0xFF, # CGA cyan
0xFF,0x55,0xFF, # CGA magenta
0xFF,0xFF,0xFF, # CGA white
0xFF,0xFF,0x00, # pure yellow (border)
]
# load image and convert to palette if needed
def img_load(filename, palette):
src = PIL.Image.open(filename)
print("Image type: %d x %d (%s)" % (src.width, src.height, src.mode))
if (src.mode == "P"): # already in palette format
return src
if src.mode != "RGB" and src.mode != "RGBA":
print("Image must be P (palette), RGB, or RGBA type.")
return None
# convert with nearest-match
linpal = palette
palette = []
for i in range(0,len(linpal),3):
palette.append(tuple(linpal[i:i+3]))
dst = PIL.Image.new("P",src.size,color=0)
for y in range(src.size[1]):
for x in range(src.size[0]):
p = src.getpixel((x,y))
mag = ((255**2)*3)+1
mat = 0
for i in range(len(palette)):
m = sum([(a-b)**2 for (a,b) in zip(p,palette[i])])
if m < mag: # better match
mat = i
mag = m
if m == 0: # perfect match
break
dst.putpixel((x,y),mat)
dst.putpalette(linpal)
#dst.save(filename+".pal.png") # for testing
return dst
# compress a Milva file
def milva_compress(d,DEBUG=False):
d.append(0) # all compressed files seemed to have an extra 0 byte on the end
d.reverse()
control_bits = [] # control bitstream
packets = [] # data packets interleaved with control stream
# internal utilities
def add_bit(b):
nonlocal control_bits
assert(b == 0 or b == 1)
control_bits.append(b)
def add_bits(na):
for b in na: add_bit(b)
def add_bitnum(n,bits):
assert(n<(1<<bits))
for i in range(bits):
add_bit((n>>(bits-(1+i)))&1)
def control_raw(count): # source copy count bytes
if count < 1:
add_bit(0)
elif count < 2:
add_bits([1,0])
elif count < 5:
add_bits([1,1])
add_bitnum(count-1,2)
elif count < 8:
add_bits([1,1,0,0])
add_bitnum(count-4,2)
elif count < 15:
add_bits([1,1,0,0,0,0])
add_bitnum(count-7,3)
elif count < 1039:
add_bits([1,1,0,0,0,0,0,0,0])
add_bitnum(count-15,10)
else:
print("Raw byte packet too large: %d > %d" % (count,1038))
assert(count<1039)
def control_back(count,offset): # back reference count bytes at offset
# count must be >= 2
# offset must be >= count
assert(count >= 2)
assert(offset >= count)
offset -= count
# special case for 2-byte count
if count == 2 and offset < 64:
add_bits([0,0])
add_bitnum(offset,6)
return
elif count == 2 and offset < 1088:
add_bits([0,1])
add_bitnum(offset-64,10)
return
# other byte counts
if count == 3:
add_bits([1,0])
elif count < 6:
add_bits([1,1,0])
add_bitnum(count-4,1)
elif count < 10:
add_bits([1,1,1,0])
add_bitnum(count-6,2)
elif count < 1034:
add_bits([1,1,1,1])
add_bitnum(count-10,10)
else:
assert(count<1034)
# offset
if offset < 16:
add_bits([0])
add_bitnum(offset,4)
elif offset < 272:
add_bits([1,0])
add_bitnum(offset-16,8)
elif offset < 4368:
add_bits([1,1,0])
add_bitnum(offset-272,12)
elif offset < 32136:
add_bits([1,1,1])
add_bitnum(offset-4368,15)
else:
assert(offset < 32136)
def add_packet(dp):
nonlocal packets
nonlocal control_bits
packets.append((len(control_bits),dp))
# alternate raw/back packets, length of 0 is valid for raw
def add_packet_raw(dp):
control_raw(len(dp))
add_packet(dp)
def add_packet_back(count,offset):
control_back(count,offset)
add_packet(())
# break data into packets
pos = 0
raw = 0
while pos < len(d):
# find best back-reference from pos
best_offset = 0
best_count = 0
for offset in range(2,1024 if COMPRESS_FAST else 32136):
# Note: the maximum range is actually 32136 - count,
# but I left that detail out for convenience.
if (offset > pos): break
count = 0
for i in range(pos,len(d)):
if d[i] != d[i-offset]: break
count += 1
if (count >= offset): break # can't encode overlapping copy, unfortunately
if (count >= 1033): break # maximum copy size
if count > best_count and (offset < (1088+2) or count > 2):
best_offset = offset
best_count = count
if best_count < 2:
# if no match of 2 bytes or more is found, add to raw bytes instead
raw += 1
pos += 1
else:
# emit raw/back packet pair
if DEBUG: print("-> Source copy: %d bytes (%04X)" % (raw,len(d)-(1+pos)))
add_packet_raw(d[pos-raw:pos])
if DEBUG: print("-> Backcopy: %d at %d (%04X)" % (best_count,best_offset,len(d)-(1+pos)))
add_packet_back(best_count,best_offset)
raw = 0
pos += best_count
# emit final raw packet
if (raw > 0):
if DEBUG: print("-> Source copy: %d bytes (%04X)" % (raw,len(d)-(1+pos)))
add_packet_raw(d[pos-raw:pos])
# interleave streams
dc = bytearray()
control_pos = 0
for (cp,dp) in packets:
while control_pos < cp: # emit 1 or more control bytes
emit = 8 if (control_pos > 0) else 7
b = 0
for i in range(emit):
bit = 0
if control_pos < len(control_bits):
bit = control_bits[control_pos]
control_pos += 1
b = (b << 1) | bit
if emit < 8: # first byte is 7-bits + 1 sentinel bit
b = (b << 1) | 1
dc.append(b)
for b in dp: # emit raw packet
dc.append(b)
dc.reverse()
# header with AHD signature and data lengths
header = bytearray([0x41,0x47,0x44,0,0,0,0])
header[3] = len(d) & 0xFF
header[4] = (len(d) >> 8) & 0xFF
header[5] = len(dc) & 0xFF
header[6] = (len(dc) >> 8) & 0xFF
assert (len(d) < 65536)
assert (len(dc) < 65536)
# debug verification
#d.reverse()
#dd = decompress_milva(header + dc,False) # to find mismatch
#dd = decompress_milva(header + dc,True) # once mismatched, use debug log to find it
#for i in range(min(len(dd),len(d))):
# if dd[len(dd)-(i+1)] != d[len(d)-(i+1)]:
# print("Mismatch at: %06X (old) / %06X (new)" % (len(d)-(i+1),len(dd)-(i+1)))
# break
return header + dc
# pack uncompressed Milva SPR/PIC file
def milva_pack(filename):
# output data header
d = bytearray([0xFD,0x00,0x70,0x00,0x00,0x00,0x00])
# load image
print("Image: " + filename)
img = img_load(filename,PALETTE)
if (img == None):
print("Unable to load image.")
print()
return
# process sprites
row_y = 0
row_x = 0
row_height = 1
count = 0
while row_y < img.height:
if row_x >= img.width: # advance vertically
row_x = 0
row_y += row_height
row_height = 1
continue
if img.getpixel((row_x,row_y))==4: # advance horizontally
row_x += 1
continue
# top left corner of image found, determine dimensions
i = row_x
while i < img.width:
if img.getpixel((i,row_y))==4: break
i += 1
if i >= img.width:
print("Warning: Sprite at %d,%d does not have frame on right?" % (row_x, row_y))
w = i - row_x
assert (w>0)
i = row_y
while i < img.height:
if img.getpixel((row_x,i))==4: break
i += 1
if i >= img.height:
print("Warning: Sprite at %d,%d does not have frame on bottom?" % (row_x, row_y))
h = i - row_y
assert (h>0)
if (h > row_height): row_height = h
sx = row_x
sy = row_y
print("Sprite %02d: %d,%d %d x %d" % (count,sx,sy,w,h))
count += 1
row_x += w
if (w & 7) != 0:
print("Sprite width (%d) must be multiple of 8!" % (w))
w -= (w & 7)
# pack CGA data
for y in range(h):
b = 0
bits = 0
for x in range(w):
p = img.getpixel((sx+x,sy+y))
b = (b << 2) | (p & 3)
bits += 2
if (bits >= 8):
d.append(b)
b = 0
bits = 0
# finish file
if COMPRESS:
dc = milva_compress(d[7:])
d = d[0:7] + dc
d[2] = 0x60
d.append(0x1A) # EOF
dsize = len(d) - 8
# don't know what the number in header bytes 4/5 means, is it important?
# (in compressed files they are normally just 0 anyway, I think?)
d[5] = dsize & 0xFF
d[6] = (dsize >> 8) & 0xFF
outfile = filename[0:-4] # remove extension
print("Output: " + outfile)
open(outfile,"wb").write(d)
print()
return
# pack all PNG files
for (root,dirs,files) in os.walk("."):
for f in files:
if f.lower().endswith(".png"):
milva_pack(f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment