From 4fe225a302bd4c9eb24ff72a4b495f992a0d0ddf Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 28 Nov 2025 12:41:07 -0800 Subject: [PATCH] fix multiple bugs in quest assembler --- src/Main.cc | 149 ++++++++---------- src/QuestScript.cc | 24 ++- tests/check-quests.test.sh | 10 -- ...de-defs.test.sh => quest-compiler.test.sh} | 2 +- 4 files changed, 85 insertions(+), 100 deletions(-) delete mode 100755 tests/check-quests.test.sh rename tests/{quest-opcode-defs.test.sh => quest-compiler.test.sh} (71%) diff --git a/src/Main.cc b/src/Main.cc index 87d023fb..c1f1a487 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -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(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(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("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); } } }); diff --git a/src/QuestScript.cc b/src/QuestScript.cc index ce7f1ceb..b26cc660 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -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& opcodes_ return index; } -void check_opcode_definitions() { +void check_quest_opcode_definitions() { static const array 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(meta.language) != meta.header_language)) { + if (!is_pre_v1(version) && !is_v4(version) && (meta.header_language >= 0) && (static_cast(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); diff --git a/tests/check-quests.test.sh b/tests/check-quests.test.sh deleted file mode 100755 index 5637b417..00000000 --- a/tests/check-quests.test.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -set -e - -EXECUTABLE="$1" -if [ -z "$EXECUTABLE" ]; then - EXECUTABLE="./newserv" -fi - -$EXECUTABLE --config=tests/config.json check-quests diff --git a/tests/quest-opcode-defs.test.sh b/tests/quest-compiler.test.sh similarity index 71% rename from tests/quest-opcode-defs.test.sh rename to tests/quest-compiler.test.sh index 1ffdc72d..a1d9d070 100755 --- a/tests/quest-opcode-defs.test.sh +++ b/tests/quest-compiler.test.sh @@ -7,4 +7,4 @@ if [ -z "$EXECUTABLE" ]; then EXECUTABLE="./newserv" fi -$EXECUTABLE check-quest-opcodes +$EXECUTABLE check-quests --reassembly