dmcsb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dmcsb/__init__.py +50 -0
- dmcsb/__main__.py +6 -0
- dmcsb/cli.py +361 -0
- dmcsb/data/graphics_map.txt +750 -0
- dmcsb/data/itemnames.txt +182 -0
- dmcsb/data/palettes.txt +56 -0
- dmcsb/data/zones.txt +343 -0
- dmcsb/dungeon.py +925 -0
- dmcsb/graphics.py +700 -0
- dmcsb-0.1.0.dist-info/METADATA +228 -0
- dmcsb-0.1.0.dist-info/RECORD +15 -0
- dmcsb-0.1.0.dist-info/WHEEL +5 -0
- dmcsb-0.1.0.dist-info/entry_points.txt +2 -0
- dmcsb-0.1.0.dist-info/licenses/LICENSE +25 -0
- dmcsb-0.1.0.dist-info/top_level.txt +1 -0
dmcsb/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""dmcsb - tools for reverse-engineering Dungeon Master / Chaos Strikes Back data files.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
|
|
5
|
+
from dmcsb import open_graphics, GraphicsFile, DungeonFile
|
|
6
|
+
|
|
7
|
+
g = open_graphics("GRAPHICS.DAT") # GRAPHICS.DAT -> decoded assets
|
|
8
|
+
d = DungeonFile().load("DUNGEON.DAT") # DUNGEON.DAT -> maps / things
|
|
9
|
+
|
|
10
|
+
See dmcsb.graphics and dmcsb.dungeon for details.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .graphics import (
|
|
14
|
+
GraphicsFile, open_graphics,
|
|
15
|
+
decode_img1, decode_fnt1, decode_snd2, decode_txt2, decode_lay1,
|
|
16
|
+
write_wav, lay1_to_text, load_lay1_zones,
|
|
17
|
+
load_palettes, load_map, item_types_for_count,
|
|
18
|
+
amiga12_to_rgb, DEFAULT_PALETTE, DEFAULT_SND_RATE,
|
|
19
|
+
NibbleReader, LAY1_TYPES,
|
|
20
|
+
__version__,
|
|
21
|
+
)
|
|
22
|
+
from .dungeon import (
|
|
23
|
+
DungeonFile, open_dungeon, decompress_dungeon,
|
|
24
|
+
THING_TYPES, THING_NONE, CELL_NAMES, SKILL_NAMES,
|
|
25
|
+
decode_dungeon_header, decode_map_info,
|
|
26
|
+
decode_dungeon_text, decode_champion, load_item_names,
|
|
27
|
+
decode_doorlist, decode_teleporterlist, decode_textstringlist,
|
|
28
|
+
decode_sensorlist, decode_creaturelist, decode_weaponlist,
|
|
29
|
+
decode_armorlist, decode_scrolllist, decode_potionlist,
|
|
30
|
+
decode_containerlist, decode_junklist, decode_projectilelist,
|
|
31
|
+
decode_explosionlist,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"GraphicsFile", "open_graphics", "DungeonFile", "open_dungeon",
|
|
37
|
+
"decode_img1", "decode_fnt1", "decode_snd2", "decode_txt2", "decode_lay1",
|
|
38
|
+
"write_wav", "lay1_to_text", "load_lay1_zones",
|
|
39
|
+
"load_palettes", "load_map", "item_types_for_count",
|
|
40
|
+
"amiga12_to_rgb", "DEFAULT_PALETTE", "DEFAULT_SND_RATE",
|
|
41
|
+
"NibbleReader", "LAY1_TYPES",
|
|
42
|
+
"decompress_dungeon", "THING_TYPES", "THING_NONE", "CELL_NAMES", "SKILL_NAMES",
|
|
43
|
+
"decode_dungeon_header", "decode_map_info",
|
|
44
|
+
"decode_dungeon_text", "decode_champion", "load_item_names",
|
|
45
|
+
"decode_doorlist", "decode_teleporterlist", "decode_textstringlist",
|
|
46
|
+
"decode_sensorlist", "decode_creaturelist", "decode_weaponlist",
|
|
47
|
+
"decode_armorlist", "decode_scrolllist", "decode_potionlist",
|
|
48
|
+
"decode_containerlist", "decode_junklist", "decode_projectilelist",
|
|
49
|
+
"decode_explosionlist",
|
|
50
|
+
]
|
dmcsb/__main__.py
ADDED
dmcsb/cli.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dmcsb.cli - command-line front-end for the dmcsb library (``dmcsb`` / ``python -m dmcsb``).
|
|
3
|
+
|
|
4
|
+
This is the sanctioned presentation layer: the library decoders in dmcsb.graphics /
|
|
5
|
+
dmcsb.dungeon stay silent (no prints), and all human-readable output lives here. The
|
|
6
|
+
``examples/`` scripts are thin shims that import the functions below.
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
dmcsb dump GRAPHICS.DAT [out_dir] [--map ...] [--palettes ...] [--zones ...]
|
|
10
|
+
dmcsb dungeon DUNGEON.DAT [--level N]
|
|
11
|
+
dmcsb uncompress DUNGEON.DAT [output.dat]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import struct
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from . import (open_graphics, open_dungeon, decompress_dungeon,
|
|
21
|
+
decode_img1, decode_fnt1, decode_snd2, decode_txt2, decode_lay1,
|
|
22
|
+
lay1_to_text, load_lay1_zones, write_wav,
|
|
23
|
+
DEFAULT_PALETTE, SKILL_NAMES)
|
|
24
|
+
from .graphics import _write_png
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# dump - GRAPHICS.DAT -> a folder of PNG/WAV/TXT/.bin + an HTML contact sheet
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
def _safe(name):
|
|
31
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_")[:80]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def dump(path, out_dir="graphics_dump", map_path=None, palettes_path=None,
|
|
35
|
+
zones_path=None):
|
|
36
|
+
g = open_graphics(path, map=map_path, palettes=palettes_path, zones=zones_path)
|
|
37
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
zones = load_lay1_zones(zones_path) # None -> bundled zones.txt
|
|
40
|
+
print("Map entries:", len(g.image_map),
|
|
41
|
+
"| Palettes:", len(g.named_palettes))
|
|
42
|
+
|
|
43
|
+
summary = {} # type -> count
|
|
44
|
+
failed = [] # (index, reason)
|
|
45
|
+
images = [] # (index, filename, w, h) for the contact sheet
|
|
46
|
+
sounds = 0 # SND items exported as WAV
|
|
47
|
+
texts = 0 # TXT2 items exported as TXT
|
|
48
|
+
layouts = 0 # LAY1 items exported as JSON
|
|
49
|
+
|
|
50
|
+
for i in range(g.item_count):
|
|
51
|
+
typ = g.item_type(i)
|
|
52
|
+
summary[typ] = summary.get(typ, 0) + 1
|
|
53
|
+
|
|
54
|
+
if typ == "COD":
|
|
55
|
+
continue # copy-protection code, not an image
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
data = g.item_data(i) # LZW-decompressed if needed (Atari ST)
|
|
59
|
+
except NotImplementedError:
|
|
60
|
+
failed.append((i, "LZW needed (Atari ST) - see _lzw_decompress"))
|
|
61
|
+
continue
|
|
62
|
+
except Exception as e:
|
|
63
|
+
failed.append((i, "item_data: %s" % e))
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
desc = g.name_for(i) # from the map (or None)
|
|
67
|
+
|
|
68
|
+
if typ == "IMG1":
|
|
69
|
+
try:
|
|
70
|
+
w, h, idx = decode_img1(data)
|
|
71
|
+
if w == 0 or h == 0:
|
|
72
|
+
failed.append((i, "empty image %dx%d" % (w, h)))
|
|
73
|
+
continue
|
|
74
|
+
ipal = g.palette_for(i)
|
|
75
|
+
rgb = bytearray(w * h * 3)
|
|
76
|
+
for p, ci in enumerate(idx):
|
|
77
|
+
rgb[p * 3:p * 3 + 3] = bytes(ipal[ci & 0xF])
|
|
78
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
79
|
+
name = "img_%04d_%dx%d%s.png" % (i, w, h, tag)
|
|
80
|
+
_write_png(os.path.join(out_dir, name), w, h, bytes(rgb))
|
|
81
|
+
images.append((i, name, w, h))
|
|
82
|
+
except Exception as e:
|
|
83
|
+
failed.append((i, "IMG1 decode: %s" % e))
|
|
84
|
+
elif typ == "FNT1":
|
|
85
|
+
try:
|
|
86
|
+
w, h, idx = decode_fnt1(data) # 1-bit font -> 0/15
|
|
87
|
+
rgb = bytearray(w * h * 3)
|
|
88
|
+
for p, ci in enumerate(idx):
|
|
89
|
+
rgb[p * 3:p * 3 + 3] = bytes(DEFAULT_PALETTE[ci & 0xF])
|
|
90
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
91
|
+
name = "img_%04d_%dx%d%s.png" % (i, w, h, tag)
|
|
92
|
+
_write_png(os.path.join(out_dir, name), w, h, bytes(rgb))
|
|
93
|
+
images.append((i, name, w, h))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
failed.append((i, "FNT1 decode: %s" % e))
|
|
96
|
+
elif typ == "SND":
|
|
97
|
+
try:
|
|
98
|
+
pcm = decode_snd2(data)
|
|
99
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
100
|
+
name = "snd_%04d%s.wav" % (i, tag)
|
|
101
|
+
write_wav(os.path.join(out_dir, name), pcm)
|
|
102
|
+
sounds += 1
|
|
103
|
+
except Exception as e:
|
|
104
|
+
failed.append((i, "SND2 decode: %s" % e))
|
|
105
|
+
elif typ == "TXT2":
|
|
106
|
+
try:
|
|
107
|
+
strings = decode_txt2(data)
|
|
108
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
109
|
+
name = "txt_%04d%s.txt" % (i, tag)
|
|
110
|
+
with open(os.path.join(out_dir, name), "w", encoding="utf-8") as f:
|
|
111
|
+
f.write("\n".join(strings))
|
|
112
|
+
texts += 1
|
|
113
|
+
except Exception as e:
|
|
114
|
+
failed.append((i, "TXT2 decode: %s" % e))
|
|
115
|
+
elif typ == "LAY1":
|
|
116
|
+
# Raw binary = compact form for the engine (parse with decode_lay1).
|
|
117
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
118
|
+
with open(os.path.join(out_dir, "lay_%04d%s.bin" % (i, tag)), "wb") as f:
|
|
119
|
+
f.write(data)
|
|
120
|
+
# Additionally a READABLE listing annotated with zone names
|
|
121
|
+
# (for inspection only; zones optional via --zones).
|
|
122
|
+
with open(os.path.join(out_dir, "lay_%04d%s.txt" % (i, tag)),
|
|
123
|
+
"w", encoding="utf-8") as f:
|
|
124
|
+
f.write(lay1_to_text(decode_lay1(data, zones), zones))
|
|
125
|
+
layouts += 1
|
|
126
|
+
else:
|
|
127
|
+
# Binary tables and others: save the raw data
|
|
128
|
+
tag = "_" + _safe(desc) if desc else ""
|
|
129
|
+
name = "%s_%04d%s.bin" % (typ.lower(), i, tag)
|
|
130
|
+
with open(os.path.join(out_dir, name), "wb") as f:
|
|
131
|
+
f.write(data)
|
|
132
|
+
|
|
133
|
+
_write_contact_sheet(os.path.join(out_dir, "index.html"), images, g)
|
|
134
|
+
_print_summary(g, summary, failed, images, sounds, texts, layouts, out_dir)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _write_contact_sheet(path, images, g):
|
|
138
|
+
cells = []
|
|
139
|
+
for i, name, w, h in images:
|
|
140
|
+
disp_w = max(w * 2, 48) # scale up small sprites
|
|
141
|
+
cells.append(
|
|
142
|
+
'<figure><img src="%s" width="%d" '
|
|
143
|
+
'style="image-rendering:pixelated;background:#222;border:1px solid #444">'
|
|
144
|
+
'<figcaption>#%d · %d×%d</figcaption></figure>'
|
|
145
|
+
% (name, disp_w, i, w, h)
|
|
146
|
+
)
|
|
147
|
+
html = (
|
|
148
|
+
"<!doctype html><meta charset=utf-8>"
|
|
149
|
+
"<title>GRAPHICS.DAT Dump</title>"
|
|
150
|
+
"<style>body{background:#111;color:#ccc;font:13px sans-serif;margin:16px}"
|
|
151
|
+
"figure{display:inline-block;margin:6px;text-align:center;vertical-align:top}"
|
|
152
|
+
"figcaption{font-size:11px;color:#888;margin-top:2px}</style>"
|
|
153
|
+
"<h1>GRAPHICS.DAT — %d images (%s)</h1>%s"
|
|
154
|
+
% (len(images), g.format, "".join(cells))
|
|
155
|
+
)
|
|
156
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
157
|
+
f.write(html)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _print_summary(g, summary, failed, images, sounds, texts, layouts, out_dir):
|
|
161
|
+
print("Format:", g.format, "| Items total:", g.item_count)
|
|
162
|
+
print("Types :", ", ".join("%s=%d" % (t, n) for t, n in sorted(summary.items())))
|
|
163
|
+
print("Images as PNG:", len(images), "| Sounds as WAV:", sounds,
|
|
164
|
+
"| Texts as TXT:", texts, "| Layouts (.bin):", layouts)
|
|
165
|
+
if failed:
|
|
166
|
+
print("Not decoded:", len(failed))
|
|
167
|
+
for i, reason in failed[:20]:
|
|
168
|
+
print(" #%-4d %s" % (i, reason))
|
|
169
|
+
if len(failed) > 20:
|
|
170
|
+
print(" ... (%d more)" % (len(failed) - 20))
|
|
171
|
+
print("Contact sheet ->", os.path.join(out_dir, "index.html"))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# dungeon - print one DUNGEON.DAT level (map, things, champions)
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
def print_level(d, level):
|
|
178
|
+
"""Render one dungeon Level: header summary, ornament lists, ASCII tile map."""
|
|
179
|
+
map_info = d.mapsinfo[str(level)]
|
|
180
|
+
|
|
181
|
+
print(f"\nMap at Level {map_info['Level']} ----------------")
|
|
182
|
+
print(f" RawMapDataByteOffset: {map_info['RawMapDataByteOffset']}")
|
|
183
|
+
print(f" OffsetMapX: {map_info['OffsetMapX']}, OffsetMapY: {map_info['OffsetMapY']}")
|
|
184
|
+
print(f" Width: {map_info['Width']}, Height: {map_info['Height']}, Level: {map_info['Level']}")
|
|
185
|
+
print(f" RandomFloorOrnamentCount: {map_info['RandomFloorOrnamentCount']}")
|
|
186
|
+
print(f" FloorOrnamentCount: {map_info['FloorOrnamentCount']}")
|
|
187
|
+
print(f" RandomWallOrnamentCount: {map_info['RandomWallOrnamentCount']}")
|
|
188
|
+
print(f" WallOrnamentCount: {map_info['WallOrnamentCount']}")
|
|
189
|
+
print(f" Difficulty: {map_info['Difficulty']}")
|
|
190
|
+
print(f" CreatureTypeCount: {map_info['CreatureTypeCount']}")
|
|
191
|
+
print(f" DoorOrnamentCount: {map_info['DoorOrnamentCount']}")
|
|
192
|
+
print(f" DoorSet1: {map_info['DoorSet1']}, DoorSet0: {map_info['DoorSet0']}")
|
|
193
|
+
print(f" WallSet: {map_info['WallSet']}, FloorSet: {map_info['FloorSet']}")
|
|
194
|
+
|
|
195
|
+
# The ornament/creature index tables sit right after this map's tile grid.
|
|
196
|
+
w = map_info['Width'] + 1
|
|
197
|
+
h = map_info['Height'] + 1
|
|
198
|
+
base = map_info['RawMapDataByteOffset'] + (w * h)
|
|
199
|
+
counts = [
|
|
200
|
+
("Creature", map_info['CreatureTypeCount']),
|
|
201
|
+
("WallOrnate", map_info['WallOrnamentCount']),
|
|
202
|
+
("FloorOrnate", map_info['FloorOrnamentCount']),
|
|
203
|
+
("DoorDeco", map_info['DoorOrnamentCount']),
|
|
204
|
+
]
|
|
205
|
+
print("\nCreatures/Wall/Floor/Doors used ----------------")
|
|
206
|
+
off = 0
|
|
207
|
+
parts = []
|
|
208
|
+
for name, count in counts:
|
|
209
|
+
vals = "".join(str(d.tile_data[base + off + c]) + ", " for c in range(count))
|
|
210
|
+
parts.append(f" {name}: {vals}")
|
|
211
|
+
off += count
|
|
212
|
+
print("\n".join(parts) + "\n")
|
|
213
|
+
|
|
214
|
+
print("MapData ----------------\n")
|
|
215
|
+
grid = d.tile_grid(level)
|
|
216
|
+
lines = []
|
|
217
|
+
for row in grid:
|
|
218
|
+
cells = []
|
|
219
|
+
for val in row:
|
|
220
|
+
kind = (val >> 5) & 0x7
|
|
221
|
+
cells.append(" " if kind == 0 else f"{kind} ")
|
|
222
|
+
lines.append(" " + "".join(cells))
|
|
223
|
+
print("\n".join(lines))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def print_things_on_level(d, level):
|
|
227
|
+
"""Every Thing located on the selected level, grouped by type, with its map
|
|
228
|
+
(x, y) position. TextString things also show their decoded message."""
|
|
229
|
+
located = d.located_things(level)
|
|
230
|
+
grouped = {}
|
|
231
|
+
for item in located:
|
|
232
|
+
grouped.setdefault(item["type"], []).append(item)
|
|
233
|
+
print(f"\nThings on Level {level} ({len(located)}) ----------------")
|
|
234
|
+
for name, items in grouped.items():
|
|
235
|
+
print(f" {name}: {len(items)}")
|
|
236
|
+
for item in items:
|
|
237
|
+
pos = f"({item['x']},{item['y']}){item['dir']}"
|
|
238
|
+
rec = item["thing"]
|
|
239
|
+
if name == "TextString":
|
|
240
|
+
label = d.text(rec).replace("\n", " / ")
|
|
241
|
+
elif name == "Scroll":
|
|
242
|
+
txt = d.scroll_text(rec)
|
|
243
|
+
label = txt.replace("\n", " / ") if txt else None
|
|
244
|
+
else:
|
|
245
|
+
label = d.item_name(name, rec.get("Type"))
|
|
246
|
+
line = f" {pos:>9} {rec}"
|
|
247
|
+
if label:
|
|
248
|
+
line += f" -> {label!r}"
|
|
249
|
+
print(line)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def print_champions(d, level):
|
|
253
|
+
"""Champions whose sheet sits on this level (e.g. the Hall of Champions)."""
|
|
254
|
+
champs = d.champions_on_level(level)
|
|
255
|
+
if not champs:
|
|
256
|
+
return
|
|
257
|
+
print(f"\nChampions on Level {level} ({len(champs)}) ----------------")
|
|
258
|
+
for c in champs:
|
|
259
|
+
pos = f"({c['x']},{c['y']}){c['dir']}"
|
|
260
|
+
title = (" " + c["title"]) if c["title"] else ""
|
|
261
|
+
print(f" {pos:>9} {c['name']}{title} [{c['gender']}] "
|
|
262
|
+
f"Health={c['Health']} Stamina={c['Stamina'] // 10} Mana={c['Mana']} "
|
|
263
|
+
f"Str={c['Strength']} Dex={c['Dexterity']} Wis={c['Wisdom']} "
|
|
264
|
+
f"Vit={c['Vitality']} AntiMagic={c['AntiMagic']} AntiFire={c['AntiFire']}")
|
|
265
|
+
# 16 hidden-skill starting levels, named (SKILL_NAMES[4:] in 4 groups).
|
|
266
|
+
for gi, basic in enumerate(("Fighter", "Ninja", "Priest", "Wizard")):
|
|
267
|
+
sub = SKILL_NAMES[4 + gi * 4: 8 + gi * 4]
|
|
268
|
+
named = " ".join(f"{n}={v}" for n, v in zip(sub, c[basic]))
|
|
269
|
+
print(f" {basic:8} {named}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def show_dungeon(path, level):
|
|
273
|
+
"""Load a DUNGEON.DAT and print one level. Returns a process exit code."""
|
|
274
|
+
d = open_dungeon(path)
|
|
275
|
+
if str(level) not in d.mapsinfo:
|
|
276
|
+
levels = ", ".join(sorted(d.mapsinfo, key=int))
|
|
277
|
+
print(f"No level {level} in this dungeon. Available levels: {levels}")
|
|
278
|
+
return 1
|
|
279
|
+
print_level(d, level)
|
|
280
|
+
print_things_on_level(d, level)
|
|
281
|
+
print_champions(d, level)
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# uncompress - DUNGEON.DAT -> raw decompressed bytes
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
def uncompress(in_path, out_path):
|
|
289
|
+
with open(in_path, "rb") as f:
|
|
290
|
+
buffer = f.read()
|
|
291
|
+
|
|
292
|
+
signature, decompressed_byte_count, _dungeon_id = struct.unpack(">HlH", buffer[:8])
|
|
293
|
+
if signature == 0x8104: # big-endian, compressed
|
|
294
|
+
data = bytes(decompress_dungeon(buffer[8:], decompressed_byte_count))
|
|
295
|
+
note = "decompressed %d -> %d bytes" % (len(buffer), len(data))
|
|
296
|
+
elif signature == 0x0481 or buffer[1] == 0x00: # little-endian variants
|
|
297
|
+
print("Little-endian (PC) DUNGEON.DAT is not supported yet.")
|
|
298
|
+
return 1
|
|
299
|
+
elif buffer[1] == 0x63: # big-endian, already uncompressed
|
|
300
|
+
data = buffer
|
|
301
|
+
note = "already uncompressed (%d bytes), copied as-is" % len(buffer)
|
|
302
|
+
else:
|
|
303
|
+
print("Not a recognized DUNGEON.DAT file.")
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
with open(out_path, "wb") as f:
|
|
307
|
+
f.write(data)
|
|
308
|
+
print("%s -> %s (%s)" % (in_path, out_path, note))
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ---------------------------------------------------------------------------
|
|
313
|
+
# argparse front-end
|
|
314
|
+
# ---------------------------------------------------------------------------
|
|
315
|
+
def build_parser():
|
|
316
|
+
p = argparse.ArgumentParser(
|
|
317
|
+
prog="dmcsb",
|
|
318
|
+
description="Tools for Dungeon Master / Chaos Strikes Back data files.")
|
|
319
|
+
sub = p.add_subparsers(dest="command", metavar="{dump,dungeon,uncompress}")
|
|
320
|
+
|
|
321
|
+
pd = sub.add_parser("dump", help="extract GRAPHICS.DAT to a folder of assets")
|
|
322
|
+
pd.add_argument("input", metavar="GRAPHICS.DAT")
|
|
323
|
+
pd.add_argument("out_dir", nargs="?", default="graphics_dump",
|
|
324
|
+
help="output directory (default: graphics_dump)")
|
|
325
|
+
pd.add_argument("--map", dest="map_path", help="override bundled graphics_map.txt")
|
|
326
|
+
pd.add_argument("--palettes", dest="palettes_path", help="override bundled palettes.txt")
|
|
327
|
+
pd.add_argument("--zones", dest="zones_path", help="override bundled zones.txt")
|
|
328
|
+
|
|
329
|
+
pg = sub.add_parser("dungeon", help="print one DUNGEON.DAT level")
|
|
330
|
+
pg.add_argument("input", metavar="DUNGEON.DAT")
|
|
331
|
+
pg.add_argument("--level", type=int, default=1, help="dungeon level (default: 1)")
|
|
332
|
+
|
|
333
|
+
pu = sub.add_parser("uncompress", help="write a decompressed copy of DUNGEON.DAT")
|
|
334
|
+
pu.add_argument("input", metavar="DUNGEON.DAT")
|
|
335
|
+
pu.add_argument("output", nargs="?",
|
|
336
|
+
help="output file (default: <input>.uncompressed)")
|
|
337
|
+
|
|
338
|
+
return p
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def main(argv=None):
|
|
342
|
+
"""Console entry point. Returns a process exit code."""
|
|
343
|
+
parser = build_parser()
|
|
344
|
+
args = parser.parse_args(argv)
|
|
345
|
+
|
|
346
|
+
if args.command == "dump":
|
|
347
|
+
dump(args.input, args.out_dir, map_path=args.map_path,
|
|
348
|
+
palettes_path=args.palettes_path, zones_path=args.zones_path)
|
|
349
|
+
return 0
|
|
350
|
+
if args.command == "dungeon":
|
|
351
|
+
return show_dungeon(args.input, args.level)
|
|
352
|
+
if args.command == "uncompress":
|
|
353
|
+
out = args.output or args.input + ".uncompressed"
|
|
354
|
+
return uncompress(args.input, out)
|
|
355
|
+
|
|
356
|
+
parser.print_help()
|
|
357
|
+
return 1
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
sys.exit(main())
|