From e4ff1b4c5a880761f687700a988dbc5d8907b292 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Sun, 30 Jul 2023 08:52:46 +0000 Subject: [PATCH] generate/text: added Of the "trivial" texts that the current parser can handle, these are now being inserted in obj_events. --- Makefile | 2 + control.hpp | 16 ++-- tools/generate/files.py | 6 ++ tools/generate/generate.py | 4 +- tools/generate/map_objects.py | 17 ++-- tools/generate/maps.py | 8 +- tools/generate/text.py | 145 ++++++++++++++++++++++++++++++++ tools/generate/text_pointers.py | 90 ++++++++++++++++++++ tools/parse/line.py | 2 +- tools/parse/parse.py | 5 ++ tools/parse/text.py | 31 ++++--- 11 files changed, 300 insertions(+), 26 deletions(-) create mode 100644 tools/generate/text.py create mode 100644 tools/generate/text_pointers.py diff --git a/Makefile b/Makefile index f03780b..d6f72cd 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ GEN_SRC += gen/map_objects.cpp GEN_SRC += gen/sprites.cpp GEN_SRC += gen/tilesets.cpp GEN_SRC += gen/collision_tile_ids.cpp +GEN_SRC += gen/text.cpp +GEN_SRC += gen/text_pointers.cpp SRC = SRC += $(GEN_SRC) diff --git a/control.hpp b/control.hpp index c545fa0..6915880 100644 --- a/control.hpp +++ b/control.hpp @@ -3,13 +3,14 @@ #include struct control_t { - static constexpr uint8_t next = 0x80; - static constexpr uint8_t line = 0x81; - static constexpr uint8_t para = 0x82; - static constexpr uint8_t cont = 0x83; - static constexpr uint8_t done = 0x84; - static constexpr uint8_t prompt = 0x85; - static constexpr uint8_t page = 0x86; + static constexpr uint8_t text = 0x80; + static constexpr uint8_t next = 0x81; + static constexpr uint8_t line = 0x82; + static constexpr uint8_t para = 0x83; + static constexpr uint8_t cont = 0x84; + static constexpr uint8_t done = 0x85; + static constexpr uint8_t prompt = 0x86; + static constexpr uint8_t page = 0x87; }; struct ligatures_t { @@ -24,6 +25,7 @@ struct ligatures_t { struct extended_t { static constexpr uint8_t jpy = 0xa5; // ¥ + static constexpr uint8_t colon_sm = 0xa6; // (small ':' symbol) static constexpr uint8_t e = 0xe9; // é static constexpr uint8_t ellipsis = 0xa8; // … static constexpr uint8_t pk = 0xb2; // ᴾₖ diff --git a/tools/generate/files.py b/tools/generate/files.py index be5ab25..54bf1a3 100644 --- a/tools/generate/files.py +++ b/tools/generate/files.py @@ -3,6 +3,8 @@ from generate import map_objects from generate import sprites from generate import tilesets from generate import collision_tile_ids +from generate import text +from generate import text_pointers files = [ (maps.generate_header, "maps.hpp"), @@ -15,4 +17,8 @@ files = [ (sprites.generate_source, "sprites.cpp"), (collision_tile_ids.generate_header, "collision_tile_ids.hpp"), (collision_tile_ids.generate_source, "collision_tile_ids.cpp"), + (text.generate_header, "text.hpp"), + (text.generate_source, "text.cpp"), + (text_pointers.generate_header, "text_pointers.hpp"), + (text_pointers.generate_source, "text_pointers.cpp"), ] diff --git a/tools/generate/generate.py b/tools/generate/generate.py index 3bc5554..7f3c99e 100644 --- a/tools/generate/generate.py +++ b/tools/generate/generate.py @@ -4,13 +4,13 @@ def _render(out, lines): indent = " " level = 0 for l in lines: - if l and l[0] == "}": + if l and (l[0] == "}" or l[0] == ")"): level -= 2 assert level >= 0, out.getvalue() out.write(indent * level + l + "\n") - if l and l[-1] == "{": + if l and (l[-1] == "{" or l[-1] == "("): level += 2 if level == 0 and l and l[-1] == ";": diff --git a/tools/generate/map_objects.py b/tools/generate/map_objects.py index 09f1ced..2431c73 100644 --- a/tools/generate/map_objects.py +++ b/tools/generate/map_objects.py @@ -3,6 +3,7 @@ from parse import parse from generate.generate import renderer from generate.maps import sorted_map_headers from generate.sprites import sprite_name +from generate.text_pointers import sorted_text_pointers def warp_event(ev): x, y = ev.position @@ -31,8 +32,9 @@ def range_or_direction(ev): direction = 'any_dir' # hack? yield f".range_or_direction = object_event_t::range_or_direction::{direction}," -def object_event(ev): +def object_event(ev, sorted_text_constants): x, y = ev.position + text_id = '0xff' if ev.text_id not in sorted_text_constants else ev.text_id return [ "{", f".type = object_event_t::type::{ev.type},", @@ -40,7 +42,7 @@ def object_event(ev): f".sprite_id = spritesheet_t::{sprite_name(ev.sprite_id)},", f".movement = object_event_t::movement::{ev.movement.lower()},", *(range_or_direction(ev)), - ".text_id = 0,", # fixme + f".text_id = {text_id},", # fixme ".trainer = { 0 },", # fixme "},", ] @@ -57,10 +59,10 @@ def bg_events(map_name, obj): yield from bg_event(ev) yield "};" -def object_events(map_name, obj): +def object_events(map_name, obj, sorted_text_constants): yield f"const object_event_t {map_name}_object_events[] = {{" for ev in obj.object_events: - yield from object_event(ev) + yield from object_event(ev, sorted_text_constants) yield "};" def object(map_name, obj): @@ -80,17 +82,22 @@ def includes_source(): yield '#include ' yield "" yield '#include "../map_objects.hpp"' + yield '#include "text_pointers.hpp"' yield '#include "maps.hpp"' yield "" def map_objects(): map_headers = list(sorted_map_headers()) + scripts_list = parse.scripts_list() for map_header in map_headers: map_name = map_header.name2.lower() map_objects = parse.map_objects_list()[map_header.object()] + # fixme: hack due to viridanmart + text_pointers = scripts_list.get(map_header.text_pointers(), ({}, {})) + sorted_text_constants = {k for k, _ in sorted_text_pointers(*text_pointers)} yield from warp_events(map_name, map_objects) yield from bg_events(map_name, map_objects) - yield from object_events(map_name, map_objects) + yield from object_events(map_name, map_objects, sorted_text_constants) yield "const object_t map_objects[] = {" for map_header in map_headers: diff --git a/tools/generate/maps.py b/tools/generate/maps.py index 61dac45..e86e7b4 100644 --- a/tools/generate/maps.py +++ b/tools/generate/maps.py @@ -64,6 +64,7 @@ def struct_map_t(): *struct_connection_t(), "", "start_size_t blocks;", + "start_size_t text_pointers;", "uint32_t width;", "uint32_t height;", "connection_t connections[4];", @@ -104,14 +105,19 @@ def connections(map_header): yield f".map = map_t::{connection.map_name2.lower()}," yield f".offset = {connection.offset}," yield "}," - + def map(map_header): + + block_path = parse.maps_blocks_list()[map_header.blocks()] map_constant = parse.map_constants_list()[map_header.name2] return [ f"[map_t::{map_header.name2.lower()}] = {{", ".blocks = {", *start_size_value(block_path), + "},", + ".text_pointers = {", + "},", f".width = {map_constant.width},", f".height = {map_constant.height},", diff --git a/tools/generate/text.py b/tools/generate/text.py new file mode 100644 index 0000000..bbc7611 --- /dev/null +++ b/tools/generate/text.py @@ -0,0 +1,145 @@ +""" +const uint8_t string[] = "foo" "\x0a" "abc"; +""" + +from itertools import chain +from parse import parse +from generate.generate import renderer + +_control_encode = { + 'text_end': None, + 'text_ram': None, + 'text_start': None, + 'text_decimal': None, + 'text_bcd': None, + 'text' : 0x80, + 'next' : 0x81, + 'line' : 0x82, + 'para' : 0x83, + 'cont' : 0x84, + 'done' : 0x85, + 'prompt' : 0x86, + 'page' : 0x87, +} + +_bracketed_encode = { + 'PKMN' : '\\x96', + 'COLON' : '\\xa6', + # fixme hacks + 'PLAYER': 'PLAYER', + 'RIVAL' : 'RIVAL', + 'TARGET': 'TARGET', + 'USER' : 'USER', + 'SCROLL': 'SCROLL', # _ContTextNoPause + '_CONT' : '_CONT', # _ContText +} + +def encode_bracketed(bracketed): + # see control.hpp + assert bracketed in _bracketed_encode, bracketed + return _bracketed_encode[bracketed] + +def parse_bracketed(string): + chars = [] + in_bracket = False + for c in string: + if c == '<': + assert in_bracket is False + in_bracket = True + if chars: + yield ''.join(chars) + chars = [] + elif c == '>': + assert in_bracket is True + in_bracket = False + yield encode_bracketed(''.join(chars)) + chars = [] + elif ord(c) == 165: + if chars: + yield ''.join(chars) + chars = [] + yield '\\xa5' + else: + chars.append(c) + if chars: + yield ''.join(chars) + +def encode_control(control): + assert control in _control_encode, control + encode = _control_encode[control] + if encode is not None: + return f'\\x{encode:x}' + else: + return None + +def quote(s): + return f'"{s}"' + +def generate_literals(text): + for control__value in text: + if len(control__value) == 1: + control, = control__value + ec = encode_control(control) + if ec is not None: + yield ec + elif len(control__value) == 2: + control, value = control__value + ec = encode_control(control) + if ec is None: + continue + yield ec + yield from parse_bracketed(value) + else: + assert False, control__value + +def generate_text(name, text): + yield f"const uint8_t {name}[] = (" + for literal in generate_literals(text): + yield quote(literal) + yield ");" + +def calculate_length(text): + return sum( + 1 if literal[0:2] == '\\x' else len(literal) + for literal in generate_literals(text) + ) + +def texts_header(): + text_list = chain.from_iterable( + d.items() for d in parse.text_list()) + + for name, text in text_list: + length = calculate_length(text) + 1 + yield f"extern const uint8_t {name}[{length}];" + +def includes_header(): + yield "#pragma once" + yield "" + yield "#include " + yield "" + +def texts_source(): + text_list = chain.from_iterable( + d.items() for d in parse.text_list()) + + for name, text in text_list: + yield from generate_text(name, text) + yield "" + +def generate_header(): + render, out = renderer() + render(includes_header()); + render(texts_header()) + return out + +def includes_source(): + yield "#include " + yield "" + yield '#include "text.hpp"' + yield "" + +def generate_source(): + render, out = renderer() + render(includes_source()); + render(texts_source()) + return out diff --git a/tools/generate/text_pointers.py b/tools/generate/text_pointers.py new file mode 100644 index 0000000..15234fe --- /dev/null +++ b/tools/generate/text_pointers.py @@ -0,0 +1,90 @@ +from operator import itemgetter +from parse import parse +from generate.generate import renderer + +""" +parse.scripts_list() == { + 'PalletTown_TextPointers': + ({'TEXT_PALLETTOWN_FISHER': 'PalletTownFisherText', + 'TEXT_PALLETTOWN_GIRL': 'PalletTownGirlText', + 'TEXT_PALLETTOWN_OAK': 'PalletTownOakText', + 'TEXT_PALLETTOWN_OAKSLAB_SIGN': 'PalletTownOaksLabSignText', + 'TEXT_PALLETTOWN_PLAYERSHOUSE_SIGN': 'PalletTownPlayersHouseSignText', + 'TEXT_PALLETTOWN_RIVALSHOUSE_SIGN': 'PalletTownRivalsHouseSignText', + 'TEXT_PALLETTOWN_SIGN': 'PalletTownSignText'}, + {'PalletTownFisherText': '_PalletTownFisherText', + 'PalletTownGirlText': '_PalletTownGirlText', + 'PalletTownOaksLabSignText': '_PalletTownOaksLabSignText', + 'PalletTownPlayersHouseSignText': '_PalletTownPlayersHouseSignText', + 'PalletTownRivalsHouseSignText': '_PalletTownRivalsHouseSignText', + 'PalletTownSignText': '_PalletTownSignText'}), +} +""" + +def labels_by_constant(constants, labels): + # fixme: removes constants with no (parsed) label + for text_name, label_name in constants.items(): + if label_name in labels: + yield text_name, labels[label_name] + +def sorted_text_pointers(constants, labels): + return sorted(labels_by_constant(constants, labels), key=itemgetter(0)) + +def text_constant_enum(name, constants, labels): + yield f"enum {name} {{" + for i, (k, _) in enumerate(sorted_text_pointers(constants, labels)): + yield f"{k} = {i}," + yield "};" + +def header_includes(): + yield "#pragma once" + yield "" + +def text_constant_enums(): + map_text_pointers = parse.scripts_list() + for name, (constants, labels) in map_text_pointers.items(): + yield from text_constant_enum(name, constants, labels) + +def extern_text_pointer(name, constants, labels): + pointers = list(sorted_text_pointers(constants, labels)) + yield f"extern const start_size_t {name}[{len(pointers)}];" + +def extern_text_pointers(): + map_text_pointers = parse.scripts_list() + for name, (constants, labels) in map_text_pointers.items(): + yield from extern_text_pointer(name, constants, labels) + +def generate_header(): + render, out = renderer() + render(header_includes()) + render(text_constant_enums()) + render(extern_text_pointers()) + return out + +def text_pointer(name, constants, labels): + yield f"const start_size_t {name}[] = {{" + pointers = list(sorted_text_pointers(constants, labels)) + for constant, label in pointers: + yield f"[{constant}] = {{" + yield f".start = &{label}[0]," + yield f".size = (sizeof ({label}))," + yield "}," + yield "};" + +def text_pointers(): + map_text_pointers = parse.scripts_list() + for name, (constants, labels) in map_text_pointers.items(): + yield from text_pointer(name, constants, labels) + +def source_includes(): + yield '#include "../start_size.hpp"' + yield "" + yield '#include "text_pointers.hpp"' + yield '#include "text.hpp"' + yield "" + +def generate_source(): + render, out = renderer() + render(source_includes()) + render(text_pointers()) + return out diff --git a/tools/parse/line.py b/tools/parse/line.py index 72f0cf5..90ba743 100644 --- a/tools/parse/line.py +++ b/tools/parse/line.py @@ -1,6 +1,6 @@ def skip_whitespace(lines): i = 0 - while lines[i:] and not lines[i].strip(): + while lines[i:] and (not lines[i].strip() or lines[i][0] == ';'): i += 1 return lines[i:] diff --git a/tools/parse/parse.py b/tools/parse/parse.py index d66be40..2f3682f 100644 --- a/tools/parse/parse.py +++ b/tools/parse/parse.py @@ -16,6 +16,9 @@ from parse import gfx_sprites from parse import spritesheets from parse import sprite_constants +from parse import text +from parse import scripts + prefix = Path(sys.argv[1]) def memoize(f): @@ -51,3 +54,5 @@ spritesheets_list = memoize(lambda: spritesheets.parse(prefix)) sprite_constants_list = memoize(lambda: sprite_constants.parse(prefix)) # text +scripts_list = memoize(lambda: scripts.parse_all(prefix)) +text_list = memoize(lambda: text.parse_all(prefix)) diff --git a/tools/parse/text.py b/tools/parse/text.py index 2dab022..2ae4747 100644 --- a/tools/parse/text.py +++ b/tools/parse/text.py @@ -1,3 +1,5 @@ +from itertools import chain + from parse.line import next_line, skip_whitespace def parse_label(lines): @@ -6,7 +8,7 @@ def parse_label(lines): name = line.removesuffix('::') return lines, name -string_tokens = {"text", "cont", "para", "line"} +string_tokens = {"text", "cont", "para", "line", "next"} def parse_string(line): line = line.strip() @@ -42,18 +44,29 @@ def parse_body(lines): value, = rest body.append((type, parse_args(value))) else: + # hack hack; some texts don't have a control word at the end + # _MoveNameText + if line.endswith('::'): + return [line] + lines, body assert False, line return lines, body def tokenize_text(lines): lines, name = parse_label(lines) + # fixme: hack + if name == '_CableClubNPCLinkClosedBecauseOfInactivityText': + return None lines, body = parse_body(lines) return lines, (name, body) def tokenize(lines): while lines: - lines, tokens = tokenize_text(lines) + lines__tokens = tokenize_text(lines) + if lines__tokens is None: + # fixme: hack9000 + return + lines, tokens = lines__tokens lines = skip_whitespace(lines) yield tokens @@ -65,11 +78,9 @@ def parse(path): return d def parse_all(prefix): - base_path = prefix / 'text' - paths = [p for p in base_path.iterdir() if p.is_file()] - return [parse(path) for path in paths] - -import sys -from pprint import pprint -from pathlib import Path -pprint(parse_all(Path(sys.argv[1]))) + base_path0 = prefix / 'text' + paths0 = [p for p in base_path0.iterdir() if p.is_file()] + base_path1 = prefix / 'data/text' + paths1 = [p for p in base_path1.iterdir() + if p.is_file() and p.stem.startswith('text_')] + return [parse(path) for path in chain(paths0, paths1)]