Enhance sotn_str & Encode Menu Strings (#1320)

`sotn_str` can now handle escaped quotes, right parenthesis, and more in
in `_S` strings. This allows all of the strings in `dra/menu.c` to be
encoded as static C-strings.

This also converts the spell button sequences to UTF-8.

---------

Co-authored-by: Jonathan Hohle <jon@ttkb.co>
This commit is contained in:
Jonathan Hohle 2024-06-20 13:49:16 -07:00 committed by GitHub
parent d45701638f
commit 078ed30c06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 109 deletions

View File

@ -444,34 +444,34 @@ const char* g_MenuStr[] = {
};
SpellDef g_SpellDefs[] = {
{"Dark Metamorphosis", "\xe0\xe1\xe2\xe3\xe4\xFF", "Heal HP by shedding blood", 10, 0, 0, 0, 1, 0, 0, 0, 0},
{"Summon Spirit", "\xe0\xe4\xe2\xe6\xFF", "Summons a fierce spirit", 5, 20, 8, 2, 1, 0, 0, 15, 0},
{"Hellfire", "\xe2\xe6\xe5\xe4\xFF", "Transport and fireball attack", 15, 20, 8, 2, 1, 0, 32768, 30, 0},
{"Tetra Spirit", "\xe2\xc0\xd2\xe3\xe4\xe5\xe6\xFF", "Summons four spirits", 20, 20, 8, 2, 1, 0, 0, 15, 0},
{"Wolf Charge", "\xe6\xe5\xe4\xFF", "Powerful attack by Wolf", 10, 40, 10, 2, 129, 64, 64, 30, 0},
{"Soul Steal", "\xe0\xe4\xe5\xe6\xe7\xe0\xe4\xFF", "Steals HP from nearby enemies", 50, 10, 8, 130, 2, 0, 0, 15, 0},
{"Wing Smash", "\xe2\xe1\xe0\xe7\xe6\xe5\xe4\xFF", "Powerful attack by Bat", 8, 10, 10, 2, 1, 16, 0, 50, 0},
{"Sword Brothers", "\xe6\xe5\xe4\xe3\xe2\xc0\xd2\xe6\xFF", "Summons Sword Brothers", 30, 20, 8, 130, 129, 0, 64, 20, 0},
{"", "\xFF", "", 0, 0, 0, 0, 0, 0, 0, 0, 0},
{"", "\xFF", "", 8, 10, 10, 2, 1, 0, 32768, 10, 0},
{"", "\xFF", "", 0, 20, 20, 2, 1, 0, 0, 10, 0},
{"", "\xFF", "", 0, 20, 20, 2, 129, 64, 64, 30, 0},
{"", "\xFF", "", 0, 20, 20, 2, 1, 0, 128, 20, 0},
{"", "\xFF", "", 5, 10, 10, 2, 1, 16, 0, 50, 0},
{"", "\xFF", "", 0, 40, 10, 2, 129, 16, 64, 10, 0},
{"", "\xFF", "", 0, 10, 4, 2, 1, 0, 0, 5, 0},
{"", "\xFF", "", 0, 10, 10, 2, 1, 0, 32768, 10, 0},
{"", "\xFF", "", 0, 2, 4, 2, 1, 0, 0, 5, 0},
{"", "\xFF", "", 0, 2, 4, 2, 2, 0, 0, 5, 0},
{"", "\xFF", "", 0, 20, 4, 2, 129, 0, 64, 5, 0},
{"", "\xFF", "", 0, 20, 4, 2, 129, 0, 64, 10, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 64, 5, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 64, 10, 0},
{"", "\xFF", "", 0, 4, 8, 2, 129, 0, 64, 5, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 32832, 30, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 8256, 30, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 16448, 30, 0},
{"", "\xFF", "", 0, 20, 8, 2, 129, 0, 16448, 45, 0},
{"Dark Metamorphosis", _S("←↖↑↗→"), "Heal HP by shedding blood", 10, 0, 0, 0, 1, 0, 0, 0, 0},
{"Summon Spirit", _S("←→↑↓"), "Summons a fierce spirit", 5, 20, 8, 2, 1, 0, 0, 15, 0},
{"Hellfire", _S("↑↓↘→"), "Transport and fireball attack", 15, 20, 8, 2, 1, 0, 32768, 30, 0},
{"Tetra Spirit", _S("↑ため↗→↘↓"), "Summons four spirits", 20, 20, 8, 2, 1, 0, 0, 15, 0},
{"Wolf Charge", _S("↓↘→"), "Powerful attack by Wolf", 10, 40, 10, 2, 129, 64, 64, 30, 0},
{"Soul Steal", _S("←→↘↓↙←→"), "Steals HP from nearby enemies", 50, 10, 8, 130, 2, 0, 0, 15, 0},
{"Wing Smash", _S("↑↖←↙↓↘→"), "Powerful attack by Bat", 8, 10, 10, 2, 1, 16, 0, 50, 0},
{"Sword Brothers", _S("↓↘→↗↑ため↓"), "Summons Sword Brothers", 30, 20, 8, 130, 129, 0, 64, 20, 0},
{"", _S(""), "", 0, 0, 0, 0, 0, 0, 0, 0, 0},
{"", _S(""), "", 8, 10, 10, 2, 1, 0, 32768, 10, 0},
{"", _S(""), "", 0, 20, 20, 2, 1, 0, 0, 10, 0},
{"", _S(""), "", 0, 20, 20, 2, 129, 64, 64, 30, 0},
{"", _S(""), "", 0, 20, 20, 2, 1, 0, 128, 20, 0},
{"", _S(""), "", 5, 10, 10, 2, 1, 16, 0, 50, 0},
{"", _S(""), "", 0, 40, 10, 2, 129, 16, 64, 10, 0},
{"", _S(""), "", 0, 10, 4, 2, 1, 0, 0, 5, 0},
{"", _S(""), "", 0, 10, 10, 2, 1, 0, 32768, 10, 0},
{"", _S(""), "", 0, 2, 4, 2, 1, 0, 0, 5, 0},
{"", _S(""), "", 0, 2, 4, 2, 2, 0, 0, 5, 0},
{"", _S(""), "", 0, 20, 4, 2, 129, 0, 64, 5, 0},
{"", _S(""), "", 0, 20, 4, 2, 129, 0, 64, 10, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 64, 5, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 64, 10, 0},
{"", _S(""), "", 0, 4, 8, 2, 129, 0, 64, 5, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 32832, 30, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 8256, 30, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 16448, 30, 0},
{"", _S(""), "", 0, 20, 8, 2, 129, 0, 16448, 45, 0},
};
RelicDesc g_RelicDefs[] = {

View File

@ -46,62 +46,66 @@ const char* D_800A2D48[] = {
#endif
};
#ifndef SOTN_STR
// Similar to `_S`, a second lookup table is used for some menus based on BIOS
// fonts in a custom table. This is only relevant to the strings below.
// e.g. "\x02\x03\x04\xFF" -> _S2("")
#define _S2(x) (x)
// Like `_S2`, but uses a lookup table unique to the HD version of the game
#define _S2_HD(x) (x)
#endif
#if defined(VERSION_US)
const char* D_800A2D64[] = {
static const char* D_800A2D64[] = {
"",
};
static const char* D_800A2D68[] = {
_S2(""),
_S2(""),
};
#elif defined(VERSION_HD)
const char* D_800A2D10[] = {
static const char* D_800A2D10[] = {
"装備技システム短剣必殺使攻撃力防",
};
const char* D_800A2D14[] = {
static const char* D_800A2D14[] = {
"御魔導器拳こ一覧棒両手食物爆弾盾",
};
const char* D_800A2D18[] = {
static const char* D_800A2D18[] = {
"投射薬ん右左武兜鎧マントその他い",
};
#endif
#if defined(VERSION_US)
const char* D_800A2D68[] = {
"\x00\x01\x01\xFF",
"\x02\x03\x04\xFF",
};
#elif defined(VERSION_HD)
extern const char* D_800A2D68[];
#endif
#if defined(VERSION_HD)
const char* D_800A2D68[] = {
"\x0C\x0D\x0E\xFF", //
"\x0F\x10\x0E\xFF", // D_800A83AC
"\x09\x0A\x02\x16\x17\xFF", // D_800A2D24
"\x00\x01\xFF", // g_MenuStr[93]
"\x09\x0A\x02\xFF", // g_MenuStr[96]
"\x11\x12\x13\xFF", // g_MenuStr[92]
"\x03\x04\x05\x06\xFF", // g_MenuStr[94]
"\x07\x08\xFF", // g_MenuStr[99]
"\x08\xFF", //
"\x20\x08\xFF", //
"\x14\xFF", //
"\x15\x23\x18\xFF", //
"\x19\x1A\x08\xFF", //
"\x1B\x1C\xFF", //
"\x1D\x1E\xFF", //
"\x20\x21\xFF", //
"\x1F\xFF", //
"\x22\xFF", //
"\x24\x1A\x26\x13\xFF", //
"\x25\x1A\x26\x13\xFF", //
"\x27\xFF", //
"\x28\xFF", //
"\x29\x2A\x2B\xFF", //
"\x2C\x2D\x2E\xFF", //
"\x2C\x2D\x2E\xFF", //
"\x0B\x2F\x11\xFF", // g_MenuStr[95]
"\x0B\x2F\x11\x16\x17\xFF", // D_800A2D84
static const char* D_800A2D68[] = {
_S2_HD("攻撃力"), //
_S2_HD("防御力"), // D_800A83AC
_S2_HD("必殺技一覧"), // D_800A2D24
_S2_HD("装備"), // g_MenuStr[93]
_S2_HD("必殺技"), // g_MenuStr[96]
_S2_HD("魔導器"), // g_MenuStr[92]
_S2_HD("システム"), // g_MenuStr[94]
_S2_HD("短剣"), // g_MenuStr[99]
_S2_HD(""), //
_S2_HD("投剣"), //
_S2_HD(""), //
_S2_HD("こん棒"), //
_S2_HD("両手剣"), //
_S2_HD("食物"), //
_S2_HD("爆弾"), //
_S2_HD("投射"), //
_S2_HD(""), //
_S2_HD(""), //
_S2_HD("右手武器"), //
_S2_HD("左手武器"), //
_S2_HD(""), //
_S2_HD(""), //
_S2_HD("マント"), //
_S2_HD("その他"), //
_S2_HD("その他"), //
_S2_HD("使い魔"), // g_MenuStr[95]
_S2_HD("使い魔一覧"), // D_800A2D84
};
#endif

View File

@ -124,6 +124,16 @@ def get_chr(chr):
return table[chr]
alt_utf8_to_index = dict((value, index) for index, value in enumerate(""))
alt_hd_utf8_to_index = dict((value, index) for index, value in enumerate("".join([
#0 1 2 3 4 5 6 7 8 9 A B C D E F
"装備技システム短剣必殺使攻撃力防",
"御魔導器拳こ一覧棒両手食物爆弾盾",
"投射薬ん右左武兜鎧マントその他い",
])))
def convert_j(f):
pos = 0
str = ""
@ -259,9 +269,8 @@ def dakuten_to_bytes(input_chr):
def utf8_to_byte_literals(input_str):
clean_str = input_str.replace("_SJ(", "").replace(")", "")
bytes = []
for char in clean_str:
for char in input_str:
if has_dakuten(char) or has_handakuten(char):
bytes += dakuten_to_bytes(char)
elif char == '':
@ -269,16 +278,21 @@ def utf8_to_byte_literals(input_str):
else:
bytes.append(utf8_to_index[char])
bytes.append(0xFF)
hex_list = [hex(num) for num in bytes]
return bytes
def alt_utf8_to_byte_literals(input_str):
bytes = []
for char in input_str:
bytes.append(alt_utf8_to_index[char])
bytes.append(0xFF)
return bytes
def utf8_to_byte_literals_wrapped(input):
out = utf8_to_byte_literals(input)
str = f"_SJ()"
escaped_string = "".join([f"\\x{val:02X}" for val in out])
out = f"_SJ({escaped_string})"
return out
def alt_hd_utf8_to_byte_literals(input_str):
bytes = []
for char in input_str:
bytes.append(alt_hd_utf8_to_index[char])
bytes.append(0xFF)
return bytes
def utf8_to_byte_literals_escaped(input):
out = utf8_to_byte_literals(input)

View File

@ -3,7 +3,7 @@
import argparse
import re
import sys
from jp import utf8_to_byte_literals
from jp import utf8_to_byte_literals, alt_utf8_to_byte_literals, alt_hd_utf8_to_byte_literals
def parse(filename, str_offset):
@ -27,22 +27,40 @@ def process_string(match: re.Match[str]):
r += f"\\x{ch - 0x20:02X}"
return f'"{r}\\xFF"'
def transform_match(transform):
def repl(match: re.Match[str]):
s = match.group(1)
s = s.replace('\\"', '"')
out = transform(s)
escaped = "".join([f"\\x{val:02X}" for val in out])
return f'"{escaped}"'
return repl
def process_string_jp(match: re.Match[str]):
s = match.group(1)
out = utf8_to_byte_literals(s)
escaped = "".join([f"\\x{val:02X}" for val in out])
return f'"{escaped}"'
def process_macro(line, macro_name, transform):
return re.sub(macro_name + r'\("((?:\\["]|[^"])*)"\)',
transform,
line)
def process_s_macro(line):
return process_macro(line,
'_S',
transform_match(utf8_to_byte_literals))
def process_s2_macro(line):
return process_macro(line,
"_S2",
transform_match(alt_utf8_to_byte_literals))
def process_s2_hd_macro(line):
return process_macro(line,
"_S2_HD",
transform_match(alt_hd_utf8_to_byte_literals))
def do_sub(line):
pattern = r'_S\("([^"]*)"\)'
# english_str_processed = re.sub(pattern, process_string, line)
# pattern_jp = r'_SJ\("([^"]+)"\)'
jp_str_processed = re.sub(pattern, process_string_jp, line)
jp_str_processed = jp_str_processed.replace("_S(\"\")", "\"\\xFF\"")
return jp_str_processed
processed = process_s_macro(line)
processed = process_s2_macro(processed)
processed = process_s2_hd_macro(processed)
return processed
def process(filename):
if not filename or filename == "-":

View File

@ -22,28 +22,28 @@ class TestingJp(unittest.TestCase):
bytes = dakuten_to_bytes("")
assert bytes == [0xC3, 0xFF, 0x9E]
def test_utf8_to_byte_literals_wrapped_dakuten(self):
input = "_SJ(すで)"
out = utf8_to_byte_literals_wrapped(input)
assert out == "_SJ(\\xBD\\xC3\\xFF\\x9E\\xFF)"
def test_utf8_to_byte_literals_escaped_dakuten(self):
input = "すで"
out = utf8_to_byte_literals_escaped(input)
assert out == "\\xBD\\xC3\\xFF\\x9E\\xFF"
def test_utf8_to_byte_literals_wrapped_kanji(self):
input = "_SJ(あかつきの剣)"
out = utf8_to_byte_literals_wrapped(input)
assert out == "_SJ(\\xB1\\xB6\\xC2\\xB7\\xC9\\x3C\\xFF)"
def test_utf8_to_byte_literals_escaped_kanji(self):
input = "あかつきの剣"
out = utf8_to_byte_literals_escaped(input)
assert out == "\\xB1\\xB6\\xC2\\xB7\\xC9\\x3C\\xFF"
def check_sei():
assert(utf8_to_index[''] == 222)
def test_glasses(self):
input = "_SJ(聖なるめがね)"
out = utf8_to_byte_literals_wrapped(input)
assert out == "_SJ(\\xEE\\xC5\\xD9\\xD2\\xB6\\xFF\\x9E\\xC8\\xFF)"
input = "聖なるめがね"
out = utf8_to_byte_literals_escaped(input)
assert out == "\\xEE\\xC5\\xD9\\xD2\\xB6\\xFF\\x9E\\xC8\\xFF"
def test_moon(self):
input = "_SJ(バルザイのえん月刀)"
out = utf8_to_byte_literals_wrapped(input)
assert out == "_SJ(\\x8A\\xFF\\x9E\\x99\\x7B\\xFF\\x9E\\x72\\xC9\\xB4\\xDD\\xFF\\xFF\\xED\\xFF)"
input = "バルザイのえん月刀"
out = utf8_to_byte_literals_escaped(input)
assert out == "\\x8A\\xFF\\x9E\\x99\\x7B\\xFF\\x9E\\x72\\xC9\\xB4\\xDD\\xFF\\xFF\\xED\\xFF"
def test_str_potion(self):
input = "Str. potion"
@ -52,17 +52,43 @@ class TestingJp(unittest.TestCase):
class TestingSotnStr(unittest.TestCase):
def test_do_sub_jp(self):
line = '{_SJ("すで"), "装備なし(素手)", 0, 0, 0, 3, 255, 0, 0, 36, 42, 0, 5, 128, 0, 0, false, 8, 0, 0, 0, 0, 4, 2, 1, 1, 1, 1, 0},'
line = '{_S("すで"), "装備なし(素手)", 0, 0, 0, 3, 255, 0, 0, 36, 42, 0, 5, 128, 0, 0, false, 8, 0, 0, 0, 0, 4, 2, 1, 1, 1, 1, 0},'
out = do_sub(line)
expected = '{"\\xBD\\xC3\\xFF\\x9E\\xFF", "装備なし(素手)", 0, 0, 0, 3, 255, 0, 0, 36, 42, 0, 5, 128, 0, 0, false, 8, 0, 0, 0, 0, 4, 2, 1, 1, 1, 1, 0},'
assert out == expected
def test_jp_empty(self):
line = "_SJ(\"\")"
line = "_S(\"\")"
out = do_sub(line)
expected = '\"\\xFF\"'
assert out == expected
def test_jp_symbols_and_quotes(self):
line = "_S(\"\\\"(\\\")\")"
out = do_sub(line)
expected = '"\\x02\\x08\\x02\\x09\\xFF"'
assert out == expected, (out, expected)
def test_s2_us(self):
line = '_S2("")'
out = do_sub(line)
assert out == '"\\x00\\x01\\x01\\xFF"'
def test_s2_empty(self):
line = '_S2("")'
out = do_sub(line)
assert out == '"\\xFF"'
def test_s2_hd(self):
line = '_S2_HD("攻撃力")'
out = do_sub(line)
assert out == '"\\x0C\\x0D\\x0E\\xFF"'
def test_s2_empty(self):
line = '_S2_HD("")'
out = do_sub(line)
assert out == '"\\xFF"'
if __name__ == "__main__":
unittest.main()