Last active
October 12, 2015 02:13
-
-
Save magical/8cb0713d5a4d22daf96c to your computer and use it in GitHub Desktop.
in-progress CC2 level 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
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