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