From 566de06fd12c1d61b6d9becac3df0dd550ca1faf Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 24 Nov 2025 01:03:24 -0800 Subject: [PATCH] rewrite quest disassembler --- src/Main.cc | 16 +- src/ProxyCommands.cc | 9 - src/QuestScript.cc | 1354 +++++++++++++++++++++++++----------------- src/QuestScript.hh | 19 +- 4 files changed, 824 insertions(+), 574 deletions(-) diff --git a/src/Main.cc b/src/Main.cc index f6675852..b48366fa 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -1595,7 +1595,7 @@ Action a_check_quest_opcodes( }); Action a_disassemble_quest_script( "disassemble-quest-script", "\ - disassemble-quest-script [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ + disassemble-quest-script [OPTIONS] [INPUT-FILENAME [OUTPUT-FILENAME]]\n\ Disassemble the input quest script (.bin file) into a text representation\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\ @@ -1603,17 +1603,25 @@ Action a_disassemble_quest_script( default; the --qedit option will result in names matching those used by\n\ QEdit. If you intend to reassemble the script, after editing it, use the\n\ --reassembly option to add explicit label numbers and remove offsets and\n\ - data in code sections.\n", + data in code sections. To include script references from the map, use the\n\ + --map-file=FILENAME option.", +[](phosg::Arguments& args) { string data = read_input_data(args); auto version = get_cli_version(args); if (!args.get("decompressed")) { data = prs_decompress(data); } - Language override_language = static_cast(args.get("language", 0xFF)); + shared_ptr map_file; + string map_filename = args.get("map-file", false); + if (!map_filename.empty()) { + auto map_data = make_shared(prs_decompress(phosg::load_file(map_filename))); + map_file = make_shared(map_data); + } + Language language = static_cast(args.get("language", 0xFF)); bool reassembly_mode = args.get("reassembly"); bool use_qedit_names = args.get("qedit"); - string result = disassemble_quest_script(data.data(), data.size(), version, override_language, reassembly_mode, use_qedit_names); + string result = disassemble_quest_script( + data.data(), data.size(), version, language, map_file, reassembly_mode, use_qedit_names); write_output_data(args, result.data(), result.size(), "txt"); }); Action a_disassemble_quest_map( diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index acb1b625..48ac6dae 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -1338,15 +1338,6 @@ static asio::awaitable S_13_A7(shared_ptr c, Channel::Mes sf->data = decode_dlq_data(sf->data); } phosg::save_file(sf->output_filename, sf->data); - if (sf->basename.ends_with(".bin")) { - try { - string decompressed = prs_decompress(sf->data); - auto disassembly = disassemble_quest_script(decompressed.data(), decompressed.size(), c->version(), c->language(), false); - phosg::save_file(sf->output_filename + ".txt", disassembly); - } catch (const exception& e) { - c->log.warning_f("Failed to disassemble quest file: {}", e.what()); - } - } } else { c->log.info_f("Download complete for file {}", sf->basename); } diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 984bed86..bad865df 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -180,7 +180,7 @@ struct QuestScriptOpcodeDefinition { ATTACK_DATA, MOVEMENT_DATA, IMAGE_DATA, - BEZIER_CONTROL_POINT_DATA, + VECTOR4F_LIST, }; Type type; @@ -204,14 +204,14 @@ struct QuestScriptOpcodeDefinition { const char* name; const char* qedit_name; std::vector args; - uint16_t flags; + uint32_t flags; QuestScriptOpcodeDefinition( uint16_t opcode, const char* name, const char* qedit_name, std::vector args, - uint16_t flags) + uint32_t flags) : opcode(opcode), name(name), qedit_name(qedit_name), @@ -224,31 +224,33 @@ struct QuestScriptOpcodeDefinition { } }; -constexpr uint16_t v_flag(Version v) { - return (1 << static_cast(v)); +constexpr uint32_t v_flag(Version v) { + return (1 << static_cast(v)); } using Arg = QuestScriptOpcodeDefinition::Argument; static_assert(NUM_VERSIONS == 14, "Don\'t forget to update the QuestScript flags and opcode definitions table"); -static constexpr uint16_t F_PUSH_ARG = 0x0001; // Version::PC_PATCH (unused for quests) +static constexpr uint32_t F_PUSH_ARG = 0x00010000; +static constexpr uint32_t F_CLEAR_ARGS = 0x00020000; // F_ARGS means this opcode takes its arguments via the argument list on v3 and // later. It has no effect on v2 and earlier. -static constexpr uint16_t F_ARGS = 0x0002; // Version::BB_PATCH (unused for quests) +static constexpr uint32_t F_ARGS = 0x00040000; +static constexpr uint32_t F_TERMINATOR = 0x00080000; // The following flags are used to specify which versions support each opcode. -static constexpr uint16_t F_DC_NTE = 0x0004; // Version::DC_NTE -static constexpr uint16_t F_DC_112000 = 0x0008; // Version::DC_11_2000 -static constexpr uint16_t F_DC_V1 = 0x0010; // Version::DC_V1 -static constexpr uint16_t F_DC_V2 = 0x0020; // Version::DC_V2 -static constexpr uint16_t F_PC_NTE = 0x0040; // Version::PC_NTE -static constexpr uint16_t F_PC_V2 = 0x0080; // Version::PC_V2 -static constexpr uint16_t F_GC_NTE = 0x0100; // Version::GC_NTE -static constexpr uint16_t F_GC_V3 = 0x0200; // Version::GC_V3 -static constexpr uint16_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_NTE -static constexpr uint16_t F_GC_EP3 = 0x0800; // Version::GC_EP3 -static constexpr uint16_t F_XB_V3 = 0x1000; // Version::XB_V3 -static constexpr uint16_t F_BB_V4 = 0x2000; // Version::BB_V4 +static constexpr uint32_t F_DC_NTE = 0x0004; // Version::DC_NTE +static constexpr uint32_t F_DC_112000 = 0x0008; // Version::DC_11_2000 +static constexpr uint32_t F_DC_V1 = 0x0010; // Version::DC_V1 +static constexpr uint32_t F_DC_V2 = 0x0020; // Version::DC_V2 +static constexpr uint32_t F_PC_NTE = 0x0040; // Version::PC_NTE +static constexpr uint32_t F_PC_V2 = 0x0080; // Version::PC_V2 +static constexpr uint32_t F_GC_NTE = 0x0100; // Version::GC_NTE +static constexpr uint32_t F_GC_V3 = 0x0200; // Version::GC_V3 +static constexpr uint32_t F_GC_EP3TE = 0x0400; // Version::GC_EP3_NTE +static constexpr uint32_t F_GC_EP3 = 0x0800; // Version::GC_EP3 +static constexpr uint32_t F_XB_V3 = 0x1000; // Version::XB_V3 +static constexpr uint32_t F_BB_V4 = 0x2000; // Version::BB_V4 static_assert(F_DC_NTE == v_flag(Version::DC_NTE)); static_assert(F_DC_112000 == v_flag(Version::DC_11_2000)); @@ -265,20 +267,20 @@ static_assert(F_BB_V4 == v_flag(Version::BB_V4)); // clang-format off // These are shortcuts for common version ranges in the definitions below. -static constexpr uint16_t F_V0_V2 = F_DC_NTE | F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; -static constexpr uint16_t F_V0_V4 = F_DC_NTE | F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; -static constexpr uint16_t F_V05_V2 = F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; -static constexpr uint16_t F_V05_V4 = F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; -static constexpr uint16_t F_V1_V2 = F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; -static constexpr uint16_t F_V1_V4 = F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; -static constexpr uint16_t F_V2 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; -static constexpr uint16_t F_V2_V3 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3; -static constexpr uint16_t F_V2_V4 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; -static constexpr uint16_t F_V3 = F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3; -static constexpr uint16_t F_V3_V4 = F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; -static constexpr uint16_t F_V4 = F_BB_V4; +static constexpr uint32_t F_V0_V2 = F_DC_NTE | F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; +static constexpr uint32_t F_V0_V4 = F_DC_NTE | F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; +static constexpr uint32_t F_V05_V2 = F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; +static constexpr uint32_t F_V05_V4 = F_DC_112000 | F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; +static constexpr uint32_t F_V1_V2 = F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; +static constexpr uint32_t F_V1_V4 = F_DC_V1 | F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; +static constexpr uint32_t F_V2 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE; +static constexpr uint32_t F_V2_V3 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3; +static constexpr uint32_t F_V2_V4 = F_DC_V2 | F_PC_NTE | F_PC_V2 | F_GC_NTE | F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; +static constexpr uint32_t F_V3 = F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3; +static constexpr uint32_t F_V3_V4 = F_GC_V3 | F_GC_EP3TE | F_GC_EP3 | F_XB_V3 | F_BB_V4; +static constexpr uint32_t F_V4 = F_BB_V4; // clang-format on -static constexpr uint16_t F_HAS_ARGS = F_V3_V4; +static constexpr uint32_t F_HAS_ARGS = F_V3_V4; // These are the argument data types. All values are stored little-endian in // the script data, even on the GameCube. @@ -354,26 +356,28 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x00, "nop", nullptr, {}, F_V0_V4}, // Pops new PC off stack - {0x01, "ret", nullptr, {}, F_V0_V4}, + {0x01, "ret", nullptr, {}, F_V0_V4 | F_TERMINATOR}, // Stops execution for the current frame. Execution resumes immediately // after this opcode on the next frame. {0x02, "sync", nullptr, {}, F_V0_V4}, // Exits entirely - {0x03, "exit", nullptr, {I32}, F_V0_V4}, + {0x03, "exit", nullptr, {I32}, F_V0_V4 | F_TERMINATOR}, // Starts a new thread at labelA {0x04, "thread", nullptr, {SCRIPT16}, F_V0_V4}, // Pushes r1-r7 to the stack - {0x05, "va_start", nullptr, {}, F_V3_V4}, + {0x05, "va_start", nullptr, {}, F_V3_V4 | F_CLEAR_ARGS}, // Pops r7-r1 from the stack {0x06, "va_end", nullptr, {}, F_V3_V4}, // Replaces r1-r7 with the args list, then calls labelA - {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4}, + // Note: This opcode doesn't directly clear the args list, but we assume + // during disassembly that the code being called does so. + {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4 | F_CLEAR_ARGS}, // Copies a value from regB to regA {0x08, "let", nullptr, {W_REG, R_REG}, F_V0_V4}, @@ -396,7 +400,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // Sets regA to the address of the offset of labelB in the function table // (to get the offset, use read4 after this) - {0x0D, "leto", nullptr, {W_REG, SCRIPT16}, F_V3_V4}, + {0x0D, "leto", nullptr, {W_REG, LABEL16}, F_V3_V4}, // Sets regA to 1 {0x10, "set", nullptr, {W_REG}, F_V0_V4}, @@ -477,11 +481,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x27, "modi", nullptr, {W_REG, I32}, F_V3_V4}, // Jumps to labelA - {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4}, + {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4 | F_TERMINATOR}, // Pushes the script offset immediately after this opcode and jumps to // labelA - {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4}, + // Note: This opcode doesn't directly clear the args list, but we assume + // during disassembly that the code being called does so. + {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4 | F_CLEAR_ARGS}, // If all values in regsB are nonzero, jumps to labelA {0x2A, "jmp_on", nullptr, {SCRIPT16, R_REG_SET}, F_V0_V4}, @@ -549,11 +555,13 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // If regA <= valueB (signed), jumps to labelC {0x3F, "jmpi_le", "jmpi_<=", {R_REG, I32, SCRIPT16}, F_V0_V4}, - // Jumps to labelsB[regA] + // Jumps to labelsB[regA]; if regA is out of range of labelsB, does nothing {0x40, "switch_jmp", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4}, - // Calls labelsB[regA] - {0x41, "switch_call", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4}, + // Calls labelsB[regA]; if regA is out of range of labelsB, does nothing + // Note: This opcode doesn't directly clear the args list, but we assume + // during disassembly that the code being called does so. + {0x41, "switch_call", nullptr, {R_REG, SCRIPT16_SET}, F_V0_V4 | F_CLEAR_ARGS}, // Does nothing {0x42, "nop_42", nullptr, {I32}, F_V0_V2}, @@ -1056,10 +1064,10 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // 0F: NPC hit for > 20% of max HP (and <= 30%) // 10: NPC hit for > 1% of max HP (and <= 20%) // 11: NPC healed by another player - // 12: Room cleared (set event cleared which did not trigger another set) + // 12: Room cleared (wave cleared which did not trigger another wave) // 13: NPC used a recovery item // 14: NPC cannot heal / recover - // 15: Wave but not room cleared (set event triggered by another set) + // 15: Wave but not room cleared (wave triggered by another wave) // 16: NPC casting Resta or Anti // 17: NPC casting Foie, Zonde, or Barta // 18: NPC regained sight of player (not valid on 11/2000) @@ -2348,7 +2356,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // regsE[0-2] = result point (x, y, z as floats) // regsE[3] = result code (0 = failed, 1 = success) // labelF = control point entries (array of valueA VectorXYZTF structures) - {0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::BEZIER_CONTROL_POINT_DATA}}, F_V3_V4 | F_ARGS}, + {0xF8F2, "compute_bezier_curve_point", "load_unk_data", {I32, FLOAT32, FLOAT32, I32, {W_REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::VECTOR4F_LIST}}, F_V3_V4 | F_ARGS}, // Creates a timed particle effect. Like the particle opcode, but the // location (and duration, for some reason) are floats. @@ -2653,7 +2661,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // by prepare_statistic // labelB = label1 (used in send_statistic) // labelC = label2 (used in send_statistic) - {0xF93E, "prepare_statistic", "prepare_statistic?", {I32, LABEL32, LABEL32}, F_V3_V4 | F_ARGS}, + {0xF93E, "prepare_statistic", "prepare_statistic?", {I32, SCRIPT32, SCRIPT32}, F_V3_V4 | F_ARGS}, // Enables use of the check_for_keyword opcode. {0xF93F, "enable_keyword_detect", "keyword_detect", {}, F_V3_V4}, @@ -2898,8 +2906,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF961, "bb_get_6xE3_status", "unknownF961", {W_REG}, F_V4}, }; -static const unordered_map& -opcodes_for_version(Version v) { +static const unordered_map& opcodes_for_version(Version v) { static array< unordered_map, static_cast(Version::BB_V4) + 1> @@ -2907,7 +2914,7 @@ opcodes_for_version(Version v) { auto& index = indexes.at(static_cast(v)); if (index.empty()) { - uint16_t vf = v_flag(v); + uint32_t vf = v_flag(v); for (size_t z = 0; z < sizeof(opcode_defs) / sizeof(opcode_defs[0]); z++) { const auto& def = opcode_defs[z]; if (!(def.flags & vf)) { @@ -2921,16 +2928,12 @@ opcodes_for_version(Version v) { return index; } -static const unordered_map& -opcodes_by_name_for_version(Version v) { - static array< - unordered_map, - static_cast(Version::BB_V4) + 1> - indexes; +static const unordered_map& opcodes_by_name_for_version(Version v) { + static array, static_cast(Version::BB_V4) + 1> indexes; auto& index = indexes.at(static_cast(v)); if (index.empty()) { - uint16_t vf = v_flag(v); + uint32_t vf = v_flag(v); for (size_t z = 0; z < sizeof(opcode_defs) / sizeof(opcode_defs[0]); z++) { const auto& def = opcode_defs[z]; if (!(def.flags & vf)) { @@ -3019,25 +3022,28 @@ CreateItemMaskEntry::operator QuestMetadata::CreateItemMask() const { } std::string disassemble_quest_script( - const void* data, - size_t size, + const void* bin_data, + size_t bin_size, Version version, - Language override_language, + Language language, + shared_ptr dat, bool reassembly_mode, bool use_qedit_names) { - phosg::StringReader r(data, size); + + phosg::StringReader r(bin_data, bin_size); deque lines; lines.emplace_back(std::format(".version {}", phosg::name_for_enum(version))); + // Phase 0: Parse the header and generate the metadata section + bool use_wstrs = false; size_t code_offset = 0; - size_t function_table_offset = 0; - Language language; + size_t label_table_offset = 0; switch (version) { case Version::DC_NTE: { const auto& header = r.get(); code_offset = header.code_offset; - function_table_offset = header.function_table_offset; + label_table_offset = header.label_table_offset; language = Language::JAPANESE; lines.emplace_back(".name " + escape_string(header.name.decode(Language::JAPANESE))); break; @@ -3047,13 +3053,9 @@ std::string disassemble_quest_script( case Version::DC_V2: { const auto& header = r.get(); code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - if (override_language != Language::UNKNOWN) { - language = override_language; - } else if (static_cast(header.language) < 5) { - language = header.language; - } else { - language = Language::ENGLISH; + label_table_offset = header.label_table_offset; + if (language == Language::UNKNOWN) { + language = (static_cast(header.language) < 5) ? header.language : Language::ENGLISH; } lines.emplace_back(std::format(".quest_num {}", header.quest_number)); lines.emplace_back(std::format(".language {}", char_for_language(header.language))); @@ -3067,13 +3069,9 @@ std::string disassemble_quest_script( use_wstrs = true; const auto& header = r.get(); code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - if (override_language != Language::UNKNOWN) { - language = override_language; - } else if (static_cast(header.language) < 8) { - language = header.language; - } else { - language = Language::ENGLISH; + label_table_offset = header.label_table_offset; + if (language == Language::UNKNOWN) { + language = (static_cast(header.language) < 8) ? header.language : Language::ENGLISH; } lines.emplace_back(std::format(".quest_num {}", header.quest_number)); lines.emplace_back(std::format(".language {}", char_for_language(header.language))); @@ -3089,13 +3087,9 @@ std::string disassemble_quest_script( case Version::XB_V3: { const auto& header = r.get(); code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - if (override_language != Language::UNKNOWN) { - language = override_language; - } else if (static_cast(header.language) < 5) { - language = header.language; - } else { - language = Language::ENGLISH; + label_table_offset = header.label_table_offset; + if (language == Language::UNKNOWN) { + language = (static_cast(header.language) < 5) ? header.language : Language::ENGLISH; } lines.emplace_back(std::format(".quest_num {}", header.quest_number)); lines.emplace_back(std::format(".language {}", char_for_language(header.language))); @@ -3108,10 +3102,8 @@ std::string disassemble_quest_script( use_wstrs = true; const auto& header = r.get(); code_offset = header.code_offset; - function_table_offset = header.function_table_offset; - if (override_language != Language::UNKNOWN) { - language = override_language; - } else { + label_table_offset = header.label_table_offset; + if (language == Language::UNKNOWN) { language = Language::ENGLISH; } lines.emplace_back(std::format(".quest_num {}", header.quest_number)); @@ -3126,8 +3118,7 @@ std::string disassemble_quest_script( // Quests saved with Qedit may not have the full header, so only parse // the full header if the code and function table offsets don't point to // space within it - if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) && - (header.function_table_offset >= sizeof(PSOQuestHeaderBB))) { + if ((header.code_offset >= sizeof(PSOQuestHeaderBB)) && (header.label_table_offset >= sizeof(PSOQuestHeaderBB))) { r.go(0); const auto& header = r.get(); for (size_t z = 0; z < header.create_item_mask_entries.size(); z++) { @@ -3144,104 +3135,399 @@ std::string disassemble_quest_script( default: throw logic_error("invalid quest version"); } + lines.emplace_back(); - const auto& opcodes = opcodes_for_version(version); - phosg::StringReader cmd_r = r.sub(code_offset, function_table_offset - code_offset); + // Phase 1: Parse label table + + phosg::StringReader text_r, label_table_r; + if (code_offset < label_table_offset) { + text_r = r.sub(code_offset, label_table_offset - code_offset); + label_table_r = r.sub(label_table_offset); + } else { + text_r = r.sub(code_offset); + label_table_r = r.sub(label_table_offset, code_offset - label_table_offset); + } struct Label { - string name; - uint32_t offset; - uint32_t label_index; // 0xFFFFFFFF = no label - uint64_t type_flags; - set references; + struct ObjectSetRef { + uint8_t floor; + size_t set_index; + const MapFile::ObjectSetEntry* obj_set; + }; + struct EnemySetRef { + uint8_t floor; + size_t set_index; + const MapFile::EnemySetEntry* ene_set; + }; - Label(const string& name, uint32_t offset, int64_t label_index = -1, uint64_t type_flags = 0) - : name(name), - offset(offset), - label_index(label_index), - type_flags(type_flags) {} - void add_data_type(Arg::DataType type) { - this->type_flags |= (1 << static_cast(type)); - } - bool has_data_type(Arg::DataType type) const { - return this->type_flags & (1 << static_cast(type)); - } + set label_nums; + uint32_t offset; + uint32_t size = 0; + set script_refs; + vector object_refs; + vector enemy_refs; + Arg::DataType type = Arg::DataType::NONE; + deque lines; }; - vector> function_table; - multimap> offset_to_label; - phosg::StringReader function_table_r = r.sub(function_table_offset); - while (!function_table_r.eof()) { + vector> label_table; + map> offset_to_label; + deque, Arg::DataType>> pending_labels; + while (!label_table_r.eof()) { try { - uint32_t label_index = function_table.size(); - string name = (label_index == 0) ? "start" : std::format("label{:04X}", label_index); - uint32_t offset = function_table_r.get_u32l(); - auto l = make_shared