991 lines
19 KiB
Python
991 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import math
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
# =============================================================================
|
|
# NES CHR constants
|
|
# =============================================================================
|
|
|
|
CHR_BANK_SIZE = 0x2000
|
|
PATTERN_TABLE_SIZE = 0x1000
|
|
|
|
TILE_SIZE_BYTES = 16
|
|
TILE_WIDTH = 8
|
|
TILE_HEIGHT = 8
|
|
|
|
TILES_PER_PATTERN_TABLE = 256
|
|
|
|
|
|
# =============================================================================
|
|
# NES RGB palette
|
|
#
|
|
# Common RGB approximation of the NES 64-color system palette.
|
|
# CHR-ROM itself does NOT store these colors. CHR pixels contain values 0-3,
|
|
# which are mapped through NES palette RAM into one of these 64 colors.
|
|
# =============================================================================
|
|
|
|
NES_RGB = [
|
|
# $00-$0F
|
|
(84, 84, 84),
|
|
(0, 30, 116),
|
|
(8, 16, 144),
|
|
(48, 0, 136),
|
|
(68, 0, 100),
|
|
(92, 0, 48),
|
|
(84, 4, 0),
|
|
(60, 24, 0),
|
|
(32, 42, 0),
|
|
(8, 58, 0),
|
|
(0, 64, 0),
|
|
(0, 60, 0),
|
|
(0, 50, 60),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
|
|
# $10-$1F
|
|
(152, 150, 152),
|
|
(8, 76, 196),
|
|
(48, 50, 236),
|
|
(92, 30, 228),
|
|
(136, 20, 176),
|
|
(160, 20, 100),
|
|
(152, 34, 32),
|
|
(120, 60, 0),
|
|
(84, 90, 0),
|
|
(40, 114, 0),
|
|
(8, 124, 0),
|
|
(0, 118, 40),
|
|
(0, 102, 120),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
|
|
# $20-$2F
|
|
(236, 238, 236),
|
|
(76, 154, 236),
|
|
(120, 124, 236),
|
|
(176, 98, 236),
|
|
(228, 84, 236),
|
|
(236, 88, 180),
|
|
(236, 106, 100),
|
|
(212, 136, 32),
|
|
(160, 170, 0),
|
|
(116, 196, 0),
|
|
(76, 208, 32),
|
|
(56, 204, 108),
|
|
(56, 180, 204),
|
|
(60, 60, 60),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
|
|
# $30-$3F
|
|
(236, 238, 236),
|
|
(168, 204, 236),
|
|
(188, 188, 236),
|
|
(212, 178, 236),
|
|
(236, 174, 236),
|
|
(236, 174, 212),
|
|
(236, 180, 176),
|
|
(228, 196, 144),
|
|
(204, 210, 120),
|
|
(180, 222, 120),
|
|
(168, 226, 144),
|
|
(152, 226, 180),
|
|
(160, 214, 228),
|
|
(160, 162, 160),
|
|
(0, 0, 0),
|
|
(0, 0, 0),
|
|
]
|
|
|
|
|
|
GRAYSCALE = [
|
|
(0, 0, 0),
|
|
(85, 85, 85),
|
|
(170, 170, 170),
|
|
(255, 255, 255),
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# CHR decoder
|
|
# =============================================================================
|
|
|
|
def decode_tile(chr_data, tile_index):
|
|
"""
|
|
Decode one NES 8x8 2bpp CHR tile.
|
|
|
|
Each tile occupies 16 bytes:
|
|
|
|
bytes 0-7 bitplane 0
|
|
bytes 8-15 bitplane 1
|
|
|
|
Each resulting pixel has a value from 0 through 3.
|
|
"""
|
|
|
|
offset = tile_index * TILE_SIZE_BYTES
|
|
tile = chr_data[offset:offset + TILE_SIZE_BYTES]
|
|
|
|
if len(tile) != TILE_SIZE_BYTES:
|
|
raise ValueError(
|
|
f"Incomplete CHR tile ${tile_index:03X}"
|
|
)
|
|
|
|
pixels = []
|
|
|
|
for y in range(TILE_HEIGHT):
|
|
plane0 = tile[y]
|
|
plane1 = tile[y + 8]
|
|
|
|
row = []
|
|
|
|
for x in range(TILE_WIDTH):
|
|
bit = 7 - x
|
|
|
|
low = (plane0 >> bit) & 1
|
|
high = (plane1 >> bit) & 1
|
|
|
|
pixel = low | (high << 1)
|
|
|
|
row.append(pixel)
|
|
|
|
pixels.append(row)
|
|
|
|
return pixels
|
|
|
|
|
|
# =============================================================================
|
|
# Palette handling
|
|
# =============================================================================
|
|
|
|
def nes_palette_to_rgb(indices):
|
|
"""
|
|
Convert four NES palette indices into RGB colors.
|
|
"""
|
|
|
|
if len(indices) != 4:
|
|
raise ValueError(
|
|
"Palette must contain exactly four NES colors"
|
|
)
|
|
|
|
return [
|
|
NES_RGB[index & 0x3F]
|
|
for index in indices
|
|
]
|
|
|
|
|
|
def parse_hex_byte(value):
|
|
"""
|
|
Parse a command-line hexadecimal byte.
|
|
|
|
Accepted examples:
|
|
|
|
0F
|
|
FF
|
|
0x20
|
|
$26
|
|
"""
|
|
|
|
value = value.strip()
|
|
|
|
if value.startswith("$"):
|
|
value = value[1:]
|
|
|
|
if value.lower().startswith("0x"):
|
|
value = value[2:]
|
|
|
|
number = int(value, 16)
|
|
|
|
if not 0 <= number <= 0xFF:
|
|
raise argparse.ArgumentTypeError(
|
|
f"Value out of byte range: {value}"
|
|
)
|
|
|
|
return number
|
|
|
|
|
|
# =============================================================================
|
|
# Tile rendering
|
|
# =============================================================================
|
|
|
|
def make_tile_image(tile, scale, colors=None):
|
|
"""
|
|
Render one decoded 8x8 tile.
|
|
|
|
colors[0..3] maps CHR pixel values 0..3 to RGB colors.
|
|
"""
|
|
|
|
if colors is None:
|
|
colors = GRAYSCALE
|
|
|
|
image = Image.new(
|
|
"RGB",
|
|
(TILE_WIDTH, TILE_HEIGHT),
|
|
colors[0]
|
|
)
|
|
|
|
pixels = image.load()
|
|
|
|
for y in range(TILE_HEIGHT):
|
|
for x in range(TILE_WIDTH):
|
|
pixels[x, y] = colors[tile[y][x]]
|
|
|
|
if scale != 1:
|
|
image = image.resize(
|
|
(
|
|
TILE_WIDTH * scale,
|
|
TILE_HEIGHT * scale
|
|
),
|
|
Image.Resampling.NEAREST
|
|
)
|
|
|
|
return image
|
|
|
|
|
|
# =============================================================================
|
|
# Clean sprite sheet
|
|
# =============================================================================
|
|
|
|
def make_clean_sheet(
|
|
chr_data,
|
|
columns=16,
|
|
scale=4,
|
|
colors=None
|
|
):
|
|
"""
|
|
Render CHR tiles as a compact sprite sheet.
|
|
"""
|
|
|
|
tile_count = len(chr_data) // TILE_SIZE_BYTES
|
|
rows = math.ceil(tile_count / columns)
|
|
|
|
rendered_width = TILE_WIDTH * scale
|
|
rendered_height = TILE_HEIGHT * scale
|
|
|
|
sheet = Image.new(
|
|
"RGB",
|
|
(
|
|
columns * rendered_width,
|
|
rows * rendered_height
|
|
),
|
|
(0, 0, 0)
|
|
)
|
|
|
|
for tile_index in range(tile_count):
|
|
tile = decode_tile(
|
|
chr_data,
|
|
tile_index
|
|
)
|
|
|
|
tile_image = make_tile_image(
|
|
tile,
|
|
scale,
|
|
colors
|
|
)
|
|
|
|
column = tile_index % columns
|
|
row = tile_index // columns
|
|
|
|
x = column * rendered_width
|
|
y = row * rendered_height
|
|
|
|
sheet.paste(
|
|
tile_image,
|
|
(x, y)
|
|
)
|
|
|
|
return sheet
|
|
|
|
|
|
# =============================================================================
|
|
# Labeled sprite sheet
|
|
# =============================================================================
|
|
|
|
def make_labeled_sheet(
|
|
chr_data,
|
|
columns=16,
|
|
tile_scale=4,
|
|
label_height=12,
|
|
tile_base=0,
|
|
colors=None
|
|
):
|
|
"""
|
|
Render a debugging sheet with each tile labeled by hexadecimal tile ID.
|
|
"""
|
|
|
|
tile_count = len(chr_data) // TILE_SIZE_BYTES
|
|
rows = math.ceil(tile_count / columns)
|
|
|
|
rendered_width = TILE_WIDTH * tile_scale
|
|
rendered_height = TILE_HEIGHT * tile_scale
|
|
|
|
cell_width = max(
|
|
rendered_width + 4,
|
|
40
|
|
)
|
|
|
|
cell_height = (
|
|
rendered_height +
|
|
label_height +
|
|
6
|
|
)
|
|
|
|
sheet = Image.new(
|
|
"RGB",
|
|
(
|
|
columns * cell_width,
|
|
rows * cell_height
|
|
),
|
|
(24, 24, 24)
|
|
)
|
|
|
|
draw = ImageDraw.Draw(sheet)
|
|
font = ImageFont.load_default()
|
|
|
|
for tile_index in range(tile_count):
|
|
tile = decode_tile(
|
|
chr_data,
|
|
tile_index
|
|
)
|
|
|
|
tile_image = make_tile_image(
|
|
tile,
|
|
tile_scale,
|
|
colors
|
|
)
|
|
|
|
column = tile_index % columns
|
|
row = tile_index // columns
|
|
|
|
cell_x = column * cell_width
|
|
cell_y = row * cell_height
|
|
|
|
tile_x = (
|
|
cell_x +
|
|
(cell_width - rendered_width) // 2
|
|
)
|
|
|
|
tile_y = cell_y + 2
|
|
|
|
sheet.paste(
|
|
tile_image,
|
|
(tile_x, tile_y)
|
|
)
|
|
|
|
display_index = tile_base + tile_index
|
|
|
|
label = f"{display_index:03X}"
|
|
|
|
bbox = draw.textbbox(
|
|
(0, 0),
|
|
label,
|
|
font=font
|
|
)
|
|
|
|
text_width = bbox[2] - bbox[0]
|
|
|
|
text_x = (
|
|
cell_x +
|
|
(cell_width - text_width) // 2
|
|
)
|
|
|
|
text_y = (
|
|
cell_y +
|
|
rendered_height +
|
|
4
|
|
)
|
|
|
|
draw.text(
|
|
(text_x, text_y),
|
|
label,
|
|
fill=(255, 255, 255),
|
|
font=font
|
|
)
|
|
|
|
return sheet
|
|
|
|
|
|
# =============================================================================
|
|
# iNES extraction
|
|
# =============================================================================
|
|
|
|
def extract_chr_banks_from_ines(rom):
|
|
"""
|
|
Extract every 8 KiB CHR-ROM bank from an iNES ROM.
|
|
"""
|
|
|
|
if len(rom) < 16:
|
|
raise ValueError(
|
|
"File is too small to contain an iNES header"
|
|
)
|
|
|
|
if rom[:4] != b"NES\x1A":
|
|
raise ValueError(
|
|
"Input does not contain a valid iNES header"
|
|
)
|
|
|
|
prg_banks = rom[4]
|
|
chr_banks = rom[5]
|
|
flags6 = rom[6]
|
|
|
|
if chr_banks == 0:
|
|
raise ValueError(
|
|
"ROM contains no CHR-ROM. "
|
|
"This cartridge may use CHR-RAM."
|
|
)
|
|
|
|
trainer_size = (
|
|
512
|
|
if flags6 & 0x04
|
|
else 0
|
|
)
|
|
|
|
prg_size = (
|
|
prg_banks * 0x4000
|
|
)
|
|
|
|
chr_offset = (
|
|
16 +
|
|
trainer_size +
|
|
prg_size
|
|
)
|
|
|
|
banks = []
|
|
|
|
for bank_index in range(chr_banks):
|
|
|
|
start = (
|
|
chr_offset +
|
|
bank_index * CHR_BANK_SIZE
|
|
)
|
|
|
|
end = (
|
|
start +
|
|
CHR_BANK_SIZE
|
|
)
|
|
|
|
if end > len(rom):
|
|
raise ValueError(
|
|
f"ROM truncated while reading "
|
|
f"CHR bank ${bank_index:02X}"
|
|
)
|
|
|
|
banks.append(
|
|
rom[start:end]
|
|
)
|
|
|
|
return banks
|
|
|
|
|
|
# =============================================================================
|
|
# Raw CHR extraction
|
|
# =============================================================================
|
|
|
|
def extract_raw_chr_banks(data):
|
|
"""
|
|
Split raw CHR data into 8 KiB banks.
|
|
"""
|
|
|
|
if len(data) % CHR_BANK_SIZE != 0:
|
|
raise ValueError(
|
|
"Raw CHR data size must be a multiple "
|
|
f"of ${CHR_BANK_SIZE:04X} bytes"
|
|
)
|
|
|
|
banks = []
|
|
|
|
for offset in range(
|
|
0,
|
|
len(data),
|
|
CHR_BANK_SIZE
|
|
):
|
|
banks.append(
|
|
data[
|
|
offset:
|
|
offset + CHR_BANK_SIZE
|
|
]
|
|
)
|
|
|
|
return banks
|
|
|
|
|
|
# =============================================================================
|
|
# GIF output
|
|
# =============================================================================
|
|
|
|
def save_gif(image, filename):
|
|
"""
|
|
Save a static GIF.
|
|
"""
|
|
|
|
image.save(
|
|
filename,
|
|
format="GIF",
|
|
optimize=False
|
|
)
|
|
|
|
print(
|
|
f" {filename} "
|
|
f"({image.width}x{image.height})"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Pattern table generation
|
|
# =============================================================================
|
|
|
|
def process_pattern_table(
|
|
pattern_data,
|
|
output_prefix,
|
|
bank_index,
|
|
table_index,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix=""
|
|
):
|
|
"""
|
|
Produce clean and labeled sheets for a 4 KiB pattern table.
|
|
"""
|
|
|
|
tile_base = (
|
|
table_index * 0x100
|
|
)
|
|
|
|
base_name = (
|
|
f"{output_prefix}"
|
|
f"_chr{bank_index:02X}"
|
|
f"_pt{table_index}"
|
|
f"{suffix}"
|
|
)
|
|
|
|
clean = make_clean_sheet(
|
|
pattern_data,
|
|
columns=columns,
|
|
scale=scale,
|
|
colors=colors
|
|
)
|
|
|
|
save_gif(
|
|
clean,
|
|
f"{base_name}.gif"
|
|
)
|
|
|
|
labeled = make_labeled_sheet(
|
|
pattern_data,
|
|
columns=columns,
|
|
tile_scale=scale,
|
|
tile_base=tile_base,
|
|
colors=colors
|
|
)
|
|
|
|
save_gif(
|
|
labeled,
|
|
f"{base_name}_labels.gif"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Full CHR bank rendering
|
|
# =============================================================================
|
|
|
|
def process_chr_bank(
|
|
bank,
|
|
bank_index,
|
|
output_prefix,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix=""
|
|
):
|
|
"""
|
|
Generate:
|
|
full 8 KiB CHR bank
|
|
pattern table 0
|
|
pattern table 1
|
|
labeled pattern table 0
|
|
labeled pattern table 1
|
|
"""
|
|
|
|
full_name = (
|
|
f"{output_prefix}"
|
|
f"_chr{bank_index:02X}"
|
|
f"_full"
|
|
f"{suffix}"
|
|
f".gif"
|
|
)
|
|
|
|
full_sheet = make_clean_sheet(
|
|
bank,
|
|
columns=columns,
|
|
scale=scale,
|
|
colors=colors
|
|
)
|
|
|
|
save_gif(
|
|
full_sheet,
|
|
full_name
|
|
)
|
|
|
|
pattern0 = bank[
|
|
0x0000:
|
|
0x1000
|
|
]
|
|
|
|
pattern1 = bank[
|
|
0x1000:
|
|
0x2000
|
|
]
|
|
|
|
process_pattern_table(
|
|
pattern0,
|
|
output_prefix,
|
|
bank_index,
|
|
0,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix
|
|
)
|
|
|
|
process_pattern_table(
|
|
pattern1,
|
|
output_prefix,
|
|
bank_index,
|
|
1,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Full 32-byte palette RAM mode
|
|
# =============================================================================
|
|
|
|
def process_palette_ram(
|
|
banks,
|
|
palette_ram,
|
|
output_prefix,
|
|
scale,
|
|
columns
|
|
):
|
|
"""
|
|
Render each CHR bank through all four background palettes and all four
|
|
sprite palettes contained in a 32-byte NES palette-RAM image.
|
|
"""
|
|
|
|
if len(palette_ram) != 32:
|
|
raise ValueError(
|
|
"Palette RAM must contain exactly 32 bytes"
|
|
)
|
|
|
|
print()
|
|
print("Generating background palette sheets")
|
|
|
|
for palette_number in range(4):
|
|
|
|
start = palette_number * 4
|
|
|
|
palette_indices = (
|
|
palette_ram[
|
|
start:
|
|
start + 4
|
|
]
|
|
)
|
|
|
|
colors = nes_palette_to_rgb(
|
|
palette_indices
|
|
)
|
|
|
|
suffix = (
|
|
f"_bg{palette_number}"
|
|
)
|
|
|
|
print()
|
|
print(
|
|
f" BG{palette_number}: " +
|
|
" ".join(
|
|
f"${value:02X}"
|
|
for value
|
|
in palette_indices
|
|
)
|
|
)
|
|
|
|
for bank_index, bank in enumerate(banks):
|
|
process_chr_bank(
|
|
bank,
|
|
bank_index,
|
|
output_prefix,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix
|
|
)
|
|
|
|
print()
|
|
print("Generating sprite palette sheets")
|
|
|
|
for palette_number in range(4):
|
|
|
|
start = (
|
|
0x10 +
|
|
palette_number * 4
|
|
)
|
|
|
|
palette_indices = (
|
|
palette_ram[
|
|
start:
|
|
start + 4
|
|
]
|
|
)
|
|
|
|
colors = nes_palette_to_rgb(
|
|
palette_indices
|
|
)
|
|
|
|
suffix = (
|
|
f"_sp{palette_number}"
|
|
)
|
|
|
|
print()
|
|
print(
|
|
f" SP{palette_number}: " +
|
|
" ".join(
|
|
f"${value:02X}"
|
|
for value
|
|
in palette_indices
|
|
)
|
|
)
|
|
|
|
for bank_index, bank in enumerate(banks):
|
|
process_chr_bank(
|
|
bank,
|
|
bank_index,
|
|
output_prefix,
|
|
scale,
|
|
columns,
|
|
colors,
|
|
suffix
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Main
|
|
# =============================================================================
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"NES CHR-ROM GIF visualizer "
|
|
"for reverse engineering"
|
|
)
|
|
)
|
|
|
|
parser.add_argument(
|
|
"input",
|
|
help="iNES ROM or raw CHR binary"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output-prefix",
|
|
default=None,
|
|
help="Output filename prefix"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--raw",
|
|
action="store_true",
|
|
help="Treat input as raw CHR data"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--scale",
|
|
type=int,
|
|
default=4,
|
|
help="Tile scale factor (default: 4)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--columns",
|
|
type=int,
|
|
default=16,
|
|
help="Tiles per row (default: 16)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--palette",
|
|
nargs=4,
|
|
type=parse_hex_byte,
|
|
metavar=("C0", "C1", "C2", "C3"),
|
|
help=(
|
|
"Render using one NES four-color palette. "
|
|
"Example: --palette 0F 20 0F 06"
|
|
)
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--palette-ram",
|
|
nargs=32,
|
|
type=parse_hex_byte,
|
|
metavar="XX",
|
|
help=(
|
|
"Complete 32-byte NES palette RAM image. "
|
|
"Generates output for all four BG and "
|
|
"all four sprite palettes."
|
|
)
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.scale < 1:
|
|
parser.error(
|
|
"--scale must be at least 1"
|
|
)
|
|
|
|
if args.columns < 1:
|
|
parser.error(
|
|
"--columns must be at least 1"
|
|
)
|
|
|
|
if (
|
|
args.palette is not None and
|
|
args.palette_ram is not None
|
|
):
|
|
parser.error(
|
|
"--palette and --palette-ram "
|
|
"cannot be used together"
|
|
)
|
|
|
|
input_path = Path(
|
|
args.input
|
|
)
|
|
|
|
data = input_path.read_bytes()
|
|
|
|
if args.output_prefix:
|
|
output_prefix = (
|
|
args.output_prefix
|
|
)
|
|
else:
|
|
output_prefix = (
|
|
input_path.stem
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Extract CHR
|
|
# -------------------------------------------------------------------------
|
|
|
|
if args.raw:
|
|
|
|
banks = extract_raw_chr_banks(
|
|
data
|
|
)
|
|
|
|
elif data[:4] == b"NES\x1A":
|
|
|
|
banks = extract_chr_banks_from_ines(
|
|
data
|
|
)
|
|
|
|
else:
|
|
|
|
print(
|
|
"No iNES header detected. "
|
|
"Treating input as raw CHR."
|
|
)
|
|
|
|
banks = extract_raw_chr_banks(
|
|
data
|
|
)
|
|
|
|
print(
|
|
f"Input: {input_path}"
|
|
)
|
|
|
|
print(
|
|
f"CHR banks: {len(banks)}"
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Full palette RAM mode
|
|
# -------------------------------------------------------------------------
|
|
|
|
if args.palette_ram is not None:
|
|
|
|
process_palette_ram(
|
|
banks,
|
|
args.palette_ram,
|
|
output_prefix,
|
|
args.scale,
|
|
args.columns
|
|
)
|
|
|
|
return
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Single-palette / grayscale mode
|
|
# -------------------------------------------------------------------------
|
|
|
|
if args.palette is not None:
|
|
|
|
colors = nes_palette_to_rgb(
|
|
args.palette
|
|
)
|
|
|
|
suffix = "_color"
|
|
|
|
print(
|
|
"Palette: " +
|
|
" ".join(
|
|
f"${value:02X}"
|
|
for value
|
|
in args.palette
|
|
)
|
|
)
|
|
|
|
else:
|
|
|
|
colors = GRAYSCALE
|
|
suffix = ""
|
|
|
|
print(
|
|
"Palette: grayscale CHR values"
|
|
)
|
|
|
|
for bank_index, bank in enumerate(banks):
|
|
|
|
print()
|
|
print(
|
|
f"CHR bank ${bank_index:02X}"
|
|
)
|
|
|
|
print(
|
|
f" Size: ${len(bank):04X}"
|
|
)
|
|
|
|
print(
|
|
f" Tiles: "
|
|
f"{len(bank) // TILE_SIZE_BYTES}"
|
|
)
|
|
|
|
process_chr_bank(
|
|
bank,
|
|
bank_index,
|
|
output_prefix,
|
|
args.scale,
|
|
args.columns,
|
|
colors,
|
|
suffix
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |