Skip to content
This repository was archived by the owner on Jun 22, 2025. It is now read-only.

Commit b9f6223

Browse files
committed
tidy up
1 parent 4e33452 commit b9f6223

File tree

3 files changed

+78
-58
lines changed

3 files changed

+78
-58
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ options:
107107
-m MAP_IMAGE, --map-image MAP_IMAGE
108108
Save map as PNG file (up to 32*32 blocks or 2048*2048
109109
px).
110-
-v, --verbose Print more information. Note: all addresses are
111-
hexadecimal.
110+
-v, --verbose Print more information.
112111
```
113112

114113
## nes_chr_decode.py
@@ -174,7 +173,8 @@ positional arguments:
174173
175174
options:
176175
-h, --help show this help message and exit
177-
-c {0,1,2,3} {0,1,2,3} {0,1,2,3} {0,1,2,3}, --colors {0,1,2,3} {0,1,2,3} {0,1,2,3} {0,1,2,3}
176+
-c {0,1,2,3} {0,1,2,3} {0,1,2,3} {0,1,2,3},
177+
--colors {0,1,2,3} {0,1,2,3} {0,1,2,3} {0,1,2,3}
178178
Change original colors 0...3 to these colors. Four
179179
colors (each 0...3) separated by spaces. Default: 0 2
180180
3 1

nes_blaster_mapext.py

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@
102102
0x3f: (0x00, 0x00, 0x00),
103103
}
104104

105+
def error(msg):
106+
sys.exit(f"Error: {msg}.")
107+
108+
def warn(msg):
109+
print(f"Warning: {msg}.", file=sys.stderr)
110+
105111
def parse_arguments():
106112
parser = argparse.ArgumentParser(
107113
description="Extract world maps from NES Blaster Master to PNG files."
@@ -134,7 +140,7 @@ def parse_arguments():
134140
)
135141
parser.add_argument(
136142
"-v", "--verbose", action="store_true",
137-
help="Print more information. Note: all addresses are hexadecimal."
143+
help="Print more information."
138144
)
139145
parser.add_argument(
140146
"input_file",
@@ -144,18 +150,18 @@ def parse_arguments():
144150
args = parser.parse_args()
145151

146152
if not 0 <= args.map_number <= 15:
147-
sys.exit("Invalid map number.")
153+
error("invalid map number")
148154

149155
if not os.path.isfile(args.input_file):
150-
sys.exit("Input file not found.")
156+
error("input file not found")
151157

152158
outputFiles = (
153159
args.usb_image, args.sb_image, args.block_image, args.map_image
154160
)
155-
if all(file_ is None for file_ in outputFiles):
156-
print("Warning: you didn't specify any output files.", file=sys.stderr)
161+
if all(f is None for f in outputFiles):
162+
warn("no output files specified")
157163
if any(f is not None and os.path.exists(f) for f in outputFiles):
158-
sys.exit("Some of the output files already exist.")
164+
error("some of the output files already exist")
159165

160166
return args
161167

@@ -187,19 +193,11 @@ def decode_ines_header(handle):
187193
"mapper": (flags7 & 0b11110000) | (flags6 >> 4),
188194
}
189195

190-
def is_blaster_master(fileInfo):
191-
# is the file likely Blaster Master? don't validate too accurately because
192-
# the file may be a hack; fileInfo: from decode_ines_header()
193-
return (
194-
fileInfo["prgSize"] == 128 * 1024
195-
and fileInfo["chrSize"] == 128 * 1024
196-
and fileInfo["mapper"] == 1
197-
)
198-
199196
def decode_offset(bytes_):
200197
# decode address, convert into offset within bank
201198
addr = struct.unpack("<H", bytes_)[0] # little-endian unsigned short
202-
assert 0x8000 <= addr <= 0xbfff
199+
if not 0x8000 <= addr <= 0xbfff:
200+
error("address not in first CPU PRG bank")
203201
return addr & 0x3fff
204202

205203
def get_tile_data(handle):
@@ -215,18 +213,26 @@ def get_tile_data(handle):
215213
)
216214
yield tuple(pixels)
217215

216+
def world_pal_to_rgb_pal(palette):
217+
# NES color numbers -> (R, G, B) tuples
218+
return tuple(NES_PALETTE[c] for c in palette)
219+
220+
def rgb_pal_to_img_pal(palette):
221+
# (R, G, B) tuples -> sorted unique (R, G, B) tuples
222+
return tuple(sorted(set(palette)))
223+
218224
def create_usb_image(usbData, usbAttrData, tileData, worldPal):
219225
# return image with up to 16*16 USBs
220226
# worldPal: world palette (16 NES colors)
221227

222228
image = Image.new("P", (16 * 16, (len(usbData) + 15) // 16 * 16), 0)
223229

224-
# convert world palette into RGB
225-
worldPal = [NES_PALETTE[color] for color in worldPal]
226-
# set image palette, create palette conversion table
227-
imgPal = sorted(set(worldPal))
230+
# set image palette, create conversion table
231+
worldPal = world_pal_to_rgb_pal(worldPal)
232+
imgPal = rgb_pal_to_img_pal(worldPal)
228233
image.putpalette(itertools.chain.from_iterable(imgPal))
229234
worldPalToImgPal = tuple(imgPal.index(color) for color in worldPal)
235+
del worldPal, imgPal
230236

231237
# for each USB, draw 2*2 tiles with correct palette
232238
tileImg = Image.new("P", (8, 8))
@@ -243,17 +249,13 @@ def create_usb_image(usbData, usbAttrData, tileData, worldPal):
243249
image.paste(tileImg, (x, y))
244250
return image
245251

246-
def world_pal_to_image_pal(worldPal):
247-
# 16 NES colors -> generate sorted unique colors as R,G,B,R,G,B,...
248-
yield from itertools.chain.from_iterable(
249-
sorted(set(NES_PALETTE[color] for color in worldPal))
250-
)
251-
252252
def create_sb_image(sbData, usbImg, worldPal):
253253
# return image with up to 16*16 SBs
254254

255255
outImg = Image.new("P", (16 * 32, (len(sbData) + 15) // 16 * 32), 0)
256-
outImg.putpalette(world_pal_to_image_pal(worldPal))
256+
outImg.putpalette(itertools.chain.from_iterable(
257+
rgb_pal_to_img_pal(world_pal_to_rgb_pal(worldPal))
258+
))
257259

258260
# copy USBs from source image to target image
259261
for (si, usbs) in enumerate(sbData):
@@ -273,7 +275,9 @@ def create_block_image(blockData, sbData, usbImg, worldPal):
273275
# return image with up to 16*16 blocks
274276

275277
outImg = Image.new("P", (16 * 64, (len(blockData) + 15) // 16 * 64), 0)
276-
outImg.putpalette(world_pal_to_image_pal(worldPal))
278+
outImg.putpalette(itertools.chain.from_iterable(
279+
rgb_pal_to_img_pal(world_pal_to_rgb_pal(worldPal))
280+
))
277281

278282
# copy USBs from source image to target image
279283
for (bi, block) in enumerate(blockData):
@@ -294,7 +298,9 @@ def create_map_image(mapData, blockData, sbData, usbImg, worldPal):
294298
# return image with up to 32*32 blocks
295299

296300
outImg = Image.new("P", (32 * 64, len(mapData) // 32 * 64), 0)
297-
outImg.putpalette(world_pal_to_image_pal(worldPal))
301+
outImg.putpalette(itertools.chain.from_iterable(
302+
rgb_pal_to_img_pal(world_pal_to_rgb_pal(worldPal))
303+
))
298304

299305
# copy USBs from source image to target image
300306
for (bi, block) in enumerate(mapData):
@@ -317,10 +323,14 @@ def extract_map(source, args):
317323
# parse iNES header
318324
fileInfo = decode_ines_header(source)
319325
if fileInfo is None:
320-
sys.exit("Not a valid iNES ROM file.")
326+
error("not a valid iNES ROM file")
327+
328+
if min(fileInfo["prgSize"], fileInfo["chrSize"]) < 128 * 1024:
329+
error("not Blaster Master (PRG/CHR ROM too small)")
321330

322-
if not is_blaster_master(fileInfo):
323-
sys.exit("The file doesn't look like Blaster Master.")
331+
if fileInfo["mapper"] != 1 \
332+
or max(fileInfo["prgSize"], fileInfo["chrSize"]) > 128 * 1024:
333+
warn("probably not Blaster Master")
324334

325335
(prgBank, worldPtr, chrBank) = MAP_DATA_ADDRESSES[args.map_number]
326336
# the only version difference
@@ -336,47 +346,60 @@ def extract_map(source, args):
336346

337347
if args.verbose:
338348
print(f"Map={args.map_number}, PRG bank={prgBank}, CHR bank={chrBank}")
339-
print(f"World data @ {worldAddr:04x}, scroll data @ {scrollAddr:04x}")
349+
print(
350+
f"World data @ ${worldAddr:04x}, scroll data @ ${scrollAddr:04x}"
351+
)
340352

341353
(palAddr, usbAttrAddr, usbAddr, sbAddr, blockAddr, mapAddr) = (
342354
decode_offset(prgBankData[pos:pos+2])
343355
for pos in range(worldAddr, worldAddr + 12, 2)
344356
)
345357
if args.verbose:
346358
print(
347-
f"Palette @ {palAddr:04x}, "
348-
f"USBs @ {usbAddr:04x}, "
349-
f"SBs @ {sbAddr:04x}, "
350-
f"blocks @ {blockAddr:04x}, "
351-
f"map @ {mapAddr:04x}, "
352-
f"USB attributes @ {usbAttrAddr:04x}"
359+
f"Palette @ ${palAddr:04x}, "
360+
f"USBs @ ${usbAddr:04x}, "
361+
f"SBs @ ${sbAddr:04x}, "
362+
f"blocks @ ${blockAddr:04x}, "
363+
f"map @ ${mapAddr:04x}, "
364+
f"USB attributes @ ${usbAttrAddr:04x}"
353365
)
354-
assert worldAddr < palAddr < usbAddr < sbAddr < blockAddr < mapAddr \
355-
< min(usbAttrAddr, scrollAddr)
366+
if not worldAddr < palAddr < usbAddr < sbAddr < blockAddr < mapAddr \
367+
< min(usbAttrAddr, scrollAddr):
368+
warn("order of data sections seems wrong")
356369

357370
# palette (4*4 NES color numbers)
358371
worldPalette = prgBankData[palAddr:palAddr+16]
359-
assert max(worldPalette) <= 0x3f
372+
if max(worldPalette) > 0x3f:
373+
error("invalid color in world palette")
374+
if len(set(worldPalette[i] for i in range(0, 16, 4))) > 1:
375+
warn("world subpalettes don't have the same background color")
360376

361377
# USB data
362-
assert sbAddr - usbAddr in range(4, 257 * 4, 4)
378+
if sbAddr - usbAddr not in range(4, 257 * 4, 4):
379+
error("invalid USB data size")
363380
usbData = tuple(prgBankData[i:i+4] for i in range(usbAddr, sbAddr, 4))
364381

365382
# SB data
366-
assert blockAddr - sbAddr in range(4, 257 * 4, 4)
383+
if blockAddr - sbAddr not in range(4, 257 * 4, 4):
384+
error("invalid SB data size")
367385
sbData = tuple(prgBankData[i:i+4] for i in range(sbAddr, blockAddr, 4))
368-
assert max(itertools.chain.from_iterable(sbData)) < len(usbData)
386+
if max(itertools.chain.from_iterable(sbData)) >= len(usbData):
387+
warn("SB data uses nonexistent USBs")
369388

370389
# block data
371-
assert mapAddr - blockAddr in range(4, 257 * 4, 4)
390+
if mapAddr - blockAddr not in range(4, 257 * 4, 4):
391+
error("invalid block data size")
372392
blockData = tuple(prgBankData[i:i+4] for i in range(blockAddr, mapAddr, 4))
373-
assert max(itertools.chain.from_iterable(blockData)) < len(sbData)
393+
if max(itertools.chain.from_iterable(blockData)) >= len(sbData):
394+
warn("block data uses nonexistent SBs")
374395

375396
# map data
376397
mapEnd = min(usbAttrAddr, scrollAddr)
377-
assert mapEnd - mapAddr in range(32, 33 * 32, 32)
398+
if mapEnd - mapAddr not in range(32, 33 * 32, 32):
399+
error("invalid map size")
378400
mapData = prgBankData[mapAddr:mapEnd]
379-
assert max(set(mapData)) < len(blockData)
401+
if max(set(mapData)) >= len(blockData):
402+
warn("map data uses nonexistent blocks")
380403

381404
# read USB attribute data (1 byte/USB)
382405
usbAttrData = prgBankData[usbAttrAddr:usbAttrAddr+len(usbData)]
@@ -416,6 +439,6 @@ def main():
416439
with open(args.input_file, "rb") as handle:
417440
extract_map(handle, args)
418441
except OSError:
419-
sys.exit("Error reading/writing files.")
442+
error("could not read or write a file")
420443

421444
main()

test/nes_blaster_mapext.sh

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,13 @@ python3 ../nes_blaster_mapext.py \
3333
-j -n9 -m ../test-out/map9j.png ../test-in/blastermaster-j.nes
3434
echo
3535

36-
echo "=== These should cause five errors/warnings ==="
37-
echo
36+
echo "=== These should cause some warnings and five errors ==="
3837
python3 ../nes_blaster_mapext.py ../test-in/nonexistent.nes
38+
python3 ../nes_blaster_mapext.py ../test-in/smb1.nes
3939
python3 ../nes_blaster_mapext.py \
4040
-m ../test-out/default.png ../test-in/blastermaster.nes
41-
python3 ../nes_blaster_mapext.py ../test-in/blastermaster.nes
42-
echo
4341
python3 ../nes_blaster_mapext.py \
4442
-n9 -m ../test-out/error1.png ../test-in/blastermaster-j.nes
45-
echo
4643
python3 ../nes_blaster_mapext.py \
4744
-j -n9 -m ../test-out/error2.png ../test-in/blastermaster.nes
4845
echo

0 commit comments

Comments
 (0)