Update and add more tool scripts

This commit is contained in:
Rangi 2022-09-18 23:25:22 -04:00
parent 3520a4c99a
commit 208ea07237
12 changed files with 3207 additions and 95 deletions

70
tools/free_space.awk Executable file
View file

@ -0,0 +1,70 @@
#!/usr/bin/gawk -f
# Usage: tools/free_space.awk [BANK=<bank_spec>] pokered.map
# The BANK argument allows printing free space in one, all, or none of the ROM's banks.
# Valid arguments are numbers (in decimal "42" or hexadecimal "0x2a"), "all" or "none".
# If not specified, defaults to "none".
# The `BANK` argument MUST be before the map file name, otherwise it has no effect!
# Yes: tools/free_space.awk BANK=all pokered.map
# No: tools/free_space.awk pokered.map BANK=42
# Copyright (c) 2020, Eldred Habert.
# SPDX-License-Identifier: MIT
BEGIN {
nb_banks = 0
free = 0
rom_bank = 0 # Safety net for malformed files
# Default settings
# Variables assigned via the command-line (except through `-v`) are *after* `BEGIN`
BANK="none"
}
# Only accept ROM banks, ignore everything else
toupper($0) ~ /^[ \t]*ROM[0X][ \t]+BANK[ \t]+#/ {
nb_banks++
rom_bank = 1
split($0, fields, /[ \t]/)
bank_num = strtonum(substr(fields[3], 2))
}
function register_bank(amount) {
free += amount
rom_bank = 0 # Reject upcoming banks by default
if (BANK ~ /all/ || BANK == bank_num) {
printf "Bank %3d: %5d/16384 (%.2f%%)\n", bank_num, amount, amount * 100 / 16384
}
}
rom_bank && toupper($0) ~ /^[ \t]*EMPTY/ {
# Empty bank
register_bank(16384)
}
rom_bank && toupper($0) ~ /^[ \t]*SLACK:[ \t]/ {
if ($2 ~ /\$[0-9A-F]+/) {
register_bank(strtonum("0x" substr($2, 2)))
} else {
printf "Malformed slack line? \"%s\" does not start with '$'\n", $2
}
}
END {
# Determine number of banks, by rounding to upper power of 2
total_banks = 2 # Smallest size is 2 banks
while (total_banks < nb_banks) {
total_banks *= 2
}
# RGBLINK omits "trailing" ROM banks, so fake them
bank_num = nb_banks
while (bank_num < total_banks) {
register_bank(16384)
bank_num++
}
total = total_banks * 16384
printf "Free space: %5d/%5d (%.2f%%)\n", free, total, free * 100 / total
}

69
tools/free_space.py Executable file
View file

@ -0,0 +1,69 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python3 free_space.py [BANK=none] [pokered.map]
Calculate the free space in the ROM or its individual banks.
The BANK argument allows printing free space in one, all, or none of the ROM's banks.
Valid arguments are numbers (in decimal "42" or hexadecimal "0x2A"), "all" or "none".
If not specified, defaults to "none".
"""
import sys
from mapreader import MapReader
def main():
print_bank = 'none'
filename = 'pokered.map'
for arg in sys.argv[1:]:
if arg.startswith('BANK='):
print_bank = arg.split('=', 1)[-1]
else:
filename = arg
if print_bank not in {'all', 'none'}:
try:
print_bank = (int(print_bank[2:], 16)
if print_bank.startswith('0x') or print_bank.startswith('0X')
else int(print_bank))
except ValueError:
error = f'Error: invalid BANK: {print_bank}'
if print_bank.isalnum():
error += f' (did you mean: 0x{print_bank}?)'
print(error, file=sys.stderr)
sys.exit(1)
num_banks = 0x80
bank_size = 0x4000 # bytes
total_size = num_banks * bank_size
reader = MapReader()
with open(filename, 'r', encoding='utf-8') as file:
reader.read_map_data(file.readlines())
free_space = 0
per_bank = []
default_bank_data = {'sections': [], 'used': 0, 'slack': bank_size}
for bank in range(num_banks):
bank_data = reader.bank_data['ROM0 bank' if bank == 0 else 'ROMX bank']
data = bank_data.get(bank, default_bank_data)
used, slack = data['used'], data['slack']
per_bank.append((used, slack))
free_space += slack
free_percent = 100 * free_space / total_size
print(f'Free space: {free_space}/{total_size} ({free_percent:.2f}%)')
if print_bank != 'none':
print()
print('bank, used, free')
for bank in range(num_banks):
used, slack = per_bank[bank]
if print_bank in {'all', bank}:
print(f'${bank:02X}, {used}, {slack}')
if __name__ == '__main__':
main()

173
tools/mapreader.py Normal file
View file

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# A library for parsing the pokered.map file output by rgbds.
import re
class MapReader:
# {'ROM Bank': { 0: { 'sections': [ { 'beg': 1234,
# 'end': 5678,
# 'name': 'Section001',
# 'symbols': [ { 'symbol': 'Function1234',
# 'address: 1234,
# },
# ]
# },
# ],
# 'used': 1234,
# 'slack': 4567,
# },
# },
# 'OAM': { 'sections': [ { 'beg': 1234,
# 'end': 5678,
# 'name': 'Section002',
# 'symbols': [ { 'symbol': 'Data1234',
# 'address: 1234,
# },
# ]
# },
# ],
# 'used': 1234,
# 'slack': 4567,
# },
# }
#
bank_data = {}
bank_types = {
'HRAM' : { 'size': 0x80, 'banked': False, },
'OAM' : { 'size': 0xA0, 'banked': False, },
'ROM0 bank': { 'size': 0x4000, 'banked': True, },
'ROMX bank': { 'size': 0x4000, 'banked': True, },
'SRAM bank': { 'size': 0x2000, 'banked': True, },
'VRAM bank': { 'size': 0x1000, 'banked': True, },
'WRAM bank': { 'size': 0x2000, 'banked': True, },
}
# FSM states
INIT, BANK, SECTION = range(3)
# $506D-$519A ($012E bytes) ["Type Matchups"]
section_header_regex = re.compile('\$([0-9A-Fa-f]{4})-\$([0-9A-Fa-f]{4}) \(.*\) \["(.*)"\]')
# $506D = TypeMatchups
section_data_regex = re.compile('\$([0-9A-Fa-f]{4}) = (.*)')
# $3ED2 bytes
slack_regex = re.compile('\$([0-9A-Fa-f]{4}) bytes?')
def __init__(self, *args, **kwargs):
self.__dict__.update(kwargs)
def _parse_init(self, line):
line = line.split(':', 1)[0]
parts = line.split(' #', 1)
if (parts[0] in self.bank_types):
self._cur_bank_name = parts[0]
self._cur_bank_type = self.bank_types[self._cur_bank_name]
if (self._cur_bank_type['banked'] and len(parts) > 1):
parts[1] = parts[1].split(':', 1)[0]
parts[1] = parts[1].split(' ', 1)[0]
self._cur_bank = int(parts[1], 10)
if self._cur_bank_name not in self.bank_data:
self.bank_data[self._cur_bank_name] = {}
if self._cur_bank_type['banked']:
if self._cur_bank not in self.bank_data[self._cur_bank_name]:
self.bank_data[self._cur_bank_name][self._cur_bank] = {}
self._cur_data = self.bank_data[self._cur_bank_name][self._cur_bank]
else:
self._cur_data = self.bank_data[self._cur_bank_name]
if ({} == self._cur_data):
self._cur_data['sections'] = []
self._cur_data['used'] = 0
self._cur_data['slack'] = self._cur_bank_type['size']
return True
return False
def _parse_section_header(self, header):
section_data = self.section_header_regex.match(header)
if section_data is not None:
beg = int(section_data.group(1), 16)
end = int(section_data.group(2), 16)
name = section_data.group(3)
self._cur_section = {'beg': beg, 'end': end, 'name': name, 'symbols': []}
self._cur_data['sections'].append(self._cur_section)
return True
return False
def _parse_slack(self, data):
slack_data = self.slack_regex.match(data)
slack_bytes = int(slack_data.group(1), 16)
self._cur_data['slack'] = slack_bytes
used_bytes = 0
for s in self._cur_data['sections']:
used_bytes += s['end'] - s['beg'] + 1
self._cur_data['used'] = used_bytes
def read_map_data(self, map):
if type(map) is str:
map = map.split('\n')
self._state = MapReader.INIT
self._cur_bank_name = ''
self._cur_bank_type = {}
self._cur_bank = 0
self._cur_data = {}
for line in map:
line = line.rstrip()
if (MapReader.INIT == self._state):
if (self._parse_init(line)):
self._state = MapReader.BANK
elif (MapReader.BANK == self._state or MapReader.SECTION == self._state):
if ('' == line):
self._state = MapReader.INIT
else:
line = line.lstrip()
parts = line.split(': ', 1)
if (MapReader.SECTION == self._state):
section_data = self.section_data_regex.match(parts[0])
if section_data is not None:
address = int(section_data.group(1), 16)
name = section_data.group(2)
self._cur_section['symbols'].append({'name': name, 'address': address})
continue
if ('SECTION' == parts[0]):
if (self._parse_section_header(parts[1])):
self._state = MapReader.SECTION
elif ('SLACK' == parts[0]):
self._parse_slack(parts[1])
self._state = MapReader.INIT
elif ('EMPTY' == parts[0]):
self._cur_data = {'sections': [], 'used': 0, 'slack': self._cur_bank_type['size']}
self._state = MapReader.INIT
else:
pass
for k, v in self.bank_data.items():
if (self.bank_types[k]['banked']):
for _, vv in v.items():
vv['sections'].sort(key=lambda x: x['beg'])
for vvv in vv['sections']:
vvv['symbols'].sort(key=lambda x: x['address'])
else:
v['sections'].sort(key=lambda x: x['beg'])
for vv in v['sections']:
vv['symbols'].sort(key=lambda x: x['address'])

77
tools/palfix.py Executable file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python palfix.py image.png
Fix the palette format of the input image. Colored images (Gen 2 Pokémon or
trainer sprites) will become indexed, with a palette sorted {white, light
color, dark color, black}. Grayscale images (all Gen 1 images) will become
two-bit grayscale.
"""
import sys
import png
def rgb8_to_rgb5(c):
r, g, b = c
return (r // 8, g // 8, b // 8)
def rgb5_to_rgb8(c):
r, g, b = c
return (r * 8 + r // 4, g * 8 + g // 4, b * 8 + b // 4)
def invert(c):
r, g, b = c
return (31 - r, 31 - g, 31 - b)
def luminance(c):
r, g, b = c
return 0.299 * r**2 + 0.587 * g**2 + 0.114 * b**2
def rgb5_pixels(row):
yield from (rgb8_to_rgb5(row[x:x+3]) for x in range(0, len(row), 4))
def is_grayscale(palette):
return (palette == ((31, 31, 31), (21, 21, 21), (10, 10, 10), (0, 0, 0)) or
palette == ((31, 31, 31), (20, 20, 20), (10, 10, 10), (0, 0, 0)))
def fix_pal(filename):
with open(filename, 'rb') as file:
width, height, rows = png.Reader(file).asRGBA8()[:3]
rows = list(rows)
b_and_w = {(0, 0, 0), (31, 31, 31)}
colors = {c for row in rows for c in rgb5_pixels(row)} - b_and_w
if not colors:
colors = {(21, 21, 21), (10, 10, 10)}
elif len(colors) == 1:
c = colors.pop()
colors = {c, invert(c)}
elif len(colors) != 2:
return False
palette = tuple(sorted(colors | b_and_w, key=luminance, reverse=True))
assert len(palette) == 4
rows = [list(map(palette.index, rgb5_pixels(row))) for row in rows]
if is_grayscale(palette):
rows = [[3 - c for c in row] for row in rows]
writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
else:
palette = tuple(map(rgb5_to_rgb8, palette))
writer = png.Writer(width, height, palette=palette, bitdepth=8, compression=9)
with open(filename, 'wb') as file:
writer.write(file, rows)
return True
def main():
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
sys.exit(1)
for filename in sys.argv[1:]:
if not filename.lower().endswith('.png'):
print(f'{filename} is not a .png file!', file=sys.stderr)
elif not fix_pal(filename):
print(f'{filename} has too many colors!', file=sys.stderr)
if __name__ == '__main__':
main()

0
tools/pic.py Normal file → Executable file
View file

2357
tools/png.py Normal file

File diff suppressed because it is too large Load diff

38
tools/rgb555.py Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python rgb555.py image.png
Round all colors of the input image to RGB555.
"""
import sys
import png
def rgb8_to_rgb5(c):
return (c & 0b11111000) | (c >> 5)
def round_pal(filename):
with open(filename, 'rb') as file:
width, height, rows = png.Reader(file).asRGBA8()[:3]
rows = list(rows)
for row in rows:
del row[3::4]
rows = [[rgb8_to_rgb5(c) for c in row] for row in rows]
writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
with open(filename, 'wb') as file:
writer.write(file, rows)
def main():
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} pic.png', file=sys.stderr)
sys.exit(1)
for filename in sys.argv[1:]:
if not filename.lower().endswith('.png'):
print(f'{filename} is not a .png file!', file=sys.stderr)
round_pal(filename)
if __name__ == '__main__':
main()

52
tools/sym_comments.py Executable file
View file

@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python sym_comments.py file.asm [pokered.sym] > file_commented.asm
Comments each label in file.asm with its bank:address from the sym file.
"""
import sys
import re
def main():
if len(sys.argv) not in {2, 3}:
print(f'Usage: {sys.argv[0]} file.asm [pokered.sym] > file_commented.asm', file=sys.stderr)
sys.exit(1)
wram_name = sys.argv[1]
sym_name = sys.argv[2] if len(sys.argv) == 3 else 'pokered.sym'
sym_def_rx = re.compile(r'(^{sym})(:.*)|(^\.{sym})(.*)'.format(sym=r'[A-Za-z_][A-Za-z0-9_#@]*'))
sym_addrs = {}
with open(sym_name, 'r', encoding='utf-8') as file:
for line in file:
line = line.split(';', 1)[0].rstrip()
parts = line.split(' ', 1)
if len(parts) != 2:
continue
addr, sym = parts
sym_addrs[sym] = addr
with open(wram_name, 'r', encoding='utf-8') as file:
cur_label = None
for line in file:
line = line.rstrip()
if (m = re.match(sym_def_rx, line)):
sym, rest = m.group(1), m.group(2)
if sym is None and rest is None:
sym, rest = m.group(3), m.group(4)
key = sym
if not sym.startswith('.'):
cur_label = sym
elif cur_label:
key = cur_label + sym
if key in sym_addrs:
addr = sym_addrs[key]
line = sym + rest + ' ; ' + addr
print(line)
if __name__ == '__main__':
main()

99
tools/toc.py Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python toc.py file.md
Replace a "## TOC" heading in a Markdown file with a table of contents,
generated from the other headings in the file. Supports multiple files.
Headings must start with "##" signs to be detected.
"""
import sys
import re
from collections import namedtuple
from urllib.parse import quote
toc_name = 'Contents'
valid_toc_headings = {'## TOC', '##TOC'}
TocItem = namedtuple('TocItem', ['name', 'anchor', 'level'])
punctuation_rx = re.compile(r'[^\w\- ]+')
numbered_heading_rx = re.compile(r'^[0-9]+\. ')
specialchar_rx = re.compile(r'[⅔]+')
def name_to_anchor(name):
# GitHub's algorithm for generating anchors from headings
# https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
anchor = name.strip().lower() # lowercase
anchor = re.sub(punctuation_rx, '', anchor) # remove punctuation
anchor = anchor.replace(' ', '-') # replace spaces with dash
anchor = re.sub(specialchar_rx, '', anchor) # remove misc special chars
anchor = quote(anchor) # url encode
return anchor
def get_toc_index(lines):
toc_index = None
for i, line in enumerate(lines):
if line.rstrip() in valid_toc_headings:
toc_index = i
break
return toc_index
def get_toc_items(lines, toc_index):
for i, line in enumerate(lines):
if i <= toc_index:
continue
if line.startswith('##'):
name = line.lstrip('#')
level = len(line) - len(name) - len('##')
name = name.strip()
anchor = name_to_anchor(name)
yield TocItem(name, anchor, level)
def toc_string(toc_items):
lines = [f'## {toc_name}', '']
for name, anchor, level in toc_items:
padding = ' ' * level
if re.match(numbered_heading_rx, name):
bullet, name = name.split('.', 1)
bullet += '.'
name = name.lstrip()
else:
bullet = '-'
lines.append(f'{padding}{bullet} [{name}](#{anchor})')
return '\n'.join(lines) + '\n'
def add_toc(filename):
with open(filename, 'r', encoding='utf-8') as file:
lines = file.readlines()
toc_index = get_toc_index(lines)
if toc_index is None:
return None # no TOC heading
toc_items = list(get_toc_items(lines, toc_index))
if not toc_items:
return False # no content headings
with open(filename, 'w', encoding='utf-8') as file:
for i, line in enumerate(lines):
if i == toc_index:
file.write(toc_string(toc_items))
else:
file.write(line)
return True # OK
def main():
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} file.md', file=sys.stderr)
sys.exit(1)
for filename in sys.argv[1:]:
print(filename)
result = add_toc(filename)
if result is None:
print('Warning: No "## TOC" heading found', file=sys.stderr)
elif result is False:
print('Warning: No content headings found', file=sys.stderr)
else:
print('OK')
if __name__ == '__main__':
main()

106
tools/unique.py Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python unique.py [-f|--flip] [-x|--cross] image.png
Erase duplicate tiles from an input image.
-f or --flip counts X/Y mirrored tiles as duplicates.
-x or --cross erases with a cross instead of a blank tile.
"""
import sys
import png
def rgb5_pixels(row):
yield from (tuple(c // 8 for c in row[x:x+3]) for x in range(0, len(row), 4))
def rgb8_pixels(row):
yield from (c * 8 + c // 4 for pixel in row for c in pixel)
def gray_pixels(row):
yield from (pixel[0] // 10 for pixel in row)
def rows_to_tiles(rows, width, height):
assert len(rows) == height and len(rows[0]) == width
yield from (tuple(tuple(row[x:x+8]) for row in rows[y:y+8])
for y in range(0, height, 8) for x in range(0, width, 8))
def tiles_to_rows(tiles, width, height):
assert width % 8 == 0 and height % 8 == 0
width, height = width // 8, height // 8
tiles = list(tiles)
assert len(tiles) == width * height
tile_rows = (tiles[y:y+width] for y in range(0, width * height, width))
yield from ([tile[y][x] for tile in tile_row for x in range(8)]
for tile_row in tile_rows for y in range(8))
def tile_variants(tile, flip):
yield tile
if flip:
yield tile[::-1]
yield tuple(row[::-1] for row in tile)
yield tuple(row[::-1] for row in tile[::-1])
def unique_tiles(tiles, flip, cross):
if cross:
blank = [[(0, 0, 0)] * 8 for _ in range(8)]
for y in range(8):
blank[y][y] = blank[y][7 - y] = (31, 31, 31)
blank = tuple(tuple(row) for row in blank)
else:
blank = tuple(tuple([(31, 31, 31)] * 8) for _ in range(8))
seen = set()
for tile in tiles:
if any(variant in seen for variant in tile_variants(tile, flip)):
yield blank
else:
yield tile
seen.add(tile)
def is_grayscale(colors):
return (colors.issubset({(31, 31, 31), (21, 21, 21), (10, 10, 10), (0, 0, 0)}) or
colors.issubset({(31, 31, 31), (20, 20, 20), (10, 10, 10), (0, 0, 0)}))
def erase_duplicates(filename, flip, cross):
with open(filename, 'rb') as file:
width, height, rows = png.Reader(file).asRGBA8()[:3]
rows = [list(rgb5_pixels(row)) for row in rows]
if width % 8 or height % 8:
return False
tiles = unique_tiles(rows_to_tiles(rows, width, height), flip, cross)
rows = list(tiles_to_rows(tiles, width, height))
if is_grayscale({c for row in rows for c in row}):
rows = [list(gray_pixels(row)) for row in rows]
writer = png.Writer(width, height, greyscale=True, bitdepth=2, compression=9)
else:
rows = [list(rgb8_pixels(row)) for row in rows]
writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
with open(filename, 'wb') as file:
writer.write(file, rows)
return True
def main():
flip = cross = False
while True:
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} [-f|--flip] [-x|--cross] tileset.png', file=sys.stderr)
sys.exit(1)
elif sys.argv[1] in {'-f', '--flip'}:
flip = True
elif sys.argv[1] in {'-x', '--cross'}:
cross = True
elif sys.argv[1] in {'-fx', '-xf'}:
flip = cross = True
else:
break
sys.argv.pop(1)
for filename in sys.argv[1:]:
if not filename.lower().endswith('.png'):
print(f'{filename} is not a .png file!', file=sys.stderr)
elif not erase_duplicates(filename, flip, cross):
print(f'{filename} is not divisible into 8x8 tiles!', file=sys.stderr)
if __name__ == '__main__':
main()

View file

@ -1,18 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sys import stderr, exit
from subprocess import Popen, PIPE
from struct import unpack, calcsize
from enum import Enum
"""
Usage: unnamed.py [-h] [-r rootdir] [-l count] pokered.sym
class symtype(Enum):
Parse the symfile to find unnamed symbols.
"""
import sys
import argparse
import subprocess
import struct
import enum
import signal
class symtype(enum.Enum):
LOCAL = 0
IMPORT = 1
EXPORT = 2
def unpack_file(fmt, file):
size = calcsize(fmt)
return unpack(fmt, file.read(size))
def unpack_from(fmt, file):
size = struct.calcsize(fmt)
return struct.unpack(fmt, file.read(size))
def read_string(file):
buf = bytearray()
@ -20,111 +29,109 @@ def read_string(file):
b = file.read(1)
if b is None or b == b'\0':
return buf.decode()
else:
buf += b
# Fix broken pipe when using `head`
from signal import signal, SIGPIPE, SIG_DFL
signal(SIGPIPE,SIG_DFL)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
import argparse
parser = argparse.ArgumentParser(description="Parse the symfile to find unnamed symbols")
parser.add_argument('symfile', type=argparse.FileType('r'), help="the list of symbols")
parser.add_argument('-r', '--rootdir', type=str, help="scan the output files to obtain a list of files with unnamed symbols (NOTE: will rebuild objects as necessary)")
parser.add_argument('-l', '--list', type=int, default=0, help="output this many of each file's unnamed symbols (NOTE: requires -r)")
parser = argparse.ArgumentParser(description='Parse the symfile to find unnamed symbols')
parser.add_argument('symfile', type=argparse.FileType('r'),
help='the list of symbols')
parser.add_argument('-r', '--rootdir', type=str,
help='scan the output files to obtain a list of files with unnamed symbols (note: will rebuild objects as necessary)')
parser.add_argument('-l', '--list', type=int, default=0,
help="output this many of each file's unnamed symbols (note: requires -r)")
args = parser.parse_args()
# Get list of object files
objects = None
if args.rootdir:
for line in Popen(["make", "-C", args.rootdir, "-s", "-p", "DEBUG=1"],
stdout=PIPE).stdout.read().decode().split("\n"):
if line.startswith("pokered_obj := "):
objects = line[15:].strip().split()
for line in subprocess.Popen(['make', '-C', args.rootdir, '-s', '-p', 'DEBUG=1'],
stdout=subprocess.PIPE).stdout.read().decode().split('\n'):
if line.startswith('pokered_obj := '):
objects = line[19:].strip().split()
break
else:
print("Error: Object files not found!", file=stderr)
exit(1)
print('Error: Object files not found!', file=sys.stderr)
sys.exit(1)
# Scan all unnamed symbols from the symfile
symbols_total = 0
symbols = set()
for line in args.symfile:
line = line.split(";")[0].strip()
split = line.split(" ")
line = line.split(';')[0].strip()
split = line.split(' ')
if len(split) < 2:
continue
symbols_total += 1
symbol = " ".join(split[1:]).strip()
symbol = ' '.join(split[1:]).strip()
if symbol[-3:].lower() == split[0][-3:].lower():
symbols.add(symbol)
# If no object files were provided, just print what we know and exit
print("Unnamed pokered symbols: %d (%.2f%% complete)" % (len(symbols),
(symbols_total - len(symbols)) / symbols_total * 100))
unnamed_percent = 100 * (symbols_total - len(symbols)) / symbols_total
print(f'Unnamed pokered symbols: {len(symbols)} ({unnamed_percent:.2f}% complete)')
if not objects:
for sym in symbols:
print(sym)
exit()
sys.exit()
# Count the amount of symbols in each file
files = {}
file_symbols = {}
for objfile in objects:
f = open(objfile, "rb")
with open(objfile, 'rb') as file:
obj_ver = None
magic = unpack_file("4s", f)[0]
magic = unpack_from('4s', file)[0]
if magic == b'RGB6':
obj_ver = 6
elif magic == b'RGB9':
obj_ver = 10 + unpack_file("<I", f)[0]
obj_ver = 10 + unpack_from('<I', file)[0]
if obj_ver not in [6, 10, 11, 12, 13, 15, 16, 17, 18]:
print("Error: File '%s' is of an unknown format." % objfile, file=stderr)
exit(1)
print(f"Error: File '{objfile}' is of an unknown format.", file=sys.stderr)
sys.exit(1)
num_symbols = unpack_file("<I", f)[0]
unpack_file("<I", f) # skip num sections
num_symbols = unpack_from('<I', file)[0]
unpack_from('<I', file) # skip num sections
if obj_ver in [16, 17, 18]:
node_filenames = []
num_nodes = unpack_file("<I", f)[0]
num_nodes = unpack_from('<I', file)[0]
for x in range(num_nodes):
unpack_file("<II", f) # parent id, parent line no
node_type = unpack_file("<B", f)[0]
unpack_from('<II', file) # parent id, parent line no
node_type = unpack_from('<B', file)[0]
if node_type:
node_filenames.append(read_string(f))
node_filenames.append(read_string(file))
else:
node_filenames.append("rept")
depth = unpack_file("<I", f)[0]
node_filenames.append('rept')
depth = unpack_from('<I', file)[0]
for i in range(depth):
unpack_file("<I", f) # rept iterations
unpack_from('<I', file) # rept iterations
node_filenames.reverse()
for x in range(num_symbols):
sym_name = read_string(f)
sym_type = symtype(unpack_file("<B", f)[0] & 0x7f)
for _ in range(num_symbols):
sym_name = read_string(file)
sym_type = symtype(unpack_from('<B', file)[0] & 0x7f)
if sym_type == symtype.IMPORT:
continue
if obj_ver in [16, 17, 18]:
sym_fileno = unpack_file("<I", f)[0]
sym_fileno = unpack_from('<I', file)[0]
sym_filename = node_filenames[sym_fileno]
else:
sym_filename = read_string(f)
unpack_file("<III", f)
sym_filename = read_string(file)
unpack_from('<III', file)
if sym_name not in symbols:
continue
if sym_filename not in files:
files[sym_filename] = []
files[sym_filename].append(sym_name)
if sym_filename not in file_symbols:
file_symbols[sym_filename] = []
file_symbols[sym_filename].append(sym_name)
# Sort the files, the one with the most amount of symbols first
files = sorted(((f, files[f]) for f in files), key=lambda x: len(x[1]), reverse=True)
for f in files:
filename, unnamed = f
sym_list = ', '.join(unnamed[:args.list])
print("%s: %d%s" % (filename, len(unnamed), ': ' + sym_list if sym_list else ''))
file_symbols = sorted(file_symbols.items(), key=lambda item: len(item[1]), reverse=True)
for filename, unnamed_syms in file_symbols:
sym_list = ', '.join(unnamed_syms[:args.list])
print(f'{filename}: {len(unnamed_syms)}{": " + sym_list if sym_list else ""}')

64
tools/used_space.py Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Usage: python used_space.py [pokered.map] [used_space.png]
Generate a PNG visualizing the space used by each bank in the ROM.
"""
import sys
from colorsys import hls_to_rgb
import png
from mapreader import MapReader
def main():
mapfile = sys.argv[1] if len(sys.argv) >= 2 else 'pokered.map'
outfile = sys.argv[2] if len(sys.argv) >= 3 else 'used_space.png'
num_banks = 0x80
bank_mask = 0x3FFF
bank_size = 0x4000 # bytes
bpp = 8 # bytes per pixel
height = 256 # pixels
assert bank_size % bpp == 0 and (bank_size // bpp) % height == 0
pixels_per_bank = bank_size // bpp # 2048 pixels
bank_width = pixels_per_bank // height # 8 pixels
width = bank_width * num_banks # 1024 pixels
reader = MapReader()
with open(mapfile, 'r', encoding='utf-8') as file:
reader.read_map_data(file.readlines())
hit_data = []
default_bank_data = {'sections': [], 'used': 0, 'slack': bank_size}
for bank in range(num_banks):
hits = [0] * pixels_per_bank
bank_data = reader.bank_data['ROM0 bank' if bank == 0 else 'ROMX bank']
data = bank_data.get(bank, default_bank_data)
for s in data['sections']:
beg = s['beg'] & bank_mask
end = s['end'] & bank_mask
for i in range(beg, end + 1):
hits[i // bpp] += 1
hit_data.append(hits)
pixels = [[(0xFF, 0xFF, 0xFF)] * width for _ in range(height)]
for bank, hits in enumerate(hit_data):
hue = 0 if bank == 0 else 210 if bank % 2 else 270
for i, hit in enumerate(hits):
x, y = i % bank_width + bank * bank_width, i // bank_width
hls = (hue / 360, 1 - (85 * hit / bpp) / 100, 1)
rgb = tuple(int(c * 0xFF) for c in hls_to_rgb(*hls))
pixels[y][x] = rgb
png_data = [tuple(c for pixel in row for c in pixel) for row in pixels]
with open(outfile, 'wb') as file:
writer = png.Writer(width, height, greyscale=False, bitdepth=8, compression=9)
writer.write(file, png_data)
if __name__ == '__main__':
main()