From d66c1f5de90cc26cddf0ad921b50c4dc9711b930 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 16 Oct 2023 21:14:38 -0700 Subject: [PATCH] add ItemRT.afs decoder --- CMakeLists.txt | 1 + src/AFSArchive.cc | 56 ++++++++++++++++++++++++++++++++++++++++++++++ src/AFSArchive.hh | 31 +++++++++++++++++++++++++ src/Main.cc | 47 +++++++++++++++++++++++++++++++++----- src/RareItemSet.cc | 42 +++++++++++++++++++++++++++++++++- src/RareItemSet.hh | 14 ++++++++++++ 6 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/AFSArchive.cc create mode 100644 src/AFSArchive.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index 21e49c3e..27661276 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ find_package(resource_file QUIET) # Executable definition add_executable(newserv + src/AFSArchive.cc src/BattleParamsIndex.cc src/BMLArchive.cc src/CatSession.cc diff --git a/src/AFSArchive.cc b/src/AFSArchive.cc new file mode 100644 index 00000000..49585106 --- /dev/null +++ b/src/AFSArchive.cc @@ -0,0 +1,56 @@ +#include "AFSArchive.hh" + +#include +#include + +#include +#include +#include + +using namespace std; + +AFSArchive::AFSArchive(std::shared_ptr data) + : data(data) { + struct FileHeader { + be_uint32_t magic; + le_uint32_t num_files; + } __attribute__((packed)); + + struct FileEntry { + le_uint32_t offset; + le_uint32_t size; + } __attribute__((packed)); + + StringReader r(*this->data); + const auto& header = r.get(); + if (header.magic != 0x41465300) { + throw runtime_error("file is not an AFS archive"); + } + + while (this->entries.size() < header.num_files) { + const auto& entry = r.get(); + this->entries.emplace_back(Entry{.offset = entry.offset, .size = entry.size}); + } +} + +std::pair AFSArchive::get(size_t index) const { + const auto& entry = this->entries.at(index); + if (entry.offset > this->data->size()) { + throw out_of_range("entry begins beyond end of archive"); + } + if (entry.offset + entry.size > this->data->size()) { + throw out_of_range("entry extends beyond end of archive"); + } + + return make_pair(this->data->data() + entry.offset, entry.size); +} + +std::string AFSArchive::get_copy(size_t index) const { + auto ret = this->get(index); + return string(reinterpret_cast(ret.first), ret.second); +} + +StringReader AFSArchive::get_reader(size_t index) const { + auto ret = this->get(index); + return StringReader(ret.first, ret.second); +} diff --git a/src/AFSArchive.hh b/src/AFSArchive.hh new file mode 100644 index 00000000..8b6c78ce --- /dev/null +++ b/src/AFSArchive.hh @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +class AFSArchive { +public: + AFSArchive(std::shared_ptr data); + ~AFSArchive() = default; + + struct Entry { + uint64_t offset; + uint32_t size; + }; + inline const std::vector& all_entries() const { + return this->entries; + } + + std::pair get(size_t index) const; + std::string get_copy(size_t index) const; + StringReader get_reader(size_t index) const; + +private: + std::shared_ptr data; + std::vector entries; +}; diff --git a/src/Main.cc b/src/Main.cc index dd9a8834..e26cb8fe 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -317,6 +317,7 @@ enum class Behavior { FORMAT_RARE_ITEM_SET, CONVERT_ITEMRT_REL_TO_JSON, CONVERT_ITEMRT_GSL_TO_JSON, + CONVERT_ITEMRT_AFS_TO_JSON, SHOW_EP3_MAPS, SHOW_EP3_CARDS, GENERATE_EP3_CARDS_HTML, @@ -365,6 +366,7 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::FORMAT_RARE_ITEM_SET) || (b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || (b == Behavior::CONVERT_ITEMRT_GSL_TO_JSON) || + (b == Behavior::CONVERT_ITEMRT_AFS_TO_JSON) || (b == Behavior::EXTRACT_GSL) || (b == Behavior::EXTRACT_BML) || (b == Behavior::DECODE_TEXT_ARCHIVE) || @@ -403,6 +405,7 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || (b == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || (b == Behavior::CONVERT_ITEMRT_GSL_TO_JSON) || + (b == Behavior::CONVERT_ITEMRT_AFS_TO_JSON) || (b == Behavior::DECODE_SJIS) || (b == Behavior::EXTRACT_GSL) || (b == Behavior::EXTRACT_BML) || @@ -435,6 +438,7 @@ int main(int argc, char** argv) { bool json = false; bool download = false; bool one_line = false; + bool names = false; const char* find_decryption_seed_ciphertext = nullptr; vector find_decryption_seed_plaintexts; const char* input_filename = nullptr; @@ -452,6 +456,8 @@ int main(int argc, char** argv) { num_threads = strtoull(&argv[x][10], nullptr, 0); } else if (!strcmp(argv[x], "--one-line")) { one_line = true; + } else if (!strcmp(argv[x], "--names")) { + names = true; } else if (!strcmp(argv[x], "--download")) { download = true; } else if (!strcmp(argv[x], "--patch")) { @@ -619,6 +625,8 @@ int main(int argc, char** argv) { behavior = Behavior::CONVERT_ITEMRT_REL_TO_JSON; } else if (!strcmp(argv[x], "convert-itemrt-gsl-to-json")) { behavior = Behavior::CONVERT_ITEMRT_GSL_TO_JSON; + } else if (!strcmp(argv[x], "convert-itemrt-afs-to-json")) { + behavior = Behavior::CONVERT_ITEMRT_AFS_TO_JSON; } else if (!strcmp(argv[x], "show-ep3-maps")) { behavior = Behavior::SHOW_EP3_MAPS; } else if (!strcmp(argv[x], "show-ep3-cards")) { @@ -724,7 +732,9 @@ int main(int argc, char** argv) { filename += ".json"; } else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) { filename += ".txt"; - } else if ((behavior == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || (behavior == Behavior::CONVERT_ITEMRT_GSL_TO_JSON)) { + } else if ((behavior == Behavior::CONVERT_ITEMRT_REL_TO_JSON) || + (behavior == Behavior::CONVERT_ITEMRT_GSL_TO_JSON) || + (behavior == Behavior::CONVERT_ITEMRT_AFS_TO_JSON)) { filename += ".json"; } else { filename += ".dec"; @@ -734,7 +744,8 @@ int main(int argc, char** argv) { } else if (isatty(fileno(stdout)) && (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT) && (behavior != Behavior::CONVERT_ITEMRT_REL_TO_JSON) && - (behavior != Behavior::CONVERT_ITEMRT_GSL_TO_JSON)) { + (behavior != Behavior::CONVERT_ITEMRT_GSL_TO_JSON) && + (behavior != Behavior::CONVERT_ITEMRT_AFS_TO_JSON)) { // If stdout is a terminal and the data is not known to be text, use // print_data to write the result print_data(stdout, data, size); @@ -1570,11 +1581,14 @@ int main(int argc, char** argv) { } case Behavior::CONVERT_ITEMRT_REL_TO_JSON: - case Behavior::CONVERT_ITEMRT_GSL_TO_JSON: { + case Behavior::CONVERT_ITEMRT_GSL_TO_JSON: + case Behavior::CONVERT_ITEMRT_AFS_TO_JSON: { shared_ptr data(new string(read_input_data())); unique_ptr rs; if (behavior == Behavior::CONVERT_ITEMRT_GSL_TO_JSON) { rs.reset(new GSLRareItemSet(data, big_endian)); + } else if (behavior == Behavior::CONVERT_ITEMRT_AFS_TO_JSON) { + rs.reset(new AFSRareItemSet(data)); } else { rs.reset(new RELRareItemSet(data)); } @@ -1625,8 +1639,19 @@ int main(int argc, char** argv) { continue; } + JSON id_json; + if (names) { + ItemData data; + data.data1[0] = spec.item_code[0]; + data.data1[1] = spec.item_code[1]; + data.data1[2] = spec.item_code[2]; + id_json = data.name(false); + } else { + id_json = primary_identifier; + } + auto frac = reduce_fraction(spec.probability, 0x100000000); - auto specs_json = JSON::list({JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), primary_identifier})}); + auto specs_json = JSON::list({JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), std::move(id_json)})}); for (const auto& enemy_type : enemy_types) { if (enemy_type_valid_for_episode(episode, enemy_type)) { collection_dict.emplace(name_for_enum(enemy_type), std::move(specs_json)); @@ -1643,8 +1668,20 @@ int main(int argc, char** argv) { if (primary_identifier == 0) { continue; } + + JSON id_json; + if (names) { + ItemData data; + data.data1[0] = spec.item_code[0]; + data.data1[1] = spec.item_code[1]; + data.data1[2] = spec.item_code[2]; + id_json = data.name(false); + } else { + id_json = primary_identifier; + } + auto frac = reduce_fraction(spec.probability, 0x100000000); - area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), primary_identifier})); + area_list.emplace_back(JSON::list({string_printf("%" PRIu64 "/%" PRIu64, frac.first, frac.second), std::move(id_json)})); } if (!area_list.empty()) { diff --git a/src/RareItemSet.cc b/src/RareItemSet.cc index 8e5d93b0..5b2057ac 100644 --- a/src/RareItemSet.cc +++ b/src/RareItemSet.cc @@ -88,13 +88,53 @@ uint16_t RareItemSet::key_for_params(GameMode mode, Episode episode, uint8_t dif return key; } +AFSRareItemSet::AFSRareItemSet(shared_ptr data) + : afs(data) { + const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; + for (GameMode mode : modes) { + for (size_t difficulty = 0; difficulty < 4; difficulty++) { + for (size_t section_id = 0; section_id < 10; section_id++) { + try { + size_t index = difficulty * 10 + section_id; + auto entry = this->afs.get(index); + if (entry.second < sizeof(Table)) { + throw runtime_error(string_printf("table %zu is too small", index)); + } + this->tables.emplace( + this->key_for_params(mode, Episode::EP1, difficulty, section_id), + reinterpret_cast(entry.first)); + } catch (const out_of_range&) { + } + } + } + } +} + +std::vector AFSRareItemSet::get_enemy_specs( + GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const { + try { + return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_enemy_specs(rt_index); + } catch (const out_of_range&) { + return {}; + } +} + +std::vector AFSRareItemSet::get_box_specs( + GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const { + try { + return this->tables.at(this->key_for_params(mode, episode, difficulty, secid))->get_box_specs(area); + } catch (const out_of_range&) { + return {}; + } +} + GSLRareItemSet::GSLRareItemSet(shared_ptr data, bool is_big_endian) : gsl(data, is_big_endian) { const array episodes = {Episode::EP1, Episode::EP2}; const array modes = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO}; for (GameMode mode : modes) { for (Episode episode : episodes) { - for (size_t difficulty = 0; difficulty < 3; difficulty++) { + for (size_t difficulty = 0; difficulty < 4; difficulty++) { for (size_t section_id = 0; section_id < 10; section_id++) { string filename = string_printf("ItemRT%s%s%c%1zu.rel", ((mode == GameMode::CHALLENGE) ? "c" : ""), diff --git a/src/RareItemSet.hh b/src/RareItemSet.hh index ed5bd171..45b82043 100644 --- a/src/RareItemSet.hh +++ b/src/RareItemSet.hh @@ -6,6 +6,7 @@ #include #include +#include "AFSArchive.hh" #include "GSLArchive.hh" #include "StaticGameData.hh" @@ -64,6 +65,19 @@ protected: static uint16_t key_for_params(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid); }; +class AFSRareItemSet : public RareItemSet { +public: + AFSRareItemSet(std::shared_ptr data); + virtual ~AFSRareItemSet() = default; + virtual std::vector get_enemy_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t rt_index) const; + virtual std::vector get_box_specs(GameMode mode, Episode episode, uint8_t difficulty, uint8_t secid, uint8_t area) const; + +private: + std::unordered_map tables; + + AFSArchive afs; +}; + class GSLRareItemSet : public RareItemSet { public: GSLRareItemSet(std::shared_ptr data, bool is_big_endian);