102
102
0x3f : (0x00 , 0x00 , 0x00 ),
103
103
}
104
104
105
+ def error (msg ):
106
+ sys .exit (f"Error: { msg } ." )
107
+
108
+ def warn (msg ):
109
+ print (f"Warning: { msg } ." , file = sys .stderr )
110
+
105
111
def parse_arguments ():
106
112
parser = argparse .ArgumentParser (
107
113
description = "Extract world maps from NES Blaster Master to PNG files."
@@ -134,7 +140,7 @@ def parse_arguments():
134
140
)
135
141
parser .add_argument (
136
142
"-v" , "--verbose" , action = "store_true" ,
137
- help = "Print more information. Note: all addresses are hexadecimal. "
143
+ help = "Print more information."
138
144
)
139
145
parser .add_argument (
140
146
"input_file" ,
@@ -144,18 +150,18 @@ def parse_arguments():
144
150
args = parser .parse_args ()
145
151
146
152
if not 0 <= args .map_number <= 15 :
147
- sys . exit ( "Invalid map number. " )
153
+ error ( "invalid map number" )
148
154
149
155
if not os .path .isfile (args .input_file ):
150
- sys . exit ( "Input file not found. " )
156
+ error ( "input file not found" )
151
157
152
158
outputFiles = (
153
159
args .usb_image , args .sb_image , args .block_image , args .map_image
154
160
)
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" )
157
163
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" )
159
165
160
166
return args
161
167
@@ -187,19 +193,11 @@ def decode_ines_header(handle):
187
193
"mapper" : (flags7 & 0b11110000 ) | (flags6 >> 4 ),
188
194
}
189
195
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
-
199
196
def decode_offset (bytes_ ):
200
197
# decode address, convert into offset within bank
201
198
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" )
203
201
return addr & 0x3fff
204
202
205
203
def get_tile_data (handle ):
@@ -215,18 +213,26 @@ def get_tile_data(handle):
215
213
)
216
214
yield tuple (pixels )
217
215
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
+
218
224
def create_usb_image (usbData , usbAttrData , tileData , worldPal ):
219
225
# return image with up to 16*16 USBs
220
226
# worldPal: world palette (16 NES colors)
221
227
222
228
image = Image .new ("P" , (16 * 16 , (len (usbData ) + 15 ) // 16 * 16 ), 0 )
223
229
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 )
228
233
image .putpalette (itertools .chain .from_iterable (imgPal ))
229
234
worldPalToImgPal = tuple (imgPal .index (color ) for color in worldPal )
235
+ del worldPal , imgPal
230
236
231
237
# for each USB, draw 2*2 tiles with correct palette
232
238
tileImg = Image .new ("P" , (8 , 8 ))
@@ -243,17 +249,13 @@ def create_usb_image(usbData, usbAttrData, tileData, worldPal):
243
249
image .paste (tileImg , (x , y ))
244
250
return image
245
251
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
-
252
252
def create_sb_image (sbData , usbImg , worldPal ):
253
253
# return image with up to 16*16 SBs
254
254
255
255
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
+ ))
257
259
258
260
# copy USBs from source image to target image
259
261
for (si , usbs ) in enumerate (sbData ):
@@ -273,7 +275,9 @@ def create_block_image(blockData, sbData, usbImg, worldPal):
273
275
# return image with up to 16*16 blocks
274
276
275
277
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
+ ))
277
281
278
282
# copy USBs from source image to target image
279
283
for (bi , block ) in enumerate (blockData ):
@@ -294,7 +298,9 @@ def create_map_image(mapData, blockData, sbData, usbImg, worldPal):
294
298
# return image with up to 32*32 blocks
295
299
296
300
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
+ ))
298
304
299
305
# copy USBs from source image to target image
300
306
for (bi , block ) in enumerate (mapData ):
@@ -317,10 +323,14 @@ def extract_map(source, args):
317
323
# parse iNES header
318
324
fileInfo = decode_ines_header (source )
319
325
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)" )
321
330
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" )
324
334
325
335
(prgBank , worldPtr , chrBank ) = MAP_DATA_ADDRESSES [args .map_number ]
326
336
# the only version difference
@@ -336,47 +346,60 @@ def extract_map(source, args):
336
346
337
347
if args .verbose :
338
348
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
+ )
340
352
341
353
(palAddr , usbAttrAddr , usbAddr , sbAddr , blockAddr , mapAddr ) = (
342
354
decode_offset (prgBankData [pos :pos + 2 ])
343
355
for pos in range (worldAddr , worldAddr + 12 , 2 )
344
356
)
345
357
if args .verbose :
346
358
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} "
353
365
)
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" )
356
369
357
370
# palette (4*4 NES color numbers)
358
371
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" )
360
376
361
377
# 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" )
363
380
usbData = tuple (prgBankData [i :i + 4 ] for i in range (usbAddr , sbAddr , 4 ))
364
381
365
382
# 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" )
367
385
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" )
369
388
370
389
# 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" )
372
392
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" )
374
395
375
396
# map data
376
397
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" )
378
400
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" )
380
403
381
404
# read USB attribute data (1 byte/USB)
382
405
usbAttrData = prgBankData [usbAttrAddr :usbAttrAddr + len (usbData )]
@@ -416,6 +439,6 @@ def main():
416
439
with open (args .input_file , "rb" ) as handle :
417
440
extract_map (handle , args )
418
441
except OSError :
419
- sys . exit ( "Error reading/writing files. " )
442
+ error ( "could not read or write a file " )
420
443
421
444
main ()
0 commit comments