Skip to content

Instantly share code, notes, and snippets.

@magical
Last active October 12, 2015 02:13
Show Gist options
  • Save magical/8cb0713d5a4d22daf96c to your computer and use it in GitHub Desktop.
Save magical/8cb0713d5a4d22daf96c to your computer and use it in GitHub Desktop.
in-progress CC2 level dumper
import binascii
import struct
import sys
import traceback
u16 = struct.Struct('<H')
u32 = struct.Struct('<L')
def read16(f):
b = f.read(2)
x, = u16.unpack(f.read(2))
return x
def dump(f, filename):
assert f.read(4) == b'CC2M'
assert f.read(2) == b'\x02\x00'
subtype = read16(f)
print('version', chr(subtype))
assert chr(subtype) in list("34567")
for name, size, data in dechunk(f):
if name in [b'TITL', b'LOCK', b'AUTH', b'VERS']:
assert data[-1] == 0
value = data[:-1].decode('latin1', errors='replace')
if name == b'TITL':
print('Title:', value, sep="\t")
elif name == b'LOCK':
print('Lock:', value, sep="\t")
elif name == b'AUTH':
print('Author:', value, sep="\t")
elif name == b'VERS':
print('Version:', value, sep="\t")
else:
print(name, size, repr(data[:-1]), sep="\t")
elif name == b'CLUE':
assert data[-1] == 0
if b'\n' in data:
print("Clue:")
for line in data[:-1].split(b'\n'):
print('\t'+line.decode('utf-8'))
else:
print("Clue:", data[:-1].decode(), sep="\t")
elif name == b'NOTE':
assert data[-1] == 0
if b'\n' in data:
print("Note:")
for line in data[:-1].split(b'\n'):
print('\t'+line.decode('utf-8'))
else:
print("Note:", data[:-1].decode(), sep="\t")
elif name == b'KEY ':
print(name, size, uuid(data))
elif name == b'OPTN':
print(name, size, binascii.hexlify(data), sep="\t")
if subtype in b'345':
assert size == 3
elif subtype in b'67':
assert size < 25
time, = u16.unpack(data[:2])
print("", "time:", time, sep="\t")
print("", "viewport:", data[2], sep="\t") # 10x10 / 9x9 / split
if len(data) > 3:
# the replay solves the level and level hasn't been modified
print("", "solved: ", data[3], sep="\t")
if len(data) > 4:
# map is not shown in the editor
print("", "hidemap:", data[4], sep="\t")
if len(data) > 5:
# map cannot be modified in the editor
print("", "readonly:", data[5], sep="\t")
if len(data) > 6:
print("", "uuid:", uuid(data[6:22]), sep="\t")
if len(data) > 22:
# hide wires, logic tiles, and pink and black buttons
print("", "wires:", data[22], sep='\t')
if len(data) > 23:
# don't let chip drop boots
# or pick up any of the new tools
# chips1.exe hardcodes this option
print("", "boots:", data[23], sep="\t")
elif name == b'PACK':
#print(name, size, sep="\t")
print("Map:")
data = unpack(data)
width = data[0]
height = data[1]
print("\tWidth:", width, sep="\t")
print("\tHeight:", height, sep="\t")
#print(binascii.hexlify(data))
xyiter = ((x, y) for y in range(height) for x in range(width))
tileiter = parsetiles(data[2:])
for (x, y), (tiledata, tiles) in zip(xyiter, tileiter):
print("\t{:02d};{:02d}\t{:<10s}\t{}".format(x, y, binascii.hexlify(tiledata).decode(), ", ".join(tiles)))
else:
print(name, size, sep="\t")
def dechunk(f):
while True:
name = f.read(4)
if not name:
return
if name == b'END ':
return
assert len(name) == 4
size, = u32.unpack(f.read(4))
data = f.read(size)
yield name, size, data
def unpack(data):
size, = u16.unpack(data[:2])
assert 1 <= data[2] <= 127
out = bytearray()
i = 2
while i < len(data) and len(out) <= size:
count = data[i]
if count <= 127:
count = count
for j in range(count):
out.append(data[i+1+j])
i += 1 + count
else:
count = count & 0x7f
offset = data[i+1]
assert offset > 0
for _ in range(count):
out.append(out[-offset])
i += 2
assert i == len(data)
assert len(out) == size
return bytes(out)
tilespectext = """
01 floor
02 wall
03 ice
04 ice wall ne
05 ice wall se
06 ice wall sw
07 ice wall nw
08 water
09 fire
0A force floor n
0B force floor e
0C force floor s
0D force floor w
0E green toggle wall
0F green toggle floor
10 blue teleport
11 red teleport
12 yellow teleport
13 green teleport
14 exit
15 slime floor
16,D,+ chip
17,D,+ dirt block
18,D,+ walker
19,D,+ glider
1A,D,+ ice block
1B,D,+ thin wall s
1C,D,+ thin wall e
1D,D,+ thin wall se
1E gravel
1F green button
20 blue button
21,D,+ tank
22 red door
23 blue door
24 yellow door
25 green door
26,+ red key
27,+ blue key
28,+ yellow key
29,+ green key
2A,+ ic chip
2B,+ extra chip
2C chip socket
2D popup wall
2E invisible wall
2F invisible wall (temp)
30 blue wall
31 blue floor
32 dirt
33,D,+ bug
34,D,+ centipede
35,D,+ ball
36,D,+ blob
37,D,+ red teeth
38,D,+ fireball
39 red button
3A brown button
3B,+ ice boots
3C,+ magnet boots
3D,+ fire boots
3E,+ flippers
3F boot thief
40,+ red bomb
42 trap
43 clone machine
44 clone machine
45 hint
46 force floor random
47 gray button
48 revolving door sw
49 revolving door nw
4A revolving door ne
4B revolving door se
4C,+ time bonus
4D,+ time toggle
4E transmogrifier
4F railroad
50 steel wall
51,+ time bomb
52,+ helmet
56,D,+ melinda
57,D,+ blue teeth
59,+ hiking boots
5A male-only
5B female-only
5C logic gate
5E pink button
5F flame jets off
60 flame jets on
61 orange button
62,+ lightning bolt
63,D,+ yellow tank
64 yellow tank pad
65,D,+ mirror chip
66,D,+ mirror melinda
68,+ bowling ball
69,D,+ rover
6A,+ time penalty
6B custom floor
6D,P,+ thin wall
6F,+ railroad sign
70 custom wall
71 alphabet tile
72 pink toggle floor
73 pink toggle wall
76,M,+ modifier
77,M,M,+ modifier
7A,+ bonus flag 10
7B,+ bonus flag 100
7C,+ bonus flag 1000
7D green wall
7E green floor
7F,+ no tool
80,+ bonus flag 2x
81,D,P,+ direction block
82,D,+ mimic
83,+ toggle bomb
84,+ toggle ic chip
87 UNKNOWN
88 UNKNOWN
8A key thief
8B,D,+ ghost
8C,+ foil
8D turtle
8E,+ eye
8F,+ bribe
90,+ quick boots
92,+ hook
"""
custom_colors = ["green", "pink", "yellow", "blue"]
tilespec = {}
for line in tilespectext.strip().split('\n'):
spec, name = line.split(" ", 1)
spec = spec.split(",")
byte = int(spec[0], 16)
tilespec[byte] = (byte, spec[1:], name.strip())
def parsetile(data, index):
names = []
while True:
if data[index] not in tilespec:
names.append("UNKNOWN")
index += 1
break
byte, spec, name = tilespec[data[index]]
if spec == ['M', '+']:
mod = data[index+1]
if data[index+2] == 0x71:
if ord('A') <= mod <= ord('Z'):
name = "alphabet tile "+chr(data[index+1])
elif mod == 0x1c:
name = "up arrow"
elif mod == 0x1d:
name = "right arrow"
elif mod == 0x1e:
name = "down arrow"
elif mod == 0x1f:
name = "left arrow"
else:
name = "alphabet tile unknown"
names.append(name)
return names, index+3
elif data[index+2] == 0x6b:
name = "custom floor " + custom_colors[mod]
names.append(name)
index += 3
break
elif data[index+2] == 0x70:
name = "custom wall " + custom_colors[mod]
names.append(name)
index += 3
break
else:
names.append("modifier")
index += 2
continue
elif spec == ['M', 'M', '+']:
mod = data[index+1]
mod2 = data[index+2]
names.append("modifier")
index += 3
continue
elif spec and spec[0] == 'P':
b = data[index+1]
if b == 1:
name = "thin wall n"
elif b == 2:
name = "thin wall e"
elif b == 3:
name = "thin wall ne"
elif b == 4:
name = "thin wall s"
elif b == 5:
name = "thin wall nse"
elif b == 6:
name = "thin wall se"
elif b == 8:
name = "thin wall w"
elif b == 9:
name = "thin wall nw"
elif b == 12:
name = "thin wall sw"
elif b == 13:
name = 'thin wall nsw'
elif b == 16:
name = 'canopy'
elif spec and spec[0] == 'D':
if data[index+1] < 4:
name += " " + "nesw"[data[index+1]]
else:
name += " ERR"
names.append(name)
index += 1
if spec and spec[-1] == '+':
index += len(spec) - 1
continue
else:
index += len(spec)
break
return names, index
def printtiles(data):
index = 0
while index < len(data):
start = index
tiles, index = parsetile(data, index)
print("\t{:>10s}\t{}".format(binascii.hexlify(data[start:index]).decode(), ", ".join(tiles)))
def parsetiles(data):
index = 0
while index < len(data):
start = index
tiles, index = parsetile(data, index)
yield data[start:index], tiles
def main():
for filename in sys.argv[1:]:
with open(filename, 'rb') as f:
if 1:
print(filename)
dump(f, filename)
print()
else:
try:
check(f, filename)
except AssertionError as e:
print("in", filename, e)
def check(f, filename):
assert f.read(4) == b'CC2M', 'not a CC2M file'
assert f.read(2) == b'\x02\x00'
subtype = read16(f)
assert chr(subtype) in list("34567"), 'subtype == {}'.format(subtype)
haskey = False
for name, size, data in dechunk(f):
if name == b'KEY ':
print(uuid(data), "key ", filename)
haskey= True
if name == b'OPTN':
#print(name, size, binascii.hexlify(data), sep="\t")
if subtype in b'345':
assert size == 3, 'size == {}'.format(size)
elif subtype == b'6':
assert size in (4, 5, 22), 'size == {}'.format(size)
elif subtype == b'7':
assert size in (22, 23, 24), 'size == {}'.format(size)
else:
assert size < 25, 'size == {}'.format(size)
#print("+", chr(subtype), size)
time, = u16.unpack(data[:2])
assert time <= 999, 'time == {}'.format(time)
assert data[2] in (0,1,2), 'viewport == {}'.format(data[2])
if len(data) > 3:
# records whether the replay gets to the end of the level
# and the file hasn't been modified since the replay was made
assert data[3] in (0, 1), 'solved == {}'.format(data[3])
if len(data) > 4:
# XXX what is 2?
assert data[4] in (0,1,2), 'hidemap == {}'.format(data[4])
if len(data) > 5:
# editable
assert data[5] == 0, 'editable == {}'.format(data[5])
# some sort of uuid?
print(uuid(data[6:22]), "optn", filename)
if len(data) > 22:
assert data[22] in (0,1), 'wires = {}'.format(data[22])
if len(data) > 23:
assert data[23] in (0,1), 'boots = {}'.format(data[23])
if len(data) > 24:
assert data[24] == 0, 'data[24] == {}'.format(data[24])
if not haskey:
print("NO KEY".center(36), " ", filename)
def uuid(b):
fmt = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX".replace("XX", "{:02x}")
return fmt.format(*b)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment