add basic quest map disassembler

This commit is contained in:
Martin Michelsen
2023-11-19 23:06:30 -08:00
parent cd4c06231f
commit 42e927caa9
3 changed files with 207 additions and 50 deletions
+25 -2
View File
@@ -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: {
+167 -48
View File
@@ -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<DATSectionsForFloor> floor_sections;
vector<Map::DATSectionsForFloor> Map::collect_quest_map_data_sections(const void* data, size_t size) {
vector<DATSectionsForFloor> 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<SectionHeader>(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<SectionHeader>(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<SectionHeader>(sections.enemies);
if (floor_sections.enemies != 0xFFFFFFFF) {
const auto& header = r.pget<SectionHeader>(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<SectionHeader>(sections.wave_events);
const auto& random_enemy_locations_header = r.pget<SectionHeader>(sections.random_enemy_locations);
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(sections.random_enemy_definitions);
const auto& wave_events_header = r.pget<SectionHeader>(floor_sections.wave_events);
const auto& random_enemy_locations_header = r.pget<SectionHeader>(floor_sections.random_enemy_locations);
const auto& random_enemy_definitions_header = r.pget<SectionHeader>(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<string> 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<SectionHeader>(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<ObjectEntry>().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<SectionHeader>(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<EnemyEntry>().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<SectionHeader>(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<SectionHeader>(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<SectionHeader>(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<const string> data, bool big_endian) {
if (big_endian) {
this->load_table_t<true>(data);
+15
View File
@@ -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<DATSectionsForFloor> 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