fix multiple bugs in quest assembler

This commit is contained in:
Martin Michelsen
2025-11-28 12:41:07 -08:00
parent 3ef91b0159
commit 4fe225a302
4 changed files with 85 additions and 100 deletions
+67 -82
View File
@@ -1588,11 +1588,6 @@ Action a_encode_qst(
write_output_data(args, qst_data.data(), qst_data.size(), "qst");
});
Action a_check_quest_opcodes(
"check-quest-opcodes", nullptr,
+[](phosg::Arguments&) {
check_opcode_definitions();
});
Action a_disassemble_quest_script(
"disassemble-quest-script", "\
disassemble-quest-script [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\
@@ -3123,23 +3118,11 @@ Action a_print_free_supermap(
map_state.print(stdout);
});
Action a_check_quests(
Action a_check_quest_reassembly(
"check-quests", nullptr,
+[](phosg::Arguments& args) {
auto s = make_shared<ServerState>(get_config_filename(args));
s->is_debug = true;
s->load_config_early();
s->clear_file_caches();
s->load_patch_indexes();
s->load_set_data_tables();
s->load_maps();
s->load_quest_index(true);
phosg::fwrite_fmt(stdout, "All quests indexed\n");
});
check_quest_opcode_definitions();
Action a_check_quest_reassembly(
"check-quest-reassembly", nullptr,
+[](phosg::Arguments& args) {
auto s = make_shared<ServerState>(get_config_filename(args));
s->is_debug = true;
s->load_config_early();
@@ -3149,71 +3132,73 @@ Action a_check_quest_reassembly(
s->load_maps();
s->load_quest_index(true);
for (const auto& [_, q] : s->quest_index->quests_by_number) {
for (const auto& [_, vq] : q->versions) {
auto decompressed_bin = prs_decompress(*vq->bin_contents);
auto disassembled = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false);
auto reassembly = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, true, false);
string include_dir = phosg::dirname(vq->bin_filename());
AssembledQuestScript assembled;
try {
assembled = assemble_quest_script(
reassembly,
{"system/quests/includes"},
{"system/quests/includes", "system/client-functions/System"},
false);
if (vq->json_contents) {
assembled.meta.apply_json_overrides(*vq->json_contents);
if (args.get<bool>("reassembly")) {
for (const auto& [_, q] : s->quest_index->quests_by_number) {
for (const auto& [_, vq] : q->versions) {
auto decompressed_bin = prs_decompress(*vq->bin_contents);
auto disassembled = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, false, false);
auto reassembly = disassemble_quest_script(decompressed_bin.data(), decompressed_bin.size(), vq->meta.version, vq->meta.language, vq->map_file, true, false);
string include_dir = phosg::dirname(vq->bin_filename());
AssembledQuestScript assembled;
try {
assembled = assemble_quest_script(
reassembly,
{"system/quests/includes"},
{"system/quests/includes", "system/client-functions/System"},
false);
if (vq->json_contents) {
assembled.meta.apply_json_overrides(*vq->json_contents);
}
if (assembled.data != decompressed_bin) {
throw std::runtime_error("Reassembled quest script does not match original");
}
// Don't check quest number, since we override it based on the filename
if (assembled.meta.version != vq->meta.version) {
throw std::runtime_error(std::format("Reassembled quest version ({}) does not match original ({})",
phosg::name_for_enum(assembled.meta.version), phosg::name_for_enum(vq->meta.version)));
}
if (assembled.meta.language != vq->meta.language) {
throw std::runtime_error(std::format("Reassembled quest language ({}) does not match original ({})",
name_for_language(assembled.meta.language), name_for_language(vq->meta.language)));
}
if (assembled.meta.episode != vq->meta.episode) {
throw std::runtime_error(std::format("Reassembled quest episode ({}) does not match original ({})",
name_for_episode(assembled.meta.episode), name_for_episode(vq->meta.episode)));
}
if (assembled.meta.joinable != vq->meta.joinable) {
throw std::runtime_error(std::format("Reassembled quest joinable ({}) does not match original ({})",
assembled.meta.joinable, vq->meta.joinable));
}
if (assembled.meta.max_players != vq->meta.max_players) {
throw std::runtime_error(std::format("Reassembled quest max_players ({}) does not match original ({})",
assembled.meta.max_players, vq->meta.max_players));
}
if (assembled.meta.name != vq->meta.name) {
throw std::runtime_error(std::format("Reassembled quest name ({}) does not match original ({})",
assembled.meta.name, vq->meta.name));
}
if (assembled.meta.short_description != vq->meta.short_description) {
throw std::runtime_error(std::format("Reassembled quest short description ({}) does not match original ({})",
assembled.meta.short_description, vq->meta.short_description));
}
if (assembled.meta.long_description != vq->meta.long_description) {
throw std::runtime_error(std::format("Reassembled quest long description ({}) does not match original ({})",
assembled.meta.long_description, vq->meta.long_description));
}
} catch (const std::exception& e) {
phosg::log_error_f("================ DISASSEMBLY:");
phosg::fwritex(stderr, disassembled);
phosg::log_error_f("================ REASSEMBLY:");
phosg::fwritex(stderr, reassembly);
if (!assembled.data.empty()) {
phosg::log_error_f("================ BINDIFF:");
phosg::print_binary_diff(stderr, decompressed_bin.data(), decompressed_bin.size(), assembled.data.data(), assembled.data.size(), isatty(fileno(stderr)), 3, 0);
}
phosg::log_info_f("... {} {} {} ({}) FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name);
throw;
}
if (assembled.data != decompressed_bin) {
throw std::runtime_error("Reassembled quest script does not match original");
}
// Don't check quest number, since we override it based on the filename
if (assembled.meta.version != vq->meta.version) {
throw std::runtime_error(std::format("Reassembled quest version ({}) does not match original ({})",
phosg::name_for_enum(assembled.meta.version), phosg::name_for_enum(vq->meta.version)));
}
if (assembled.meta.language != vq->meta.language) {
throw std::runtime_error(std::format("Reassembled quest language ({}) does not match original ({})",
name_for_language(assembled.meta.language), name_for_language(vq->meta.language)));
}
if (assembled.meta.episode != vq->meta.episode) {
throw std::runtime_error(std::format("Reassembled quest episode ({}) does not match original ({})",
name_for_episode(assembled.meta.episode), name_for_episode(vq->meta.episode)));
}
if (assembled.meta.joinable != vq->meta.joinable) {
throw std::runtime_error(std::format("Reassembled quest joinable ({}) does not match original ({})",
assembled.meta.joinable, vq->meta.joinable));
}
if (assembled.meta.max_players != vq->meta.max_players) {
throw std::runtime_error(std::format("Reassembled quest max_players ({}) does not match original ({})",
assembled.meta.max_players, vq->meta.max_players));
}
if (assembled.meta.name != vq->meta.name) {
throw std::runtime_error(std::format("Reassembled quest name ({}) does not match original ({})",
assembled.meta.name, vq->meta.name));
}
if (assembled.meta.short_description != vq->meta.short_description) {
throw std::runtime_error(std::format("Reassembled quest short description ({}) does not match original ({})",
assembled.meta.short_description, vq->meta.short_description));
}
if (assembled.meta.long_description != vq->meta.long_description) {
throw std::runtime_error(std::format("Reassembled quest long description ({}) does not match original ({})",
assembled.meta.long_description, vq->meta.long_description));
}
} catch (const std::exception& e) {
phosg::log_error_f("================ DISASSEMBLY:");
phosg::fwritex(stderr, disassembled);
phosg::log_error_f("================ REASSEMBLY:");
phosg::fwritex(stderr, reassembly);
if (!assembled.data.empty()) {
phosg::log_error_f("================ BINDIFF:");
phosg::print_binary_diff(stderr, decompressed_bin.data(), decompressed_bin.size(), assembled.data.data(), assembled.data.size(), isatty(fileno(stderr)), 3, 0);
}
phosg::log_info_f("... {} {} {} ({}) FAILED", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name);
throw;
phosg::log_info_f("... {} {} {} ({}) OK", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name);
}
phosg::log_info_f("... {} {} {} ({}) OK", phosg::name_for_enum(vq->meta.version), name_for_language(vq->meta.language), vq->bin_filename(), vq->meta.name);
}
}
});
+17 -7
View File
@@ -107,6 +107,8 @@ static string escape_string(const string& data, TextEncoding encoding = TextEnco
ret += "\\\'";
} else if (ch == '\"') {
ret += "\\\"";
} else if (ch == '\\') {
ret += "\\\\";
} else {
ret += ch;
}
@@ -2947,7 +2949,7 @@ static const unordered_map<string, const QuestScriptOpcodeDefinition*>& opcodes_
return index;
}
void check_opcode_definitions() {
void check_quest_opcode_definitions() {
static const array<Version, 12> versions = {
Version::DC_NTE,
Version::DC_11_2000,
@@ -3043,10 +3045,12 @@ std::string disassemble_quest_script(
}
if (!is_v1_or_v2(version) || (version == Version::GC_NTE)) {
lines.emplace_back(std::format(".episode {}", token_name_for_episode(meta.episode)));
lines.emplace_back(std::format(".header_episode 0x{:02X}", meta.header_episode));
if (meta.header_episode >= 0) {
lines.emplace_back(std::format(".header_episode 0x{:02X}", meta.header_episode));
}
}
lines.emplace_back(std::format(".language {}", char_for_language(meta.language)));
if (!is_pre_v1(version) && !is_v4(version) && (static_cast<uint8_t>(meta.language) != meta.header_language)) {
if (!is_pre_v1(version) && !is_v4(version) && (meta.header_language >= 0) && (static_cast<uint8_t>(meta.language) != meta.header_language)) {
lines.emplace_back(std::format(".header_language 0x{:02X}", meta.header_language));
}
if (is_v4(version)) {
@@ -3894,7 +3898,13 @@ std::string disassemble_quest_script(
disassemble_label_as_vector4f_list(l);
break;
case Arg::DataType::SCRIPT:
disassemble_label_as_script(l, false);
try {
disassemble_label_as_script(l, reassembly_mode);
} catch (const std::exception& e) {
l->lines.clear();
l->lines.emplace_back(std::format(" // Warning: label is code, but disassembly failed ({})", e.what()));
disassemble_label_as_data(l);
}
break;
}
@@ -3904,9 +3914,9 @@ std::string disassemble_quest_script(
l->lines.clear();
try {
disassemble_label_as_script(l, true);
} catch (const std::runtime_error& e) {
} catch (const std::exception& e) {
l->lines.clear();
l->lines.emplace_back(std::format(" // Could not disassemble as code ({})", e.what()));
l->lines.emplace_back(std::format(" // Label type is not known; could not disassemble as code ({})", e.what()));
disassemble_label_as_data(l);
}
}
@@ -4293,7 +4303,7 @@ AssembledQuestScript assemble_quest_script(
ret.meta.episode = episode_for_token_name(line.text.substr(9));
} else if (line.text.starts_with(".max_players ")) {
ret.meta.max_players = stoul(line.text.substr(12), nullptr, 0);
} else if (line.text.starts_with(".joinable ")) {
} else if (line.text.starts_with(".joinable")) {
ret.meta.joinable = true;
} else if (line.text.starts_with(".header_language ")) {
ret.meta.header_language = stoul(line.text.substr(17), nullptr, 0);
-10
View File
@@ -1,10 +0,0 @@
#!/bin/sh
set -e
EXECUTABLE="$1"
if [ -z "$EXECUTABLE" ]; then
EXECUTABLE="./newserv"
fi
$EXECUTABLE --config=tests/config.json check-quests
@@ -7,4 +7,4 @@ if [ -z "$EXECUTABLE" ]; then
EXECUTABLE="./newserv"
fi
$EXECUTABLE check-quest-opcodes
$EXECUTABLE check-quests --reassembly