From 2429c4d341b567aa5c0d86112667aa24715a95e7 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 1 Feb 2026 17:29:31 -0800 Subject: [PATCH] add decoder/encoder for AdEnding.rel --- src/Main.cc | 33 +++++++++++++++++++-- src/TextIndex.cc | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ src/TextIndex.hh | 3 ++ 3 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index 4a5acd77..84f9aaa1 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1628,7 +1628,7 @@ Action a_disassemble_quest_script( QEdit. If you intend to reassemble the script, after editing it, use the\n\ --reassembly option to add explicit label numbers and remove offsets and\n\ data in code sections. To include script references from the map, use the\n\ - --map-file=FILENAME option.", + --map-file=FILENAME option.\n", +[](phosg::Arguments& args) { string data = read_input_data(args); auto version = get_cli_version(args); @@ -1720,7 +1720,7 @@ Action a_assemble_quest_script( Assemble the input quest script (.txt file) into a compressed .bin file\n\ usable as an online quest script. If --decompressed is given, produces an\n\ uncompressed .bind file instead. If --disable-strict is given, allows some\n\ - invalid behaviors (e.g. calling an undefined label by number).", + invalid behaviors (e.g. calling an undefined label by number).\n", +[](phosg::Arguments& args) { string text = read_input_data(args); @@ -2035,6 +2035,35 @@ Action a_encode_unicode_text_set( write_output_data(args, encoded.data(), encoded.size(), "prs"); }); +Action a_decode_credits_text_archive( + "decode-credits-text-archive", "\ + decode-credits-text-archive [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Decode a credits text archive (AdEnding.rel) to JSON. Use the --big-endian\n\ + option if the file is for PSO GC.\n", + +[](phosg::Arguments& args) { + auto ret = decode_credits_text_set(read_input_data(args), args.get("big-endian")); + auto json = phosg::JSON::list(); + for (const auto& it : ret) { + json.emplace_back(it); + } + string out_data = json.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY | phosg::JSON::SerializeOption::EXPAND_LEAF_CONTAINERS); + write_output_data(args, out_data.data(), out_data.size(), "json"); + }); +Action a_encode_credits_text_archive( + "encode-credits-text-archive", "\ + encode-credits-text-archive [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Encode a credits text archive (AdEnding.rel) from JSON. Use the\n\ + --big-endian option if the file is for PSO GC.\n", + +[](phosg::Arguments& args) { + auto json = phosg::JSON::parse(read_input_data(args)); + std::vector data; + for (const auto& it : json.as_list()) { + data.emplace_back(it->as_string()); + } + auto ret = encode_credits_text_set(data, args.get("big-endian")); + write_output_data(args, ret.data(), ret.size(), "rel"); + }); + Action a_decode_word_select_set( "decode-word-select-set", "\ decode-word-select-set [INPUT-FILENAME]\n\ diff --git a/src/TextIndex.cc b/src/TextIndex.cc index 6de8a5f5..71bc7e5a 100644 --- a/src/TextIndex.cc +++ b/src/TextIndex.cc @@ -551,3 +551,79 @@ std::shared_ptr TextIndex::get(Version version, Language language uint32_t TextIndex::key_for_set(Version version, Language language) { return (static_cast(version) << 8) | static_cast(language); } + +template +std::vector decode_credits_text_set_t(const std::string& data) { + std::vector ret; + phosg::StringReader r(data); + const auto& footer = r.pget>(r.size() - sizeof(RELFileFooterT)); + r.go(footer.root_offset); + for (;;) { + ret.emplace_back(tt_sega_sjis_to_utf8(r.pget_cstr(r.get>()))); + if (!ret.back().empty() && (ret.back()[0] == '*')) { + break; + } + } + return ret; +} + +std::vector decode_credits_text_set(const std::string& data, bool big_endian) { + if (big_endian) { + return decode_credits_text_set_t(data); + } else { + return decode_credits_text_set_t(data); + } +} + +template +std::string encode_credits_text_set_t(const std::vector& data) { + if (data.empty() || (data.back() != "*")) { + throw std::runtime_error("the last string in a credits text set must be \"*\""); + } + + phosg::StringWriter strings_w; + phosg::StringWriter offsets_w; + std::unordered_map existing_offsets; + for (const auto& str : data) { + try { + offsets_w.put>(existing_offsets.at(str)); + } catch (const std::out_of_range&) { + existing_offsets.emplace(str, strings_w.size()); + offsets_w.put>(strings_w.size()); + strings_w.write(tt_utf8_to_sega_sjis(str)); + strings_w.put_u8(0); + while (strings_w.size() & 3) { + strings_w.put_u8(0); + } + } + } + + phosg::StringWriter w; + RELFileFooterT footer; + w.write(strings_w.str()); + footer.root_offset = w.size(); + w.write(offsets_w.str()); + while (w.size() & 0x1F) { + w.put_u8(0); + } + footer.relocations_offset = w.size(); + footer.num_relocations = data.size(); + w.put>(strings_w.size() / 4); + for (size_t z = 1; z < data.size(); z++) { + w.put>(1); + } + while (w.size() & 0x1F) { + w.put_u8(0); + } + w.put(footer); + + return std::move(w.str()); +} + +std::string encode_credits_text_set(const std::vector& data, bool big_endian) { + if (big_endian) { + return encode_credits_text_set_t(data); + } else { + return encode_credits_text_set_t(data); + } +} diff --git a/src/TextIndex.hh b/src/TextIndex.hh index 479618f3..0899a792 100644 --- a/src/TextIndex.hh +++ b/src/TextIndex.hh @@ -109,3 +109,6 @@ protected: phosg::PrefixedLogger log; std::unordered_map> sets; }; + +std::vector decode_credits_text_set(const std::string& data, bool big_endian); +std::string encode_credits_text_set(const std::vector& data, bool big_endian);