From 42e927caa92e9ad9bdbc32b26c1bd1f02d213989 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 19 Nov 2023 23:06:30 -0800 Subject: [PATCH] add basic quest map disassembler --- src/Main.cc | 27 ++++++- src/Map.cc | 215 ++++++++++++++++++++++++++++++++++++++++------------ src/Map.hh | 15 ++++ 3 files changed, 207 insertions(+), 50 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index 3b8dd0fa..c6de4946 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -214,6 +214,10 @@ The actions are:\n\ of the commands and metadata it contains. Specify the quest\'s game version\n\ with one of the --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --gc-ep3,\n\ --xb, or --bb options.\n\ + disassemble-quest-map [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + Disassemble the input quest map (.dat file) into a text representation of\n\ + the data it contains. Specify the quest\'s game version with one of the\n\ + --dc-nte, --dc-v1, --dc-v2, --pc, --gc-nte, --gc, --xb, or --bb options.\n\ cat-client ADDR:PORT\n\ Connect to the given server and simulate a PSO client. newserv will then\n\ print all the received commands to stdout, and forward any commands typed\n\ @@ -322,6 +326,7 @@ enum class Behavior { DECODE_QUEST_FILE, ENCODE_QST, DISASSEMBLE_QUEST_SCRIPT, + DISASSEMBLE_QUEST_MAP, EXTRACT_AFS, EXTRACT_GSL, EXTRACT_BML, @@ -375,6 +380,7 @@ static bool behavior_takes_input_filename(Behavior b) { (b == Behavior::DECODE_QUEST_FILE) || (b == Behavior::ENCODE_QST) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || + (b == Behavior::DISASSEMBLE_QUEST_MAP) || (b == Behavior::CONVERT_RARE_ITEM_SET) || (b == Behavior::EXTRACT_AFS) || (b == Behavior::EXTRACT_GSL) || @@ -416,6 +422,7 @@ static bool behavior_takes_output_filename(Behavior b) { (b == Behavior::ENCODE_GVM) || (b == Behavior::ENCODE_QST) || (b == Behavior::DISASSEMBLE_QUEST_SCRIPT) || + (b == Behavior::DISASSEMBLE_QUEST_MAP) || (b == Behavior::CONVERT_RARE_ITEM_SET) || (b == Behavior::EXTRACT_AFS) || (b == Behavior::EXTRACT_GSL) || @@ -622,6 +629,8 @@ int main(int argc, char** argv) { behavior = Behavior::ENCODE_QST; } else if (!strcmp(argv[x], "disassemble-quest-script")) { behavior = Behavior::DISASSEMBLE_QUEST_SCRIPT; + } else if (!strcmp(argv[x], "disassemble-quest-map")) { + behavior = Behavior::DISASSEMBLE_QUEST_MAP; } else if (!strcmp(argv[x], "cat-client")) { behavior = Behavior::CAT_CLIENT; } else if (!strcmp(argv[x], "convert-rare-item-set")) { @@ -737,14 +746,14 @@ int main(int argc, char** argv) { filename += ".gvm"; } else if ((behavior == Behavior::DECODE_TEXT_ARCHIVE) || (behavior == Behavior::DECODE_UNICODE_TEXT_SET)) { filename += ".json"; - } else if (behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) { + } else if ((behavior == Behavior::DISASSEMBLE_QUEST_SCRIPT) || (behavior == Behavior::DISASSEMBLE_QUEST_MAP)) { filename += ".txt"; } else { filename += ".dec"; } save_file(filename, data, size); - } else if (isatty(fileno(stdout)) && (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT)) { + } else if (isatty(fileno(stdout)) && (behavior != Behavior::DISASSEMBLE_QUEST_SCRIPT) && (behavior != Behavior::DISASSEMBLE_QUEST_MAP)) { // 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); @@ -1411,6 +1420,20 @@ int main(int argc, char** argv) { break; } + case Behavior::DISASSEMBLE_QUEST_MAP: { + if (!input_filename || !strcmp(input_filename, "-")) { + throw invalid_argument("an input filename is required"); + } + + string data = read_input_data(); + if (!expect_decompressed) { + data = prs_decompress(data); + } + string result = Map::disassemble_quest_data(data.data(), data.size()); + write_output_data(result.data(), result.size()); + break; + } + case Behavior::EXTRACT_AFS: case Behavior::EXTRACT_GSL: case Behavior::EXTRACT_BML: { diff --git a/src/Map.cc b/src/Map.cc index f86ba357..d0885a01 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -14,6 +14,59 @@ using namespace std; static constexpr float UINT32_MAX_AS_FLOAT = 4294967296.0f; +string Map::ObjectEntry::str() const { + return string_printf("[ObjectEntry type=%04hX flags=%04hX index=%04hX a2=%04hX entity_id=%04hX group=%04hX section=%04hX a3=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %08" PRIX32 " %08" PRIX32 " %08" PRIX32 "] unused=%08" PRIX32 "]", + this->base_type.load(), + this->flags.load(), + this->index.load(), + this->unknown_a2.load(), + this->entity_id.load(), + this->group.load(), + this->section.load(), + this->unknown_a3.load(), + this->x.load(), + this->y.load(), + this->z.load(), + this->x_angle.load(), + this->y_angle.load(), + this->z_angle.load(), + this->param1.load(), + this->param2.load(), + this->param3.load(), + this->param4.load(), + this->param5.load(), + this->param6.load(), + this->unused.load()); +} + +string Map::EnemyEntry::str() const { + return string_printf("[EnemyEntry type=%04hX flags=%04hX index=%04hX num_children=%04hX floor=%04hX entity_id=%04hX section=%04hX wave_number=%04hX wave_number2=%04hX a1=%04hX x=%g y=%g z=%g x_angle=%08" PRIX32 " y_angle=%08" PRIX32 " z_angle=%08" PRIX32 " params=[%g %g %g %g %g %04hX %04hX] unused=%08" PRIX32 "]", + this->base_type.load(), + this->flags.load(), + this->index.load(), + this->num_children.load(), + this->floor.load(), + this->entity_id.load(), + this->section.load(), + this->wave_number.load(), + this->wave_number2.load(), + this->unknown_a1.load(), + this->x.load(), + this->y.load(), + this->z.load(), + this->x_angle.load(), + this->y_angle.load(), + this->z_angle.load(), + this->fparam1.load(), + this->fparam2.load(), + this->fparam3.load(), + this->fparam4.load(), + this->fparam5.load(), + this->uparam1.load(), + this->uparam2.load(), + this->unused.load()); +} + Map::Enemy::Enemy(uint8_t floor, EnemyType type) : type(type), floor(floor), @@ -676,24 +729,8 @@ void Map::add_random_enemies_from_map_data( } } -void Map::add_enemies_and_objects_from_quest_data( - Episode episode, - uint8_t difficulty, - uint8_t event, - const void* data, - size_t size, - uint32_t rare_seed, - const RareEnemyRates& rare_rates) { - - struct DATSectionsForFloor { - uint32_t objects = 0xFFFFFFFF; - uint32_t enemies = 0xFFFFFFFF; - uint32_t wave_events = 0xFFFFFFFF; - uint32_t random_enemy_locations = 0xFFFFFFFF; - uint32_t random_enemy_definitions = 0xFFFFFFFF; - }; - - vector floor_sections; +vector Map::collect_quest_map_data_sections(const void* data, size_t size) { + vector ret; StringReader r(data, size); while (!r.eof()) { size_t header_offset = r.where(); @@ -712,61 +749,74 @@ void Map::add_enemies_and_objects_from_quest_data( throw runtime_error("section floor number too large"); } - if (header.floor >= floor_sections.size()) { - floor_sections.resize(header.floor + 1); + if (header.floor >= ret.size()) { + ret.resize(header.floor + 1); } - auto& sections = floor_sections[header.floor]; + auto& floor_sections = ret[header.floor]; switch (header.type()) { case SectionHeader::Type::OBJECTS: - if (sections.objects != 0xFFFFFFFF) { + if (floor_sections.objects != 0xFFFFFFFF) { throw runtime_error("multiple objects sections for same floor"); } - sections.objects = header_offset; + floor_sections.objects = header_offset; break; case SectionHeader::Type::ENEMIES: - if (sections.enemies != 0xFFFFFFFF) { + if (floor_sections.enemies != 0xFFFFFFFF) { throw runtime_error("multiple enemies sections for same floor"); } - sections.enemies = header_offset; + floor_sections.enemies = header_offset; break; case SectionHeader::Type::WAVE_EVENTS: - if (sections.wave_events != 0xFFFFFFFF) { + if (floor_sections.wave_events != 0xFFFFFFFF) { throw runtime_error("multiple wave events sections for same floor"); } - sections.wave_events = header_offset; + floor_sections.wave_events = header_offset; break; case SectionHeader::Type::RANDOM_ENEMY_LOCATIONS: - if (sections.random_enemy_locations != 0xFFFFFFFF) { + if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { throw runtime_error("multiple random enemy locations sections for same floor"); } - sections.random_enemy_locations = header_offset; + floor_sections.random_enemy_locations = header_offset; break; case SectionHeader::Type::RANDOM_ENEMY_DEFINITIONS: - if (sections.random_enemy_definitions != 0xFFFFFFFF) { + if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { throw runtime_error("multiple random enemy definitions sections for same floor"); } - sections.random_enemy_definitions = header_offset; + floor_sections.random_enemy_definitions = header_offset; break; default: throw runtime_error("invalid section type"); } r.skip(header.data_size); } + return ret; +} - for (size_t floor = 0; floor < floor_sections.size(); floor++) { - const auto& sections = floor_sections[floor]; +void Map::add_enemies_and_objects_from_quest_data( + Episode episode, + uint8_t difficulty, + uint8_t event, + const void* data, + size_t size, + uint32_t rare_seed, + const RareEnemyRates& rare_rates) { + auto all_floor_sections = this->collect_quest_map_data_sections(data, size); - if (sections.objects != 0xFFFFFFFF) { - const auto& header = r.pget(sections.objects); + StringReader r(data, size); + for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { + const auto& floor_sections = all_floor_sections[floor]; + + if (floor_sections.objects != 0xFFFFFFFF) { + const auto& header = r.pget(floor_sections.objects); if (header.data_size % sizeof(ObjectEntry)) { throw runtime_error("quest layout object section size is not a multiple of object entry size"); } static_game_data_log.info("(Floor %02zX) Adding objects", floor); - this->add_objects_from_map_data(floor, r.pgetv(sections.objects + sizeof(header), header.data_size), header.data_size); + this->add_objects_from_map_data(floor, r.pgetv(floor_sections.objects + sizeof(header), header.data_size), header.data_size); } - if (sections.enemies != 0xFFFFFFFF) { - const auto& header = r.pget(sections.enemies); + if (floor_sections.enemies != 0xFFFFFFFF) { + const auto& header = r.pget(floor_sections.enemies); if (header.data_size % sizeof(EnemyEntry)) { throw runtime_error("quest layout enemy section size is not a multiple of enemy entry size"); } @@ -776,31 +826,100 @@ void Map::add_enemies_and_objects_from_quest_data( difficulty, event, floor, - r.pgetv(sections.enemies + sizeof(header), header.data_size), + r.pgetv(floor_sections.enemies + sizeof(header), header.data_size), header.data_size, rare_rates); - } else if ((sections.wave_events != 0xFFFFFFFF) && - (sections.random_enemy_locations != 0xFFFFFFFF) && - (sections.random_enemy_definitions != 0xFFFFFFFF)) { + } else if ((floor_sections.wave_events != 0xFFFFFFFF) && + (floor_sections.random_enemy_locations != 0xFFFFFFFF) && + (floor_sections.random_enemy_definitions != 0xFFFFFFFF)) { static_game_data_log.info("(Floor %02zX) Adding random enemies", floor); - const auto& wave_events_header = r.pget(sections.wave_events); - const auto& random_enemy_locations_header = r.pget(sections.random_enemy_locations); - const auto& random_enemy_definitions_header = r.pget(sections.random_enemy_definitions); + const auto& wave_events_header = r.pget(floor_sections.wave_events); + const auto& random_enemy_locations_header = r.pget(floor_sections.random_enemy_locations); + const auto& random_enemy_definitions_header = r.pget(floor_sections.random_enemy_definitions); this->add_random_enemies_from_map_data( episode, difficulty, event, floor, - r.sub(sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size), - r.sub(sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size), - r.sub(sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), + r.sub(floor_sections.wave_events + sizeof(SectionHeader), wave_events_header.data_size), + r.sub(floor_sections.random_enemy_locations + sizeof(SectionHeader), random_enemy_locations_header.data_size), + r.sub(floor_sections.random_enemy_definitions + sizeof(SectionHeader), random_enemy_definitions_header.data_size), rare_seed, rare_rates); } } } +string Map::disassemble_quest_data(const void* data, size_t size) { + auto all_floor_sections = Map::collect_quest_map_data_sections(data, size); + + deque ret; + StringReader r(data, size); + size_t object_number = 0; + size_t enemy_number = 0; + for (size_t floor = 0; floor < all_floor_sections.size(); floor++) { + const auto& floor_sections = all_floor_sections[floor]; + + if (floor_sections.objects != 0xFFFFFFFF) { + ret.emplace_back(string_printf(".objects %zu", floor)); + const auto& header = r.pget(floor_sections.objects); + auto sub_r = r.sub(floor_sections.objects + sizeof(SectionHeader), header.data_size); + while (sub_r.remaining() >= sizeof(ObjectEntry)) { + string o_str = sub_r.get().str(); + ret.emplace_back(string_printf("/* K-%zX */ %s", object_number++, o_str.c_str())); + } + if (sub_r.remaining()) { + ret.emplace_back("// Warning: object section size is not a multiple of object entry size"); + size_t offset = floor_sections.objects + sizeof(SectionHeader) + r.where(); + size_t bytes = r.remaining(); + ret.emplace_back(format_data(r.getv(r.remaining()), bytes, offset)); + } + } + + if (floor_sections.enemies != 0xFFFFFFFF) { + ret.emplace_back(string_printf(".enemies %zu", floor)); + const auto& header = r.pget(floor_sections.enemies); + auto sub_r = r.sub(floor_sections.enemies + sizeof(SectionHeader), header.data_size); + while (sub_r.remaining() >= sizeof(EnemyEntry)) { + string e_str = sub_r.get().str(); + ret.emplace_back(string_printf("/* entry %zX */ %s", enemy_number++, e_str.c_str())); + } + if (sub_r.remaining()) { + ret.emplace_back("// Warning: enemy section size is not a multiple of enemy entry size"); + size_t offset = floor_sections.objects + sizeof(SectionHeader) + r.where(); + size_t bytes = r.remaining(); + ret.emplace_back(format_data(r.getv(r.remaining()), bytes, offset)); + } + } + + // TODO: Add disassembly for these section types + if (floor_sections.wave_events != 0xFFFFFFFF) { + ret.emplace_back(string_printf(".wave_events %zu", floor)); + const auto& header = r.pget(floor_sections.wave_events); + size_t offset = floor_sections.wave_events + sizeof(SectionHeader); + auto sub_r = r.sub(offset, header.data_size); + ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset)); + } + if (floor_sections.random_enemy_locations != 0xFFFFFFFF) { + ret.emplace_back(string_printf(".random_enemy_locations %zu", floor)); + const auto& header = r.pget(floor_sections.random_enemy_locations); + size_t offset = floor_sections.random_enemy_locations + sizeof(SectionHeader); + auto sub_r = r.sub(offset, header.data_size); + ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset)); + } + if (floor_sections.random_enemy_definitions != 0xFFFFFFFF) { + ret.emplace_back(string_printf(".random_enemy_definitions %zu", floor)); + const auto& header = r.pget(floor_sections.random_enemy_definitions); + size_t offset = floor_sections.random_enemy_definitions + sizeof(SectionHeader); + auto sub_r = r.sub(offset, header.data_size); + ret.emplace_back(format_data(r.getv(r.remaining()), header.data_size, offset)); + } + } + + return join(ret, "\n"); +} + SetDataTable::SetDataTable(shared_ptr data, bool big_endian) { if (big_endian) { this->load_table_t(data); diff --git a/src/Map.hh b/src/Map.hh index 212e5413..b08a4ed5 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -57,6 +57,8 @@ struct Map { /* 3C */ le_uint32_t param6; /* 40 */ le_uint32_t unused; // Reserved for pointer in client's memory; unused by server /* 44 */ + + std::string str() const; } __attribute__((packed)); struct EnemyEntry { // Section type 2 (ENEMIES) @@ -85,6 +87,8 @@ struct Map { /* 42 */ le_uint16_t uparam2; /* 44 */ le_uint32_t unused; // Reserved for pointer in client's memory; unused by server /* 48 */ + + std::string str() const; } __attribute__((packed)); struct EventsSectionHeader { // Section type 3 (WAVE_EVENTS) @@ -265,6 +269,15 @@ struct Map { uint32_t rare_seed, const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + struct DATSectionsForFloor { + uint32_t objects = 0xFFFFFFFF; + uint32_t enemies = 0xFFFFFFFF; + uint32_t wave_events = 0xFFFFFFFF; + uint32_t random_enemy_locations = 0xFFFFFFFF; + uint32_t random_enemy_definitions = 0xFFFFFFFF; + }; + static std::vector collect_quest_map_data_sections(const void* data, size_t size); + void add_enemies_and_objects_from_quest_data( Episode episode, uint8_t difficulty, @@ -273,6 +286,8 @@ struct Map { size_t size, uint32_t rare_seed, const RareEnemyRates& rare_rates = Map::DEFAULT_RARE_ENEMIES); + + static std::string disassemble_quest_data(const void* data, size_t size); }; // TODO: This class is currently unused. It would be nice if we could use this