Initial commit
This commit is contained in:
commit
8683e2f94a
22 files changed
+43771
No files matched your search
@@ -0,0 +1,10 @@
|
||||
all: reconstructed_prg.bin verify
|
||||
prg.o: prg.s symbols.inc prg.bin
|
||||
ca65 -g -o $@ prg.s
|
||||
reconstructed_prg.bin: prg.o linker.cfg
|
||||
ld65 -C linker.cfg -m reconstructed.map -Ln reconstructed.lbl -o $@ prg.o
|
||||
verify: reconstructed_prg.bin prg.bin
|
||||
cmp reconstructed_prg.bin prg.bin
|
||||
@echo BYTE-IDENTICAL
|
||||
clean:
|
||||
rm -f prg.o reconstructed_prg.bin reconstructed.map reconstructed.lbl
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
MEMORY {
|
||||
PRG: start = $C000, size = $4000, file = %O, fill = yes;
|
||||
}
|
||||
SEGMENTS {
|
||||
PRG: load = PRG, type = ro;
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,85 @@
|
||||
; PPU registers
|
||||
PPUCTRL = $2000
|
||||
PPUMASK = $2001
|
||||
PPUSTATUS = $2002
|
||||
OAMADDR = $2003
|
||||
OAMDATA = $2004
|
||||
PPUSCROLL = $2005
|
||||
PPUADDR = $2006
|
||||
PPUDATA = $2007
|
||||
OAMDMA = $4014
|
||||
JOY1 = $4016
|
||||
|
||||
; Variables
|
||||
PpuControlShadow = $10
|
||||
PpuMaskShadow = $11
|
||||
Controller1Held = $14
|
||||
Controller1Pressed = $15
|
||||
Controller2Held = $16
|
||||
Controller2Pressed = $17
|
||||
ControllerState = Controller1Held
|
||||
NmiDispatchMode = $40
|
||||
NmiState = $41
|
||||
MenuSelectionIndex = $42
|
||||
NmiSequenceStep = $44
|
||||
NmiSyncState = $48
|
||||
InputActionLatch = $49
|
||||
OamBuffer = $0200
|
||||
PersistentRecord0 = $0580
|
||||
PersistentRecord1 = $05A3
|
||||
ResetMagic1 = $05CE
|
||||
ResetMagic2 = $05CF
|
||||
PersistentBuffer = $06E0
|
||||
PAD_RIGHT=$01
|
||||
PAD_LEFT=$02
|
||||
PAD_DOWN=$04
|
||||
PAD_UP=$08
|
||||
PAD_START=$10
|
||||
PAD_SELECT=$20
|
||||
PAD_B=$40
|
||||
PAD_A=$80
|
||||
PAD_HORIZONTAL=PAD_LEFT|PAD_RIGHT
|
||||
PAD_VERTICAL=PAD_UP|PAD_DOWN
|
||||
PAD_DPAD=PAD_HORIZONTAL|PAD_VERTICAL
|
||||
PAD_MENU_INPUT=PAD_SELECT|PAD_START|PAD_VERTICAL
|
||||
MENU_HORIZONTAL_COUNT=$05
|
||||
MENU_VERTICAL_COUNT=$06
|
||||
MENU_ACTION_NONE=$00
|
||||
MENU_ACTION_ACCEPTED=$04
|
||||
NMI_SYNC_PENDING=$FF
|
||||
NMI_SYNC_COMPLETE=$01
|
||||
RESET_MAGIC_1=$A5
|
||||
RESET_MAGIC_2=$5A
|
||||
PPUCTRL_NMI_ENABLE=$80
|
||||
|
||||
WorkingBuffer = $05E0
|
||||
PERSISTENT_BUFFER_SIZE = $0100
|
||||
|
||||
StateTargetCA85 = $CA85
|
||||
|
||||
; Serial I/O helpers using $4016 (verified structure; peripheral identity unknown)
|
||||
WriteSerialTiming = $C62D
|
||||
WaitSerialPulse = $C639
|
||||
TransmitPulseShort = $C64E
|
||||
TransmitPulseLong = $C653
|
||||
TransmitByte = $C66E
|
||||
IncrementSerialCount = $C68A
|
||||
ReceiveSerialBit = $C691
|
||||
ReceiveByte = $C6A5
|
||||
|
||||
; Block protocol helpers
|
||||
TransmitRuntimeData = $C6C8
|
||||
TransmitDataBlock = $C6D1
|
||||
SetupTransmitRamBlock = $C716
|
||||
SetupTransmitWorkingBuffer = $C726
|
||||
SetTransmitPreambleCounts = $C734
|
||||
ReceiveRuntimeData = $C739
|
||||
ReceiveDataBlock = $C792
|
||||
SerialBlockLength = $0D
|
||||
SerialBlockPtr = $0E
|
||||
SerialBitCountLo = $08
|
||||
SerialBitCountHi = $09
|
||||
|
||||
; App Specific Variables
|
||||
TMP00 = $00
|
||||
TMP01 = $01
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
all: reconstructed_prg.bin verify
|
||||
prg.o: prg.s symbols.inc prg.bin
|
||||
ca65 -g -o $@ prg.s
|
||||
reconstructed_prg.bin: prg.o linker.cfg
|
||||
ld65 -C linker.cfg -m reconstructed.map -Ln reconstructed.lbl -o $@ prg.o
|
||||
verify: reconstructed_prg.bin prg.bin
|
||||
cmp reconstructed_prg.bin prg.bin
|
||||
@echo BYTE-IDENTICAL
|
||||
clean:
|
||||
rm -f prg.o reconstructed_prg.bin reconstructed.map reconstructed.lbl
|
||||
@@ -0,0 +1,6 @@
|
||||
MEMORY {
|
||||
PRG: start = $C000, size = $4000, file = %O, fill = yes;
|
||||
}
|
||||
SEGMENTS {
|
||||
PRG: load = PRG, type = ro;
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Whitespace-only changes.
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
# Disassembly Project 1
|
||||
|
||||
This is a set of disassembled NES roms using AI to translate.
|
||||
|
||||
This is not for distribution.
|
||||
This is not for you.
|
||||
|
||||
## Roms Reproduced
|
||||
|
||||
| Game | Rom | MD5 | Status |
|
||||
| Balloon Fight | Balloon Fight (U) | c58e5f600884aa7daadc1ea498f1df24 | WIP |
|
||||
| Excitebike | Excitebike (Japan, USA) | d7fe15cf2bc7b6582c07d12b3cf3bede | Complete |
|
||||
| Pacman (Namco) | Pac-Man (USA) (Namco) | 3ddca58c51dd38d59f9271d24e7105f9 | Complete |
|
||||
@@ -0,0 +1,991 @@
|
||||
#!/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()
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
all: reconstructed_prg.bin verify
|
||||
prg.o: prg.s symbols.inc prg.bin
|
||||
ca65 -g -o $@ prg.s
|
||||
reconstructed_prg.bin: prg.o linker.cfg
|
||||
ld65 -C linker.cfg -m reconstructed.map -Ln reconstructed.lbl -o $@ prg.o
|
||||
verify: reconstructed_prg.bin prg.bin
|
||||
cmp reconstructed_prg.bin prg.bin
|
||||
@echo BYTE-IDENTICAL
|
||||
clean:
|
||||
rm -f prg.o reconstructed_prg.bin reconstructed.map reconstructed.lbl
|
||||
@@ -0,0 +1,6 @@
|
||||
MEMORY {
|
||||
PRG: start = $C000, size = $4000, file = %O, fill = yes;
|
||||
}
|
||||
SEGMENTS {
|
||||
PRG: load = PRG, type = ro;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,85 @@
|
||||
; PPU registers
|
||||
PPUCTRL = $2000
|
||||
PPUMASK = $2001
|
||||
PPUSTATUS = $2002
|
||||
OAMADDR = $2003
|
||||
OAMDATA = $2004
|
||||
PPUSCROLL = $2005
|
||||
PPUADDR = $2006
|
||||
PPUDATA = $2007
|
||||
OAMDMA = $4014
|
||||
JOY1 = $4016
|
||||
|
||||
; Variables
|
||||
PpuControlShadow = $10
|
||||
PpuMaskShadow = $11
|
||||
Controller1Held = $14
|
||||
Controller1Pressed = $15
|
||||
Controller2Held = $16
|
||||
Controller2Pressed = $17
|
||||
ControllerState = Controller1Held
|
||||
NmiDispatchMode = $40
|
||||
NmiState = $41
|
||||
MenuSelectionIndex = $42
|
||||
NmiSequenceStep = $44
|
||||
NmiSyncState = $48
|
||||
InputActionLatch = $49
|
||||
OamBuffer = $0200
|
||||
PersistentRecord0 = $0580
|
||||
PersistentRecord1 = $05A3
|
||||
ResetMagic1 = $05CE
|
||||
ResetMagic2 = $05CF
|
||||
PersistentBuffer = $06E0
|
||||
PAD_RIGHT=$01
|
||||
PAD_LEFT=$02
|
||||
PAD_DOWN=$04
|
||||
PAD_UP=$08
|
||||
PAD_START=$10
|
||||
PAD_SELECT=$20
|
||||
PAD_B=$40
|
||||
PAD_A=$80
|
||||
PAD_HORIZONTAL=PAD_LEFT|PAD_RIGHT
|
||||
PAD_VERTICAL=PAD_UP|PAD_DOWN
|
||||
PAD_DPAD=PAD_HORIZONTAL|PAD_VERTICAL
|
||||
PAD_MENU_INPUT=PAD_SELECT|PAD_START|PAD_VERTICAL
|
||||
MENU_HORIZONTAL_COUNT=$05
|
||||
MENU_VERTICAL_COUNT=$06
|
||||
MENU_ACTION_NONE=$00
|
||||
MENU_ACTION_ACCEPTED=$04
|
||||
NMI_SYNC_PENDING=$FF
|
||||
NMI_SYNC_COMPLETE=$01
|
||||
RESET_MAGIC_1=$A5
|
||||
RESET_MAGIC_2=$5A
|
||||
PPUCTRL_NMI_ENABLE=$80
|
||||
|
||||
WorkingBuffer = $05E0
|
||||
PERSISTENT_BUFFER_SIZE = $0100
|
||||
|
||||
StateTargetCA85 = $CA85
|
||||
|
||||
; Serial I/O helpers using $4016 (verified structure; peripheral identity unknown)
|
||||
WriteSerialTiming = $C62D
|
||||
WaitSerialPulse = $C639
|
||||
TransmitPulseShort = $C64E
|
||||
TransmitPulseLong = $C653
|
||||
TransmitByte = $C66E
|
||||
IncrementSerialCount = $C68A
|
||||
ReceiveSerialBit = $C691
|
||||
ReceiveByte = $C6A5
|
||||
|
||||
; Block protocol helpers
|
||||
TransmitRuntimeData = $C6C8
|
||||
TransmitDataBlock = $C6D1
|
||||
SetupTransmitRamBlock = $C716
|
||||
SetupTransmitWorkingBuffer = $C726
|
||||
SetTransmitPreambleCounts = $C734
|
||||
ReceiveRuntimeData = $C739
|
||||
ReceiveDataBlock = $C792
|
||||
SerialBlockLength = $0D
|
||||
SerialBlockPtr = $0E
|
||||
SerialBitCountLo = $08
|
||||
SerialBitCountHi = $09
|
||||
|
||||
; App Specific Variables
|
||||
TMP00 = $00
|
||||
TMP01 = $01
|
||||
Reference in new issue
Block a user