From cb93c351498683ff06d92926661696f871a65262 Mon Sep 17 00:00:00 2001 From: Dabomstew <4035468+Dabomstew@users.noreply.github.com> Date: Mon, 8 Jun 2020 14:23:55 +1200 Subject: [PATCH] Add free space utility from Crystal, build red-speedchoice rom --- .gitignore | 3 + INSTALL.md | 6 +- Makefile | 18 +-- pokered.link => red-speedchoice.link | 0 tools/free_space.py | 73 +++++++++++ tools/mapreader.py | 173 +++++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 12 deletions(-) rename pokered.link => red-speedchoice.link (100%) create mode 100755 tools/free_space.py create mode 100755 tools/mapreader.py diff --git a/.gitignore b/.gitignore index 60fba4cd..fd20806b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ pokered.ini # rom patches *.ips + +# map file for red-speedchoice specifically, other map files can be data files +red-speedchoice.map diff --git a/INSTALL.md b/INSTALL.md index 0cfb71a4..3f6dc82e 100755 --- a/INSTALL.md +++ b/INSTALL.md @@ -11,7 +11,7 @@ git clone https://github.com/Dabomstew/pokered-speedchoice cd pokered-speedchoice -To build **pokered.gbc**: +To build **red-speedchoice.gbc**: make red make @@ -31,7 +31,7 @@ Then in **Terminal**, run: git clone https://github.com/Dabomstew/pokered-speedchoice cd pokered-speedchoice -To build **pokered.gbc**: +To build **red-speedchoice.gbc**: make red make @@ -56,7 +56,7 @@ In the **Cygwin terminal**, enter these commands: git clone https://github.com/Dabomstew/pokered-speedchoice cd pokered-speedchoice -To build **pokered.gbc**: +To build **red-speedchoice.gbc**: make red make diff --git a/Makefile b/Makefile index 8985e2c1..390da85f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -roms := pokered.gbc +roms := red-speedchoice.gbc -pokered_obj := audio_red.o main_red.o pics_red.o text_red.o wram_red.o +red-speedchoice_obj := audio_red.o main_red.o pics_red.o text_red.o wram_red.o ### Build tools @@ -24,21 +24,21 @@ RGBLINK ?= $(RGBDS)rgblink .PHONY: all red clean tidy compare tools config all: $(roms) config -red: pokered.gbc +red: red-speedchoice.gbc -config: pokered.ini +config: red-speedchoice.ini # For contributors to make sure a change didn't affect the contents of the rom. compare: $(roms) @$(MD5) roms.md5 clean: - rm -f $(roms) $(pokered_obj) $(roms:.gbc=.sym) + rm -f $(roms) $(red-speedchoice_obj) $(roms:.gbc=.sym) find . \( -iname '*.1bpp' -o -iname '*.2bpp' -o -iname '*.pic' -o -iname '*.lz' \) -exec rm {} + $(MAKE) clean -C tools/ tidy: - rm -f $(roms) $(pokered_obj) $(roms:.gbc=.sym) + rm -f $(roms) $(red-speedchoice_obj) $(roms:.gbc=.sym) $(MAKE) clean -C tools/ tools: @@ -57,13 +57,13 @@ $(shell echo "db \"-"$(shell git log -1 --format="%h")"\"" > git-revision.asm) %.asm: ; %_red.o: dep = $(shell tools/scan_includes $(@D)/$*.asm) -$(pokered_obj): %_red.o: %.asm $$(dep) +$(red-speedchoice_obj): %_red.o: %.asm $$(dep) $(RGBASM) -D _RED -h -o $@ $*.asm -pokered_opt = -cjsv -k 01 -l 0x33 -m 0x13 -p 0 -r 03 -t "RED_SPDC" -i KAPC +red-speedchoice_opt = -cjsv -k 01 -l 0x33 -m 0x13 -p 0 -r 03 -t "RED_SPDC" -i KAPC %.gbc: $$(%_obj) - $(RGBLINK) -n $*.sym -l pokered.link -o $@ $^ + $(RGBLINK) -n $*.sym -l red-speedchoice.link -m red-speedchoice.map -o $@ $^ $(RGBFIX) $($*_opt) $@ sort $*.sym -o $*.sym diff --git a/pokered.link b/red-speedchoice.link similarity index 100% rename from pokered.link rename to red-speedchoice.link diff --git a/tools/free_space.py b/tools/free_space.py new file mode 100755 index 00000000..903f399e --- /dev/null +++ b/tools/free_space.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Usage: python3 free_space.py [BANK=none] [red-speedchoice.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' + mapfile = 'red-speedchoice.map' + + if len(sys.argv) >= 2 and sys.argv[1].startswith('BANK='): + print_bank = sys.argv[1].split('=', 1)[-1] + if len(sys.argv) >= 3: + mapfile = sys.argv[2] + elif len(sys.argv) >= 3 and sys.argv[2].startswith('BANK='): + print_bank = sys.argv[2].split('=', 1)[-1] + mapfile = sys.argv[1] + + if print_bank not in {'all', 'none'}: + try: + if print_bank.startswith('0x') or print_bank.startswith('0X'): + print_bank = int(print_bank[2:], 16) + else: + print_bank = int(print_bank) + except ValueError: + error = 'Error: invalid BANK: %s' % print_bank + if print_bank.isalnum(): + error += ' (did you mean: 0x%s?)' % print_bank + print(error, file=sys.stderr) + sys.exit(1) + + num_banks = 0x40 + bank_size = 0x4000 # bytes + total_size = num_banks * bank_size + + r = MapReader() + with open(mapfile, 'r', encoding='utf-8') as f: + l = f.readlines() + r.read_map_data(l) + + free_space = 0 + per_bank = [] + default_bank_data = {'sections': [], 'used': 0, 'slack': bank_size} + for bank in range(num_banks): + bank_data = r.bank_data['ROM0 bank'] if bank == 0 else r.bank_data['ROMX bank'] + data = bank_data.get(bank, default_bank_data) + used = data['used'] + slack = data['slack'] + per_bank.append((used, slack)) + free_space += slack + + print('Free space: %d/%d (%.2f%%)' % (free_space, total_size, free_space * 100.0 / total_size)) + 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('$%02X, %d, %d' % (bank, used, slack)) + +if __name__ == '__main__': + main() diff --git a/tools/mapreader.py b/tools/mapreader.py new file mode 100755 index 00000000..de2ec342 --- /dev/null +++ b/tools/mapreader.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +# A library for parsing the pokecrystal.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'])