Last active
July 19, 2023 04:20
-
-
Save bbbradsmith/e5e9459d400d1cc6f34501429d916ae6 to your computer and use it in GitHub Desktop.
Milva DOS image dumper (Desafio, Kick Boxing Street) and re-compressor
This file contains 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
# 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) |
This file contains 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
# 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