From 9a35f5ca638dc25daf283ac2cb1beeb101cf2ed0 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 3 Sep 2022 01:13:11 -0700 Subject: [PATCH] reorganize BB file loading abstractions --- .gitignore | 3 + CMakeLists.txt | 1 + README.md | 21 +- src/GSLArchive.cc | 67 +++ src/GSLArchive.hh | 31 ++ src/LevelTable.cc | 16 +- src/LevelTable.hh | 19 +- src/Lobby.hh | 1 - src/Main.cc | 67 ++- src/Map.cc | 484 ++++++++++++++----- src/Map.hh | 84 +++- src/PSOBBFileLoader.cc | 0 src/PSOBBFileLoader.hh | 5 + src/PatchFileIndex.cc | 36 +- src/PatchFileIndex.hh | 19 +- src/RareItemSet.cc | 26 +- src/RareItemSet.hh | 43 +- src/ReceiveCommands.cc | 100 ++-- src/ReceiveSubcommands.cc | 39 +- src/SendCommands.cc | 14 +- src/ServerShell.cc | 8 +- src/ServerState.cc | 64 ++- src/ServerState.hh | 8 + src/StaticGameData.cc | 204 -------- src/StaticGameData.hh | 10 - system/blueburst/map/map_city00_00e_d.dat | Bin 1080 -> 0 bytes system/blueburst/map/map_city02_00_00e.dat | Bin 1512 -> 1512 bytes system/blueburst/map/map_crater01_00_00e.dat | Bin 15552 -> 12312 bytes system/blueburst/map/map_desert01_00_00e.dat | Bin 1008 -> 11376 bytes system/blueburst/map/map_desert01_01_00e.dat | Bin 1008 -> 11880 bytes system/blueburst/map/map_desert01_02_00e.dat | Bin 1008 -> 11952 bytes system/blueburst/map/map_desert03_00_00e.dat | Bin 1368 -> 12600 bytes system/blueburst/map/map_desert03_01_00e.dat | Bin 1296 -> 12384 bytes system/blueburst/map/map_desert03_02_00e.dat | Bin 2016 -> 12024 bytes system/blueburst/map/map_labo00_00e_d.dat | Bin 1080 -> 0 bytes system/blueburst/map/map_wilds01_00_00e.dat | Bin 16920 -> 9288 bytes system/blueburst/map/map_wilds01_03_00e.dat | Bin 14688 -> 12024 bytes system/client-data/V2-SetDataTableOn-LE.rel | Bin 0 -> 5856 bytes system/client-data/V3-SetDataTableOn-BE.rel | Bin 0 -> 9312 bytes tests/BB-CreateCharGame.test.txt | 105 ---- tests/PC-BasicGame.test.txt | 2 +- 41 files changed, 870 insertions(+), 607 deletions(-) create mode 100644 src/GSLArchive.cc create mode 100644 src/GSLArchive.hh create mode 100644 src/PSOBBFileLoader.cc create mode 100644 src/PSOBBFileLoader.hh delete mode 100755 system/blueburst/map/map_city00_00e_d.dat delete mode 100755 system/blueburst/map/map_labo00_00e_d.dat create mode 100644 system/client-data/V2-SetDataTableOn-LE.rel create mode 100755 system/client-data/V3-SetDataTableOn-BE.rel diff --git a/.gitignore b/.gitignore index d9e4cd34..8cc981cb 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ old-khyller old-newserv release release.zip +system/patch-bb/data +system/patch-bb/psobb.pat +system/dol \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b0a989e..7c99489f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,7 @@ add_executable(newserv src/Episode3.cc src/FileContentsCache.cc src/FunctionCompiler.cc + src/GSLArchive.cc src/IPFrameInfo.cc src/IPStackSimulator.cc src/Items.cc diff --git a/README.md b/README.md index 590e9a18..4664bac4 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Current known issues / missing features: - PSOBB is not well-tested and likely will disconnect or misbehave when clients try to use unimplemented features. - Fix some edge cases on the BB proxy server (e.g. make sure Change Ship does the right thing, which is not the same as what it should do on V2/V3). - PSOX is not tested at all. -- Patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly. +- Memory patches currently are platform-specific but not version-specific. This makes them quite a bit harder to write and use properly. - Find a way to silence audio in RunDOL.s. Some old DOLs don't reset audio systems at load time and it's annoying to hear the crash buzz when the GC hasn't actually crashed. - Implement private and overflow lobbies. - Enforce client-side size limits (e.g. for 60/62 commands) on the server side as well. (For 60/62 specifically, perhaps transform them to 6C/6D if needed.) @@ -53,14 +53,15 @@ There is a probably-not-too-old macOS ARM64 release on the newserv GitHub reposi If you're using an older AMD64 Mac, you're running Linux, or you just want to build newserv yourself, here's what you do: 1. Make sure you have CMake and libevent installed. (`brew install cmake libevent` on macOS, `sudo apt-get install cmake libevent-dev` on most Linuxes) 2. Build and install phosg (https://github.com/fuzziqersoftware/phosg). -3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to run patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this. +3. Optionally, install resource_dasm (https://github.com/fuzziqersoftware/resource_dasm). This will enable newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this. 4. Run `cmake . && make` on the newserv directory. After building newserv or downloading a release, do this to set it up and use it: 1. In the system/ directory, make a copy of config.example.json named config.json, and edit it appropriately. 2. Run `./newserv` in the newserv directory. This will start the game server and run the interactive shell. You may need `sudo` if newserv's built-in DNS server is enabled. 3. Use the interactive shell to add a license. Run `help` in the shell to see how to do this. -4. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect. +4. If you plan to play PSO Blue Burst on newserv, set up the patch directory appropriately. See the "Client patch directories" section below. +5. Set your client's network settings appropriately and start an online game. See the "Connecting local clients" or "Connecting remote clients" section to see how to get your game client to connect. ### Installing quests @@ -87,11 +88,21 @@ If you've changed the contents of the quests directory, you can re-index the que All quests, including those originally in GCI or DLQ format, are treated as online quests unless their filenames specify the dl category. newserv allows players to download all quests, even those in non-download categories. -### Patches and DOL files +### Client patch directories + +If you're not playing PSO Blue Burst on newserv, you can skip these steps. + +newserv implements a patch server for PSO PC and PSO BB game data. Any file or directory you put in the system/patch-bb or system/patch-pc directories will be synced to clients when they connect to the patch server. + +For BB clients, newserv reads some files out of the patch data to implement game logic, so it's important that certain game files are synchronized between the server and the client. newserv contains defaults for these files in the system/blueburst/map directory, but if these don't match the client's copies of the files, odd behavior will occur in games. + +Specifically, the patch-bb directory should contain at least the data.gsl file and all map_*.dat files from the version of PSOBB that you want to play on newserv. You can copy these files out of the client's data directory from a clean installation, and put them in system/patch-bb/data. + +### Memory patches and DOL files Everything in this section requires resource_dasm to be installed, so newserv can use the PowerPC assembler and disassembler from its libresource_file library. If resource_dasm is not installed, newserv will still build and run, but these features will not be available. -You can put patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a patch itself, but it describes how patches may be written and the restrictions that apply to them. +You can put memory patches in the system/ppc directory with filenames like PatchName.patch.s and they will appear in the Patches menu for PSO GC clients that support patching. Memory patches are written in PowerPC assembly and are compiled when newserv is started. The PowerPC assembly system's features are documented in the comments in system/ppc/WriteMemory.s - this file is not a memory patch itself, but it describes how memory patches may be written and the restrictions that apply to them. You can also put DOL files in the system/dol directory, and they will appear in the Programs menu. Selecting a DOL file there will load the file into the GameCube's memory and run it, just like the old homebrew loaders (PSUL and PSOload) did. For this to work, ReadMemoryWord.s, WriteMemory.s, and RunDOL.s must be present in the system/ppc directory. This has been tested on Dolphin but not on a real GameCube, so results may vary. diff --git a/src/GSLArchive.cc b/src/GSLArchive.cc new file mode 100644 index 00000000..0104dffb --- /dev/null +++ b/src/GSLArchive.cc @@ -0,0 +1,67 @@ +#include "GSLArchive.hh" + +#include +#include +#include + +#include "Text.hh" + +using namespace std; + + + +// TODO: Support big-endian GSLs also (e.g. from PSO GC) + +struct GSLHeaderEntry { + ptext filename; + le_uint32_t offset; // In pages, so actual offset is this * 0x800 + le_uint32_t size; + uint64_t unused; +}; + +GSLArchive::GSLArchive(shared_ptr data) : data(data) { + StringReader r(*this->data); + uint64_t min_data_offset = 0xFFFFFFFFFFFFFFFF; + while (r.where() < min_data_offset) { + const auto& entry = r.get(); + if (entry.filename.len() == 0) { + break; + } + uint64_t offset = static_cast(entry.offset) * 0x800; + if (offset + entry.size > this->data->size()) { + throw runtime_error("GSL entry extends beyond end of data"); + } + this->entries.emplace(entry.filename, Entry{offset, entry.size}); + } +} + +const unordered_map GSLArchive::all_entries() const { + return this->entries; +} + +pair GSLArchive::get(const std::string& name) const { + try { + const auto& entry = this->entries.at(name); + return make_pair(this->data->data() + entry.offset, entry.size); + } catch (const out_of_range&) { + throw out_of_range("GSL does not contain file: " + name); + } +} + +string GSLArchive::get_copy(const string& name) const { + try { + const auto& entry = this->entries.at(name); + return this->data->substr(entry.offset, entry.size); + } catch (const out_of_range&) { + throw out_of_range("GSL does not contain file: " + name); + } +} + +StringReader GSLArchive::get_reader(const string& name) const { + try { + const auto& entry = this->entries.at(name); + return StringReader(this->data->data() + entry.offset, entry.size); + } catch (const out_of_range&) { + throw out_of_range("GSL does not contain file: " + name); + } +} diff --git a/src/GSLArchive.hh b/src/GSLArchive.hh new file mode 100644 index 00000000..20936d16 --- /dev/null +++ b/src/GSLArchive.hh @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include +#include +#include + + + +class GSLArchive { +public: + GSLArchive(std::shared_ptr data); + ~GSLArchive() = default; + + struct Entry { + uint64_t offset; + uint32_t size; + }; + const std::unordered_map all_entries() const; + + std::pair get(const std::string& name) const; + std::string get_copy(const std::string& name) const; + StringReader get_reader(const std::string& name) const; + +private: + std::shared_ptr data; + + std::unordered_map entries; +}; diff --git a/src/LevelTable.cc b/src/LevelTable.cc index 6f6461b4..3547571f 100644 --- a/src/LevelTable.cc +++ b/src/LevelTable.cc @@ -10,24 +10,24 @@ using namespace std; -LevelTable::LevelTable(const string& filename, bool compressed) { - string data = load_file(filename); +LevelTable::LevelTable(shared_ptr data, bool compressed) { if (compressed) { - data = prs_decompress(data); + this->data.reset(new string(prs_decompress(*data))); + } else { + this->data = data; } - if (data.size() < sizeof(*this)) { + if (this->data->size() < sizeof(Table)) { throw invalid_argument("level table size is incorrect"); } - - memcpy(this, data.data(), sizeof(*this)); + this->table = reinterpret_cast(this->data->data()); } const PlayerStats& LevelTable::base_stats_for_class(uint8_t char_class) const { if (char_class >= 12) { throw out_of_range("invalid character class"); } - return this->base_stats[char_class]; + return this->table->base_stats[char_class]; } const LevelTable::LevelStats& LevelTable::stats_for_level(uint8_t char_class, @@ -38,7 +38,7 @@ const LevelTable::LevelStats& LevelTable::stats_for_level(uint8_t char_class, if (level >= 200) { throw invalid_argument("invalid character level"); } - return this->levels[char_class][level]; + return this->table->levels[char_class][level]; } void LevelTable::LevelStats::apply(PlayerStats& ps) const { diff --git a/src/LevelTable.hh b/src/LevelTable.hh index 9a52d6e7..82c800e1 100644 --- a/src/LevelTable.hh +++ b/src/LevelTable.hh @@ -19,7 +19,8 @@ struct PlayerStats { PlayerStats() noexcept; } __attribute__((packed)); -struct LevelTable { // from PlyLevelTbl.prs +class LevelTable { // from PlyLevelTbl.prs +public: struct LevelStats { uint8_t atp; uint8_t mst; @@ -34,12 +35,18 @@ struct LevelTable { // from PlyLevelTbl.prs void apply(PlayerStats& ps) const; } __attribute__((packed)); - PlayerStats base_stats[12]; - le_uint32_t unknown[12]; - LevelStats levels[12][200]; + struct Table { + PlayerStats base_stats[12]; + le_uint32_t unknown[12]; + LevelStats levels[12][200]; + } __attribute__((packed)); - LevelTable(const std::string& filename, bool compressed); + LevelTable(std::shared_ptr data, bool compressed); const PlayerStats& base_stats_for_class(uint8_t char_class) const; const LevelStats& stats_for_level(uint8_t char_class, uint8_t level) const; -} __attribute__((packed)); + +private: + std::shared_ptr data; + const Table* table; +}; diff --git a/src/Lobby.hh b/src/Lobby.hh index 49225e49..13499334 100644 --- a/src/Lobby.hh +++ b/src/Lobby.hh @@ -54,7 +54,6 @@ struct Lobby { uint8_t area; }; std::vector enemies; - std::shared_ptr rare_item_set; std::array next_item_id; uint32_t next_game_item_id; PlayerInventoryItem next_drop_item; diff --git a/src/Main.cc b/src/Main.cc index 393a7016..0cbcc174 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -252,6 +252,9 @@ Specifically:\n\ session. This is used for regression testing, to make sure client\n\ sessions are repeatable and code changes don\'t affect existing (working)\n\ functionality.\n\ + --extract-gsl=FILENAME\n\ + This option makes newserv extract all files from a GSL archive and place\n\ + them in the current directory.\n\ \n\ A few options apply to multiple modes described above:\n\ --parse-data\n\ @@ -268,6 +271,7 @@ enum class Behavior { ENCRYPT_DATA, DECODE_QUEST_FILE, DECODE_SJIS, + EXTRACT_GSL, REPLAY_LOG, CAT_CLIENT, }; @@ -289,6 +293,7 @@ int main(int argc, char** argv) { bool parse_data = false; bool byteswap_data = false; const char* replay_log_filename = nullptr; + const char* extract_gsl_filename = nullptr; const char* replay_required_access_key = ""; const char* replay_required_password = ""; struct sockaddr_storage cat_client_remote; @@ -340,6 +345,9 @@ int main(int argc, char** argv) { } else if (!strncmp(argv[x], "--replay-log=", 13)) { behavior = Behavior::REPLAY_LOG; replay_log_filename = &argv[x][13]; + } else if (!strncmp(argv[x], "--extract-gsl=", 14)) { + behavior = Behavior::EXTRACT_GSL; + extract_gsl_filename = &argv[x][14]; } else if (!strncmp(argv[x], "--require-password=", 19)) { replay_required_password = &argv[x][19]; } else if (!strncmp(argv[x], "--require-access-key=", 21)) { @@ -438,6 +446,17 @@ int main(int argc, char** argv) { break; } + case Behavior::EXTRACT_GSL: { + shared_ptr data(new string(load_file(extract_gsl_filename))); + GSLArchive gsl(data); + for (const auto& entry_it : gsl.all_entries()) { + auto e = gsl.get(entry_it.first); + save_file(entry_it.first, e.first, e.second); + fprintf(stderr, "... %s\n", entry_it.first.c_str()); + } + break; + } + case Behavior::CAT_CLIENT: { shared_ptr key; if (cli_version == GameVersion::BB) { @@ -483,11 +502,42 @@ int main(int argc, char** argv) { state->license_manager.reset(new LicenseManager()); } + if (isdir("system/patch-pc")) { + config_log.info("Indexing PSO PC patch files"); + state->pc_patch_file_index.reset(new PatchFileIndex("system/patch-pc")); + } else { + config_log.info("PSO PC patch files not present"); + } + if (isdir("system/patch-bb")) { + config_log.info("Indexing PSO BB patch files"); + state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb")); + try { + auto gsl_file = state->bb_patch_file_index->get("./data/data.gsl"); + state->bb_data_gsl.reset(new GSLArchive(gsl_file->data)); + config_log.info("data.gsl found in BB patch files"); + } catch (const out_of_range&) { + config_log.info("data.gsl is not present in BB patch files"); + } + } else { + config_log.info("PSO BB patch files not present"); + } + config_log.info("Loading battle parameters"); - state->battle_params.reset(new BattleParamsIndex("system/blueburst/BattleParamEntry")); + state->battle_params.reset(new BattleParamsIndex( + state->load_bb_file("BattleParamEntry_on.dat"), + state->load_bb_file("BattleParamEntry_lab_on.dat"), + state->load_bb_file("BattleParamEntry_ep4_on.dat"), + state->load_bb_file("BattleParamEntry.dat"), + state->load_bb_file("BattleParamEntry_lab.dat"), + state->load_bb_file("BattleParamEntry_ep4.dat"))); config_log.info("Loading level table"); - state->level_table.reset(new LevelTable("system/blueburst/PlyLevelTbl.prs", true)); + state->level_table.reset(new LevelTable( + state->load_bb_file("PlyLevelTbl.prs"), true)); + + config_log.info("Loading rare table"); + state->rare_item_set.reset(new RareItemSet( + state->load_bb_file("ItemRT.rel"))); config_log.info("Collecting Episode 3 data"); state->ep3_data_index.reset(new Ep3DataIndex("system/ep3")); @@ -505,19 +555,6 @@ int main(int argc, char** argv) { state->dol_file_index.reset(new DOLFileIndex()); } - if (isdir("system/patch-pc")) { - config_log.info("Indexing PSO PC patch files"); - state->pc_patch_file_index.reset(new PatchFileIndex("system/patch-pc")); - } else { - config_log.info("PSO PC patch files not present"); - } - if (isdir("system/patch-bb")) { - config_log.info("Indexing PSO BB patch files"); - state->bb_patch_file_index.reset(new PatchFileIndex("system/patch-bb")); - } else { - config_log.info("PSO BB patch files not present"); - } - config_log.info("Creating menus"); state->create_menus(config_json); diff --git a/src/Map.cc b/src/Map.cc index b0b8a39d..dc6941d3 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -10,36 +10,35 @@ using namespace std; -BattleParamsIndex::BattleParamsIndex(const char* prefix) { +BattleParamsIndex::BattleParamsIndex( + shared_ptr data_on_ep1, + shared_ptr data_on_ep2, + shared_ptr data_on_ep4, + shared_ptr data_off_ep1, + shared_ptr data_off_ep2, + shared_ptr data_off_ep4) { + this->files[0][0].data = data_on_ep1; + this->files[0][1].data = data_on_ep2; + this->files[0][2].data = data_on_ep4; + this->files[1][0].data = data_off_ep1; + this->files[1][1].data = data_off_ep2; + this->files[1][2].data = data_off_ep4; + for (uint8_t is_solo = 0; is_solo < 2; is_solo++) { for (uint8_t episode = 0; episode < 3; episode++) { - string filename = prefix; - if (episode == 1) { - filename += "_lab"; - } else if (episode == 2) { - filename += "_ep4"; + auto& file = this->files[is_solo][episode]; + if (file.data->size() < sizeof(Table)) { + throw runtime_error(string_printf( + "battle params table size is incorrect (expected %zX bytes, have %zX bytes; is_solo=%hhu, episode=%hhu)", + sizeof(Table), file.data->size(), is_solo, episode)); } - if (!is_solo) { - filename += "_on"; - } - filename += ".dat"; - - this->entries[is_solo][episode][0].reset(new TableT()); - this->entries[is_solo][episode][1].reset(new TableT()); - this->entries[is_solo][episode][2].reset(new TableT()); - this->entries[is_solo][episode][3].reset(new TableT()); - - scoped_fd fd(filename, O_RDONLY); - readx(fd, this->entries[is_solo][episode][0].get(), sizeof(TableT)); - readx(fd, this->entries[is_solo][episode][1].get(), sizeof(TableT)); - readx(fd, this->entries[is_solo][episode][2].get(), sizeof(TableT)); - readx(fd, this->entries[is_solo][episode][3].get(), sizeof(TableT)); + file.table = reinterpret_cast(file.data->data()); } } } -const BattleParams& BattleParamsIndex::get(bool solo, uint8_t episode, - uint8_t difficulty, uint8_t monster_type) const { +const BattleParamsIndex::Entry& BattleParamsIndex::get( + bool solo, uint8_t episode, uint8_t difficulty, uint8_t monster_type) const { if (episode > 3) { throw invalid_argument("incorrect episode"); } @@ -49,19 +48,7 @@ const BattleParams& BattleParamsIndex::get(bool solo, uint8_t episode, if (monster_type > 0x60) { throw invalid_argument("incorrect monster type"); } - return (*this->entries[!!solo][episode][difficulty])[monster_type]; -} - -shared_ptr -BattleParamsIndex::get_subtable( - bool solo, uint8_t episode, uint8_t difficulty) const { - if (episode > 3) { - throw invalid_argument("incorrect episode"); - } - if (difficulty > 4) { - throw invalid_argument("incorrect difficulty"); - } - return this->entries[!!solo][episode][difficulty]; + return this->files[!!solo][episode].table->difficulty[difficulty][monster_type]; } @@ -102,16 +89,16 @@ struct EnemyEntry { static uint64_t next_enemy_id = 1; vector parse_map( + shared_ptr battle_params, + bool is_solo, uint8_t episode, uint8_t difficulty, - shared_ptr battle_params_table, - const void* data, - size_t size, + shared_ptr data, bool alt_enemies) { - const auto* map = reinterpret_cast(data); - size_t entry_count = size / sizeof(EnemyEntry); - if (size != entry_count * sizeof(EnemyEntry)) { + const auto* map = reinterpret_cast(data->data()); + size_t entry_count = data->size() / sizeof(EnemyEntry); + if (data->size() != entry_count * sizeof(EnemyEntry)) { throw runtime_error("data size is not a multiple of entry size"); } @@ -122,7 +109,10 @@ vector parse_map( } }; - const auto& battle_params = *battle_params_table; + auto get_battle_params = [&](uint8_t type) -> const BattleParamsIndex::Entry& { + return battle_params->get(is_solo, episode, difficulty, type); + }; + for (size_t y = 0; y < entry_count; y++) { const auto& e = map[y]; size_t num_clones = e.num_clones; @@ -130,101 +120,101 @@ vector parse_map( switch (e.base) { case 0x40: // Hildebear and Hildetorr enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x49 + (e.skin & 0x01)].experience, + get_battle_params(0x49 + (e.skin & 0x01)).experience, 0x01 + (e.skin & 0x01)); break; case 0x41: // Rappies if (episode == 3) { // Del Rappy and Sand Rappy if (alt_enemies) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x17 + (e.skin & 0x01)].experience, + get_battle_params(0x17 + (e.skin & 0x01)).experience, 17 + (e.skin & 0x01)); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x05 + (e.skin & 0x01)].experience, + get_battle_params(0x05 + (e.skin & 0x01)).experience, 17 + (e.skin & 0x01)); } } else { // Rag Rappy and Al Rappy (Love for Episode II) if (e.skin & 0x01) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x18 + (e.skin & 0x01)].experience, + get_battle_params(0x18 + (e.skin & 0x01)).experience, 0xFF); // Don't know (yet) which rare Rappy it is } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x18 + (e.skin & 0x01)].experience, + get_battle_params(0x18 + (e.skin & 0x01)).experience, 5); } } break; case 0x42: // Monest + 30 Mothmants - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x01].experience, 4); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x01).experience, 4); for (size_t x = 0; x < 30; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x00].experience, 3); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x00).experience, 3); } break; case 0x43: // Savage Wolf and Barbarous Wolf enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x02 + ((e.reserved[10] & 0x800000) ? 1 : 0)].experience, + get_battle_params(0x02 + ((e.reserved[10] & 0x800000) ? 1 : 0)).experience, 7 + ((e.reserved[10] & 0x800000) ? 1 : 0)); break; case 0x44: // Booma family enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x4B + (e.skin % 3)].experience, + get_battle_params(0x4B + (e.skin % 3)).experience, 9 + (e.skin % 3)); break; case 0x60: // Grass Assassin - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x4E].experience, 12); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x4E).experience, 12); break; case 0x61: // Del Lily, Poison Lily, Nar Lily if ((episode == 2) && (alt_enemies)) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x25].experience, 83); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x25).experience, 83); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x04 + ((e.reserved[10] & 0x800000) ? 1 : 0)].experience, + get_battle_params(0x04 + ((e.reserved[10] & 0x800000) ? 1 : 0)).experience, 13 + ((e.reserved[10] & 0x800000) ? 1 : 0)); } break; case 0x62: // Nano Dragon - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1A].experience, 15); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1A).experience, 15); break; case 0x63: // Shark family enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x4F + (e.skin % 3)].experience, + get_battle_params(0x4F + (e.skin % 3)).experience, 16 + (e.skin % 3)); break; case 0x64: // Slime + 4 clones enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x2F + ((e.reserved[10] & 0x800000) ? 0 : 1)].experience, + get_battle_params(0x2F + ((e.reserved[10] & 0x800000) ? 0 : 1)).experience, 19 + ((e.reserved[10] & 0x800000) ? 1 : 0)); for (size_t x = 0; x < 4; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x30].experience, 19); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x30).experience, 19); } break; case 0x65: // Pan Arms, Migium, Hidoom for (size_t x = 0; x < 3; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x31 + x].experience, 21 + x); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x31 + x).experience, 21 + x); } break; case 0x80: // Dubchic and Gillchic if (e.skin & 0x01) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x1B + (e.skin & 0x01)].experience, 50); + get_battle_params(0x1B + (e.skin & 0x01)).experience, 50); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x1B + (e.skin & 0x01)].experience, 24); + get_battle_params(0x1B + (e.skin & 0x01)).experience, 24); } break; case 0x81: // Garanz - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1D].experience, 25); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1D).experience, 25); break; case 0x82: // Sinow Beat and Gold if (e.reserved[10] & 0x800000) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x13].experience, + get_battle_params(0x13).experience, 26 + ((e.reserved[10] & 0x800000) ? 1 : 0)); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x06].experience, + get_battle_params(0x06).experience, 26 + ((e.reserved[10] & 0x800000) ? 1 : 0)); } if (e.num_clones == 0) { @@ -232,193 +222,193 @@ vector parse_map( } break; case 0x83: // Canadine - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x07].experience, 28); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x07).experience, 28); break; case 0x84: // Canadine Group - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x09].experience, 29); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x09).experience, 29); for (size_t x = 0; x < 8; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x08].experience, 28); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x08).experience, 28); } break; case 0x85: // Dubwitch break; case 0xA0: // Delsaber - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x52].experience, 30); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x52).experience, 30); break; case 0xA1: // Chaos Sorcerer + 2 Bits - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0A].experience, 31); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0A).experience, 31); create_clones(2); break; case 0xA2: // Dark Gunner - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1E].experience, 34); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1E).experience, 34); break; case 0xA4: // Chaos Bringer - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0D].experience, 36); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0D).experience, 36); break; case 0xA5: // Dark Belra - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0E].experience, 37); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0E).experience, 37); break; case 0xA6: // Dimenian family enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x53 + (e.skin % 3)].experience, 41 + (e.skin % 3)); + get_battle_params(0x53 + (e.skin % 3)).experience, 41 + (e.skin % 3)); break; case 0xA7: // Bulclaw + 4 claws - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1F].experience, 40); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1F).experience, 40); for (size_t x = 0; x < 4; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x20].experience, 38); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x20).experience, 38); } break; case 0xA8: // Claw - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x20].experience, 38); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x20).experience, 38); break; case 0xC0: // Dragon or Gal Gryphon if (episode == 1) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x12].experience, 44); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x12).experience, 44); } else if (episode == 2) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1E].experience, 77); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1E).experience, 77); } break; case 0xC1: // De Rol Le - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0F].experience, 45); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0F).experience, 45); break; case 0xC2: // Vol Opt form 1 break; case 0xC5: // Vol Opt form 2 - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x25].experience, 46); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x25).experience, 46); break; case 0xC8: // Dark Falz + 510 Helpers if (difficulty) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x38].experience, 47); // Final form + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x38).experience, 47); // Final form } else { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x37].experience, 47); // Second form + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x37).experience, 47); // Second form } for (size_t x = 0; x < 510; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x35].experience, 0); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x35).experience, 0); } break; case 0xCA: // Olga Flow - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x2C].experience, 78); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x2C).experience, 78); create_clones(0x200); break; case 0xCB: // Barba Ray - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0F].experience, 73); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0F).experience, 73); create_clones(0x2F); break; case 0xCC: // Gol Dragon - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x12].experience, 76); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x12).experience, 76); create_clones(5); break; case 0xD4: // Sinows Berill & Spigell enemies.emplace_back(next_enemy_id++, e.base, - battle_params[(e.reserved[10] & 0x800000) ? 0x13 : 0x06].experience, + get_battle_params((e.reserved[10] & 0x800000) ? 0x13 : 0x06).experience, 62 + ((e.reserved[10] & 0x800000) ? 1 : 0)); create_clones(4); break; case 0xD5: // Merillia & Meriltas enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x4B + (e.skin & 0x01)].experience, + get_battle_params(0x4B + (e.skin & 0x01)).experience, 52 + (e.skin & 0x01)); break; case 0xD6: // Mericus, Merikle, & Mericarol if (e.skin) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x44 + (e.skin % 3)].experience, 56 + (e.skin % 3)); + get_battle_params(0x44 + (e.skin % 3)).experience, 56 + (e.skin % 3)); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x3A].experience, 56 + (e.skin % 3)); + get_battle_params(0x3A).experience, 56 + (e.skin % 3)); } break; case 0xD7: // Ul Gibbon and Zol Gibbon enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x3B + (e.skin & 0x01)].experience, + get_battle_params(0x3B + (e.skin & 0x01)).experience, 59 + (e.skin & 0x01)); break; case 0xD8: // Gibbles - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x3D].experience, 61); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x3D).experience, 61); break; case 0xD9: // Gee - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x07].experience, 54); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x07).experience, 54); break; case 0xDA: // Gi Gue - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1A].experience, 55); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1A).experience, 55); break; case 0xDB: // Deldepth - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x30].experience, 71); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x30).experience, 71); break; case 0xDC: // Delbiter - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x0D].experience, 72); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x0D).experience, 72); break; case 0xDD: // Dolmolm and Dolmdarl enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x4F + (e.skin & 0x01)].experience, + get_battle_params(0x4F + (e.skin & 0x01)).experience, 64 + (e.skin & 0x01)); break; case 0xDE: // Morfos - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x40].experience, 66); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x40).experience, 66); break; case 0xDF: // Recobox & Recons - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x41].experience, 67); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x41).experience, 67); for (size_t x = 0; x < e.num_clones; x++) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x42].experience, 68); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x42).experience, 68); } break; case 0xE0: // Epsilon, Sinow Zoa and Zele if ((episode == 2) && (alt_enemies)) { - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x23].experience, 84); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x23).experience, 84); create_clones(4); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x43 + (e.skin & 0x01)].experience, + get_battle_params(0x43 + (e.skin & 0x01)).experience, 69 + (e.skin & 0x01)); } break; case 0xE1: // Ill Gill - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x26].experience, 82); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x26).experience, 82); break; case 0x0110: // Astark - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x09].experience, 1); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x09).experience, 1); break; case 0x0111: // Satellite Lizard and Yowie enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x0D + ((e.reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)].experience, + get_battle_params(0x0D + ((e.reserved[10] & 0x800000) ? 1 : 0) + (alt_enemies ? 0x10 : 0)).experience, 2 + ((e.reserved[10] & 0x800000) ? 0 : 1)); break; case 0x0112: // Merissa A/AA enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x19 + (e.skin & 0x01)].experience, + get_battle_params(0x19 + (e.skin & 0x01)).experience, 4 + (e.skin & 0x01)); break; case 0x0113: // Girtablulu - enemies.emplace_back(next_enemy_id++, e.base, battle_params[0x1F].experience, 6); + enemies.emplace_back(next_enemy_id++, e.base, get_battle_params(0x1F).experience, 6); break; case 0x0114: // Zu and Pazuzu enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x0B + (e.skin & 0x01) + (alt_enemies ? 0x14: 0x00)].experience, + get_battle_params(0x0B + (e.skin & 0x01) + (alt_enemies ? 0x14: 0x00)).experience, 7 + (e.skin & 0x01)); break; case 0x0115: // Boota family if (e.skin & 2) { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x03].experience, 9 + (e.skin % 3)); + get_battle_params(0x03).experience, 9 + (e.skin % 3)); } else { enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x00 + (e.skin % 3)].experience, + get_battle_params(0x00 + (e.skin % 3)).experience, 9 + (e.skin % 3)); } break; case 0x0116: // Dorphon and Eclair enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x0F + (e.skin & 0x01)].experience, + get_battle_params(0x0F + (e.skin & 0x01)).experience, 12 + (e.skin & 0x01)); break; case 0x0117: // Goran family enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x11 + (e.skin % 3)].experience, + get_battle_params(0x11 + (e.skin % 3)).experience, (e.skin & 2) ? 15 : ((e.skin & 1) ? 16 : 14)); break; case 0x0119: // Saint Million, Shambertin, Kondrieu enemies.emplace_back(next_enemy_id++, e.base, - battle_params[0x22].experience, + get_battle_params(0x22).experience, (e.reserved[10] & 0x800000) ? 21 : (19 + (e.skin & 0x01))); break; default: @@ -433,3 +423,269 @@ vector parse_map( return enemies; } + + + +SetDataTable::SetDataTable(shared_ptr data, bool big_endian) { + if (big_endian) { + this->load_table_t(data); + } else { + this->load_table_t(data); + } +} + +template +void SetDataTable::load_table_t(shared_ptr data) { + StringReader r(*data); + + struct Footer { + U32T table3_offset; + U32T table3_count; // In le_uint16_ts (so *2 for size in bytes) + U32T unknown_a3; // == 1 + U32T unknown_a4; // == 0 + U32T root_table_offset_offset; + U32T unknown_a6; // == 0 + U32T unknown_a7; // == 0 + U32T unknown_a8; // == 0 + } __attribute__((packed)); + if (r.size() < sizeof(Footer)) { + throw runtime_error("set data table is too small"); + } + auto& footer = r.pget