tmc/tools/tmc_strings/main.cpp

800 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <fmt/format.h>
#include <fstream>
#include <nlohmann/json.hpp>
using namespace std::string_literals;
using json = nlohmann::json;
using u8 = std::uint8_t;
using u16 = std::uint16_t;
using u32 = std::uint32_t;
const u32 RomStartAddress = 0x8000000;
namespace {
struct LanguageTable {
const char *name;
u32 address;
};
enum Color {
Color_White,
Color_Red,
Color_Green,
Color_Blue,
Color_Yellow,
Color_Count,
};
constexpr const char *const ColorStrings[] = {
[Color_White] = "White",
[Color_Red] = "Red",
[Color_Green] = "Green",
[Color_Blue] = "Blue",
[Color_Yellow] = "Yellow",
};
enum Input {
Input_A,
Input_B,
Input_Left,
Input_Right,
Input_DUp,
Input_DDown,
Input_DLeft,
Input_DRight,
Input_Dpad,
Input_Select,
Input_Start,
Input_Count,
};
constexpr const char *const InputStrings[] = {
[Input_A] = "A",
[Input_B] = "B",
[Input_Left] = "Left",
[Input_Right] = "Right",
[Input_DUp] = "DUp",
[Input_DDown] = "DDown",
[Input_DLeft] = "DLeft",
[Input_DRight] = "DRight",
[Input_Dpad] = "Dpad",
[Input_Select] = "Select",
[Input_Start] = "Start",
};
const std::map<u8, std::string> CharConvertArray = {
{0x0a, "\n"},
{0x0d, "\r"},
{0x20, " "},
{0x21, "!"},
{0x22, "\""},
{0x23, "#"},
{0x24, "$"},
{0x25, "%"},
{0x26, "&"},
{0x27, "\'"},
{0x28, "("},
{0x29, ")"},
{0x2a, "*"},
{0x2b, "+"},
{0x2c, ","},
{0x2d, "-"},
{0x2e, "."},
{0x2f, "/"},
{0x30, "0"},
{0x31, "1"},
{0x32, "2"},
{0x33, "3"},
{0x34, "4"},
{0x35, "5"},
{0x36, "6"},
{0x37, "7"},
{0x38, "8"},
{0x39, "9"},
{0x3a, ":"},
{0x3b, ";"},
{0x3c, "<"},
{0x3d, "="},
{0x3e, ">"},
{0x3f, "?"},
{0x40, "@"},
{0x41, "A"},
{0x42, "B"},
{0x43, "C"},
{0x44, "D"},
{0x45, "E"},
{0x46, "F"},
{0x47, "G"},
{0x48, "H"},
{0x49, "I"},
{0x4a, "J"},
{0x4b, "K"},
{0x4c, "L"},
{0x4d, "M"},
{0x4e, "N"},
{0x4f, "O"},
{0x50, "P"},
{0x51, "Q"},
{0x52, "R"},
{0x53, "S"},
{0x54, "T"},
{0x55, "U"},
{0x56, "V"},
{0x57, "W"},
{0x58, "X"},
{0x59, "Y"},
{0x5a, "Z"},
{0x5b, "["},
{0x5c, "\'"},
{0x5d, "]"},
{0x5e, "^"},
{0x5f, "_"},
{0x60, "`"},
{0x61, "a"},
{0x62, "b"},
{0x63, "c"},
{0x64, "d"},
{0x65, "e"},
{0x66, "f"},
{0x67, "g"},
{0x68, "h"},
{0x69, "i"},
{0x6a, "j"},
{0x6b, "k"},
{0x6c, "l"},
{0x6d, "m"},
{0x6e, "n"},
{0x6f, "o"},
{0x70, "p"},
{0x71, "q"},
{0x72, "r"},
{0x73, "s"},
{0x74, "t"},
{0x75, "u"},
{0x76, "v"},
{0x77, "w"},
{0x78, "x"},
{0x79, "y"},
{0x7a, "z"},
{0x82, ","},
{0x84, ""},
{0x85, ""},
{0x8A, "Š"},
{0x8B, ""},
{0x8C, "Œ"},
{0x8E, "Ž"},
{0x91, ""},
{0x92, ""},
{0x93, ""},
{0x94, ""},
{0x95, "·"},
{0x99, ""},
{0x9A, "š"},
{0x9B, ""},
{0x9C, "œ"},
{0x9E, "ž"},
{0x9F, "Ÿ"},
{0xA1, "¡"},
{0xA3, ""},
{0xAA, "ª"},
{0xAB, "«"},
{0xB0, "º"},
{0xB4, "'"},
{0xB7, "´"},
{0xBA, "º"},
{0xBB, "»"},
{0xBF, "¿"},
{0xC0, "À"},
{0xC1, "Á"},
{0xC2, "Â"},
{0xC3, "Ã"},
{0xC4, "Ä"},
{0xC5, "Å"},
{0xC6, "Æ"},
{0xC7, "Ç"},
{0xC8, "È"},
{0xC9, "É"},
{0xCA, "Ê"},
{0xCB, "Ë"},
{0xCC, "Ì"},
{0xCD, "Í"},
{0xCE, "Î"},
{0xCF, "Ï"},
{0xD0, "Đ"},
{0xD1, "Ñ"},
{0xD2, "Ò"},
{0xD3, "Ó"},
{0xD4, "Ô"},
{0xD5, "Õ"},
{0xD6, "Ö"},
{0xD7, "×"},
{0xD8, "Ø"},
{0xD9, "Ù"},
{0xDA, "Ú"},
{0xDB, "Û"},
{0xDC, "Ü"},
{0xDD, "Ý"},
{0xDE, "Þ"},
{0xDF, "β"},
{0xE0, "à"},
{0xE1, "á"},
{0xE2, "â"},
{0xE3, "ã"},
{0xE4, "ä"},
{0xE5, "å"},
{0xE6, "æ"},
{0xE7, "ç"},
{0xE8, "è"},
{0xE9, "é"},
{0xEA, "ê"},
{0xEB, "ë"},
{0xEC, "ì"},
{0xED, "í"},
{0xEE, "î"},
{0xEF, "ï"},
{0xF0, "ð"},
{0xF1, "ñ"},
{0xF2, "ò"},
{0xF3, "ó"},
{0xF4, "ô"},
{0xF5, "õ"},
{0xF6, "ö"},
{0xF7, "÷"},
{0xF8, "ø"},
{0xF9, "ù"},
{0xFA, "ú"},
{0xFB, "û"},
{0xFC, "ü"},
{0xFD, "ý"},
{0xFE, "þ"},
{0xFF, "ÿ"},
};
using ConvertFunction = std::string (*const)(const char *&);
std::string Unk1Handler(const char *&ptr) {
u8 a = *ptr++;
return fmt::format("{{01:{:02X}}}", a);
}
std::string ColorHandler(const char *&ptr) {
u8 color = *ptr++;
if (color >= Color_Count)
throw std::runtime_error(ptr);
return fmt::format("{{Color:{}}}", ColorStrings[color]);
}
std::string SoundHandler(const char *&ptr) {
u8 a = *ptr++;
u8 b = *ptr++;
return fmt::format("{{Sound:{:02X}:{:02X}}}", a, b);
}
std::string Unk4Handler(const char *&ptr) {
u8 a = *ptr++;
if (a == 0x10) {
u8 b = *ptr++;
return fmt::format("{{04:{:02X}:{:02X}}}", a, b);
} else {
return fmt::format("{{04:{:02X}}}", a);
}
}
std::string ChoiceHandler(const char *&ptr) {
u8 category = *ptr++;
if (category == 0xff) {
return "{Choice:FF}";
} else {
u8 action = *ptr++;
return fmt::format("{{Choice:{:02X}:{:02X}}}", category, action);
}
}
std::string VariableHandler(const char *&ptr) {
u8 idx = *ptr++;
if (idx == 0) {
return "{Player}";
} else {
return fmt::format("{{Var:{:X}}}", idx);
}
}
std::string Unk7Handler(const char *&ptr) {
u8 a = *ptr++;
u8 b = *ptr++;
return fmt::format("{{07:{:02X}:{:02X}}}", a, b);
}
std::string Unk8Handler(const char *&ptr) {
u8 a = *ptr++;
if (a != 0xff)
throw std::runtime_error("unmatched unk8: "s + std::to_string(a));
return "{08:FF}";
}
std::string Unk9Handler(const char *&ptr) {
u8 a = *ptr++;
if (a != 0x78 && a != 0x00)
throw std::runtime_error("unmatched unk8: "s + std::to_string(a));
return fmt::format("{{09:{:02X}}}", a);
}
std::string InputHandler(const char *&ptr) {
u8 key = *ptr++;
if (key > 8)
throw std::runtime_error("unmatched key: "s + std::to_string(key));
return fmt::format("{{Key:{}}}", InputStrings[key]);
}
std::string SymbolHandler(const char *&ptr) {
u8 a = *ptr++;
return fmt::format("{{Symbol:{:02X}}}", a);
}
const std::map<u8, ConvertFunction> FuncConvertArray = {
{0x01, Unk1Handler},
{0x02, ColorHandler},
{0x03, SoundHandler},
{0x04, Unk4Handler},
{0x05, ChoiceHandler},
{0x06, VariableHandler},
{0x07, Unk7Handler},
{0x08, Unk8Handler},
{0x09, Unk9Handler},
{0x0c, InputHandler},
{0x0f, SymbolHandler},
};
std::string ParseTMCString(const char *ptr) {
std::string ret;
while (*ptr) {
u8 c = *ptr++;
/* Convert character. */
{
const auto it = CharConvertArray.find(c);
if (it != std::end(CharConvertArray)) {
ret += it->second;
continue;
}
}
/* Convert function. */
{
const auto it = FuncConvertArray.find(c);
if (it != std::end(FuncConvertArray)) {
ret += it->second(ptr);
continue;
}
}
throw std::runtime_error(fmt::format("Unknown characters: {}", ptr));
}
return ret;
}
using RevertFunction = void (*)(const char *&, char *&);
void ColorRevert(const char *&src, char *&dst) {
*dst++ = 0x02;
src++; // ':'
for (u32 i = 0; i < Color_Count; i++) {
const char *const color = ColorStrings[i];
const std::size_t color_len = std::strlen(color);
if (std::strncmp(src, color, color_len) == 0) {
*dst++ = i;
src += color_len;
return;
}
}
throw std::runtime_error(fmt::format("Color not found: {}", src));
}
void SoundRevert(const char *&src, char *&dst) {
*dst++ = 0x03;
*dst++ = std::strtoul(++src, nullptr, 0x10);
src += 2;
*dst++ = std::strtoul(++src, nullptr, 0x10);
src += 2;
}
void ChoiceRevert(const char *&src, char *&dst) {
*dst++ = 0x05;
u8 choice = std::strtoul(++src, nullptr, 0x10);
*dst++ = choice;
src += 2;
if (choice == 0xff)
return;
*dst++ = std::strtoul(++src, nullptr, 0x10);
src += 2;
}
void PlayerRevert(const char *&src, char *&dst) {
*dst++ = 0x06;
*dst++ = 0x00;
(void)src;
}
void VariableRevert(const char *&src, char *&dst) {
*dst++ = 0x06;
src++; // ':'
*dst++ = std::strtoul(src, nullptr, 0x10);
src++;
}
void KeyRevert(const char *&src, char *&dst) {
*dst++ = 0x0c;
src++; // ':'
for (u32 i = 0; i < Input_Count; i++) {
const char *const input = InputStrings[i];
const std::size_t input_len = std::strlen(input);
if (std::strncmp(src, input, input_len) == 0) {
*dst++ = i;
src += input_len;
return;
}
}
throw std::runtime_error(fmt::format("Input not found: {}", src));
}
void SymbolRevert(const char *&src, char *&dst) {
*dst++ = 0x0f;
src++; // ':'
*dst++ = std::strtoul(src, nullptr, 0x10);
src += 2;
}
const std::pair<std::string, RevertFunction> FuncRevertArray[] = {
{"Color", ColorRevert},
{"Sound", SoundRevert},
{"Choice", ChoiceRevert},
{"Player", PlayerRevert},
{"Var", VariableRevert},
{"Key", KeyRevert},
{"Symbol", SymbolRevert},
};
void WriteTMCString(char *&dst, const std::string &src) {
const char *ptr = src.data();
while (*ptr) {
/* Parse special */
{
if (*ptr == '{') {
ptr++;
const auto it = std::find_if(std::begin(FuncRevertArray), std::end(FuncRevertArray), [&](const auto &data) {
return std::strncmp(ptr, data.first.c_str(), data.first.size()) == 0;
});
if (it != std::end(FuncRevertArray)) {
ptr += it->first.size();
it->second(ptr, dst);
} else {
do {
*dst++ = std::strtoul(ptr, nullptr, 0x10);
ptr += 2;
} while (*ptr == ':' && (ptr++, true));
}
if (*ptr != '}')
throw std::runtime_error(fmt::format("unmatched characters: \"{}\"\n", ptr));
ptr++;
continue;
}
}
/* Convert character. */
{
const auto it = std::find_if(std::begin(CharConvertArray), std::end(CharConvertArray), [ptr](const auto &data) {
return std::strncmp(ptr, data.second.c_str(), data.second.length()) == 0;
});
if (it != std::end(CharConvertArray)) {
ptr += it->second.size();
*dst++ = it->first;
continue;
}
}
throw std::runtime_error(fmt::format("unmatched characters: \"{}\"\n", ptr));
}
*dst++ = '\0';
}
}
void ExtractStringTable(std::string &rom_path, const std::vector<LanguageTable> &tables) {
const std::vector<char> rom = [&]() {
std::vector<char> rom;
/* Open ROM. */
std::ifstream rom_file(rom_path, std::ios::in);
if (!rom_file.good()) {
fmt::print(stderr, "Couldn't open ROM {}\n", rom_path);
exit(EXIT_FAILURE);
}
/* Get ROM size. */
rom_file.seekg(0, std::ios::end);
const std::size_t rom_size = rom_file.tellg();
/* Read ROM to buffer. */
rom.resize(rom_size);
rom_file.seekg(0, std::ios::beg);
rom_file.read(&rom[0], rom_size);
return rom;
}();
auto ReadAbsolute = [&](u32 address) -> u32 {
return *(u32 *)&rom[address - RomStartAddress];
};
for (auto &language : tables) {
/* Get category table start. */
const std::size_t table_start = language.address;
/* Get category count. */
const std::size_t category_count = ReadAbsolute(table_start) / sizeof(u32);
/* Read category offsets. */
std::vector<u32> category_table(category_count);
std::memcpy(&category_table[0], &rom[table_start - RomStartAddress], category_count * sizeof(u32));
json j = json::array();
for (auto &category_offset : category_table) {
/* Get string table start. */
const std::size_t category_start = table_start + category_offset;
/* Get string count. */
const std::size_t string_count = ReadAbsolute(category_start) / sizeof(u32);
/* Read string offsets. */
std::vector<u32> string_table(string_count);
std::memcpy(&string_table[0], &rom[category_start - RomStartAddress], string_count * sizeof(u32));
auto &category = j.emplace_back();
for (std::size_t l = 0; l < string_count; l++) {
/* Get string start. */
const std::size_t string_start = category_start + string_table[l];
/* Parse TMC. */
category[l] = ParseTMCString(&rom[string_start - RomStartAddress]);
}
}
const std::string out_path = language.name + ".json"s;
std::ofstream ofs(out_path);
ofs << j.dump(4);
}
}
void PackStringTable(const std::string &src_path, const std::string &dst_path, const std::size_t out_size) {
const json j = [&]() -> json {
std::ifstream ifs(src_path);
if (!ifs.good()) {
fmt::print(stderr, "Couldn't open JSON {}\n", src_path);
exit(EXIT_FAILURE);
}
return json::parse(ifs);
}();
std::vector<char> buffer(0x100000);
std::uintptr_t root_start = (std::uintptr_t)buffer.data();
char *root_ptr = buffer.data();
char *table = buffer.data() + j.size() * sizeof(u32);
for (auto &category : j) {
char *table_ptr = table;
char *str_start = table_ptr + category.size() * sizeof(u32);
char *str_ptr = str_start;
for (auto &str_j : category) {
/* Write string offset to table. */
*(u32 *)table_ptr = (std::uintptr_t)str_ptr - (std::uintptr_t)table;
table_ptr += sizeof(u32);
auto str = str_j.get<std::string>();
/* Copy string to table. */
WriteTMCString(str_ptr, str);
}
/* Align string table size by 16 bytes. */
while (static_cast<std::uintptr_t>(str_ptr - str_start) & 0xf) {
*str_ptr = 0xff;
str_ptr++;
}
/* Write category offset to root table. */
*(u32 *)root_ptr = (std::uintptr_t)table - root_start;
root_ptr += sizeof(u32);
table = str_ptr;
}
/* Align table end to 0x10 bytes. */
while ((std::uintptr_t)table & 0xf) {
*table++ = 0xff;
}
std::size_t table_size = (uintptr_t)table - root_start;
if (out_size) {
/* Abort if string table is too big. */
if (table_size > out_size) {
fmt::print(stderr, "Table is too big. Should be {} but was {}", out_size, table_size);
exit(EXIT_FAILURE);
}
/* Pad table if it's too small. */
while (table_size < out_size) {
*table++ = 0xff;
table_size++;
}
}
std::ofstream ofs(dst_path);
ofs.write(buffer.data(), table_size);
}
// clang-format off
const std::vector<LanguageTable> LanguageTableUS = {
{"USA", 0x89B1D90},
};
const std::vector<LanguageTable> LanguageTableEU = {
{"English", 0x89AEB60},
{"French", 0x89F7420},
{"German", 0x8A3EEB0},
{"Spanish", 0x8A81E70},
{"Italian", 0x8AC37A0},
};
// clang-format on
#include <getopt.h>
const char *progname;
void usage() {
fmt::print(stderr,
"tmc_strings (c) Luis Scheurenbrand\n"
"Built: " __TIME__ " " __DATE__ "\n"
"\n"
"Usage: {} [options...]\n"
"Options:\n"
" -x, --extract Extract string table from ROM and store it in json format. (Default)\n"
" -p, --pack Pack a string table from json format.\n"
" --region Specify ROM region. [USA, EU]\n"
" --source Specify source (-x: ROM, -p: JSON)\n"
" --dest Specify string table destination.\n"
" --size Specify string table size.\n",
progname);
}
// clang-format off
constexpr const struct option long_options[] = {
{"extract", no_argument, nullptr, 'x'},
{"pack", no_argument, nullptr, 'p'},
{"region", required_argument, nullptr, 0},
{"source", required_argument, nullptr, 1},
{"dest", required_argument, nullptr, 2},
{"size", required_argument, nullptr, 3},
{},
};
// clang-format on
int main(int argc, char **argv) {
std::string src_path;
std::string dst_path;
std::size_t max_size = 0;
progname = (argc < 1) ? "tmc_strings" : argv[0];
enum {
Mode_Extract,
Mode_Pack,
} mode = Mode_Extract;
enum {
Region_USA,
Region_JAPAN,
Region_EU,
} region = Region_USA;
while (true) {
int opt_index;
int c = getopt_long(argc, argv, "xp", long_options, &opt_index);
if (c == -1)
break;
switch (c) {
case 'x':
mode = Mode_Extract;
break;
case 'p':
mode = Mode_Pack;
break;
case 0:
if (strcmp(optarg, "USA") == 0) {
region = Region_USA;
} else if (strcmp(optarg, "EU") == 0) {
region = Region_EU;
} else if (strcmp(optarg, "JAPAN") == 0) {
region = Region_JAPAN;
fmt::print("Region unsupported\n\n");
usage();
exit(EXIT_FAILURE);
}
break;
case 1:
src_path = optarg;
break;
case 2:
dst_path = optarg;
break;
case 3:
max_size = std::strtoul(optarg, nullptr, 0);
break;
default:
usage();
exit(EXIT_FAILURE);
}
}
switch (mode) {
case Mode_Extract:
if (src_path == "") {
fmt::print("ROM source path not supplied.\n\n");
usage();
exit(EXIT_FAILURE);
}
ExtractStringTable(src_path, [region]() {
switch (region) {
case Region_USA:
return LanguageTableUS;
case Region_EU:
return LanguageTableEU;
default:
fmt::print("Region unsupported\n\n");
usage();
exit(EXIT_FAILURE);
}
}());
break;
case Mode_Pack:
if (src_path == "" || dst_path == "") {
fmt::print("Source or destination path not supplied\n\n");
usage();
exit(EXIT_FAILURE);
}
if (max_size == 0) {
fmt::print("Max size not supplied. Assuming shiftable.\n");
}
PackStringTable(src_path, dst_path, max_size);
}
return EXIT_SUCCESS;
}