From 80ae6ecac8bb54da7960e437f542e2f323acdf17 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Tue, 4 Jul 2023 11:11:36 -0700 Subject: [PATCH] add stack argument disassembly in quest script --- src/Compression.cc | 16 ++- src/Compression.hh | 6 + src/QuestScript.cc | 325 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 307 insertions(+), 40 deletions(-) diff --git a/src/Compression.cc b/src/Compression.cc index 8571c987..ba558c33 100644 --- a/src/Compression.cc +++ b/src/Compression.cc @@ -767,7 +767,7 @@ string prs_compress_indexed(const string& data, function p return prs_compress_indexed(data.data(), data.size(), progress_fn); } -string prs_decompress(const void* data, size_t size, size_t max_output_size) { +PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size) { // PRS is an LZ77-based compression algorithm. Compressed data is split into // two streams: a control stream and a data stream. The control stream is read // one bit at a time, and the data stream is read one byte at a time. The @@ -862,11 +862,21 @@ string prs_decompress(const void* data, size_t size, size_t max_output_size) { } } - return std::move(w.str()); + return {std::move(w.str()), r.where()}; +} + +PRSDecompressResult prs_decompress_with_meta(const string& data, size_t max_output_size) { + return prs_decompress_with_meta(data.data(), data.size(), max_output_size); +} + +string prs_decompress(const void* data, size_t size, size_t max_output_size) { + auto ret = prs_decompress_with_meta(data, size, max_output_size); + return std::move(ret.data); } string prs_decompress(const string& data, size_t max_output_size) { - return prs_decompress(data.data(), data.size(), max_output_size); + auto ret = prs_decompress_with_meta(data.data(), data.size(), max_output_size); + return std::move(ret.data); } size_t prs_decompress_size(const void* data, size_t size, size_t max_output_size) { diff --git a/src/Compression.hh b/src/Compression.hh index 620c87ef..55843199 100644 --- a/src/Compression.hh +++ b/src/Compression.hh @@ -190,6 +190,12 @@ std::string prs_compress_optimal( std::function progress_fn = nullptr); // Decompresses PRS-compressed data. +struct PRSDecompressResult { + std::string data; + size_t input_bytes_used; +}; +PRSDecompressResult prs_decompress_with_meta(const void* data, size_t size, size_t max_output_size = 0); +PRSDecompressResult prs_decompress_with_meta(const std::string& data, size_t max_output_size = 0); std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0); std::string prs_decompress(const std::string& data, size_t max_output_size = 0); diff --git a/src/QuestScript.cc b/src/QuestScript.cc index f8bdd81e..57080997 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -16,6 +16,29 @@ using namespace std; +static string format_and_indent_data(const void* data, size_t size, uint64_t start_address) { + struct iovec iov; + iov.iov_base = const_cast(data); + iov.iov_len = size; + + string ret = " "; + format_data( + [&ret](const void* vdata, size_t size) -> void { + const char* data = reinterpret_cast(vdata); + for (size_t z = 0; z < size; z++) { + if (data[z] == '\n') { + ret += "\n "; + } else { + ret.push_back(data[z]); + } + } + }, + &iov, 1, start_address, nullptr, 0, PrintDataFlags::PRINT_ASCII); + + strip_trailing_whitespace(ret); + return ret; +} + static string dasm_u16string(const char16_t* data, size_t size) { try { return format_data_string(encode_sjis(data, size)); @@ -67,6 +90,10 @@ struct MovementData { parray unknown_a2; } __attribute__((packed)); +struct UnknownF8F2Entry { + parray unknown_a1; +} __attribute__((packed)); + struct QuestScriptOpcodeDefinition { struct Argument { enum class Type { @@ -95,6 +122,7 @@ struct QuestScriptOpcodeDefinition { ATTACK_DATA, MOVEMENT_DATA, IMAGE_DATA, + UNKNOWN_F8F2_DATA, }; Type type; @@ -258,7 +286,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0x004A, "arg_pushb", {INT8}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes imm to the args list {0x004B, "arg_pushw", {INT16}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes imm to the args list {0x004C, "arg_pusha", {REG}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes memory address of regA to the args list - {0x004D, "arg_pusho", {SCRIPT16}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes function_table[fn_id] to the args list + {0x004D, "arg_pusho", {LABEL16}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes function_table[fn_id] to the args list {0x004E, "arg_pushs", {CSTRING}, {}, V3, V4, PRESERVE_ARG_STACK}, // Pushes memory address of str to the args list {0x0050, "message", {INT32, CSTRING}, {}, V1, V2}, // Creates a dialogue with object/NPC N starting with message str {0x0050, "message", {}, {INT32, CSTRING}, V3, V4}, // Creates a dialogue with object/NPC N starting with message str @@ -745,7 +773,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF8EF, "nop_F8EF", {}, {}, V3, V4}, {0xF8F0, "turn_off_bgm_p2", {}, {}, V3, V4}, {0xF8F1, "turn_on_bgm_p2", {}, {}, V3, V4}, - {0xF8F2, nullptr, {}, {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, DATA16}, V3, V4}, // TODO (DX) + {0xF8F2, nullptr, {}, {INT32, FLOAT32, FLOAT32, INT32, {REG_SET_FIXED, 4}, {LABEL16, Arg::DataType::UNKNOWN_F8F2_DATA}}, V3, V4}, // TODO (DX) {0xF8F3, "particle2", {}, {{REG_SET_FIXED, 3}, INT32, FLOAT32}, V3, V4}, {0xF901, "dec2float", {REG, REG}, {}, V3, V4}, {0xF902, "float2dec", {REG, REG}, {}, V3, V4}, @@ -798,7 +826,7 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { {0xF932, "set_episode2", {REG}, {}, V3, V4}, {0xF933, "item_create_multi_cm", {{REG_SET_FIXED, 7}}, {}, V3, V3}, // regsA[1-6] form an ItemData's data1[0-5] {0xF933, "nop_F933", {{REG_SET_FIXED, 7}}, {}, V4, V4}, - {0xF934, "scroll_text", {}, {FLOAT32, FLOAT32, FLOAT32, FLOAT32, INT32, FLOAT32, REG, CSTRING}, V3, V4}, + {0xF934, "scroll_text", {}, {INT32, INT32, INT32, INT32, INT32, FLOAT32, REG, CSTRING}, V3, V4}, {0xF935, "gba_create_dl_graph", {}, {}, V3, V3}, {0xF935, "nop_F935", {}, {}, V4, V4}, {0xF936, "gba_destroy_dl_graph", {}, {}, V3, V3}, @@ -1004,15 +1032,42 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion next_offset(next_offset) {} }; + struct ArgStackValue { + enum class Type { + REG, + REG_PTR, + LABEL, + INT, + CSTRING, + }; + Type type; + uint32_t as_int; + std::string as_string; + + ArgStackValue(Type type, uint32_t value) { + this->type = type; + this->as_int = value; + } + ArgStackValue(const std::string& value) { + this->type = Type::CSTRING; + this->as_string = value; + } + }; + map dasm_lines; set pending_dasm_start_offsets; - pending_dasm_start_offsets.emplace(function_table.at(0)->offset); + for (const auto& l : function_table) { + if (l->offset < cmd_r.size()) { + pending_dasm_start_offsets.emplace(l->offset); + } + } while (!pending_dasm_start_offsets.empty()) { auto dasm_start_offset_it = pending_dasm_start_offsets.begin(); cmd_r.go(*dasm_start_offset_it); pending_dasm_start_offsets.erase(dasm_start_offset_it); + vector arg_stack_values; while (!cmd_r.eof() && !dasm_lines.count(cmd_r.where())) { size_t opcode_start_offset = cmd_r.where(); string dasm_line; @@ -1041,12 +1096,15 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion switch (arg.type) { case Type::LABEL16: case Type::LABEL32: { - uint32_t function_id = (arg.type == Type::LABEL32) ? cmd_r.get_u32l() : cmd_r.get_u16l(); - if (function_id >= function_table.size()) { - dasm_arg = string_printf("label%04" PRIX32 " /* invalid */", function_id); + uint32_t label_id = (arg.type == Type::LABEL32) ? cmd_r.get_u32l() : cmd_r.get_u16l(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(ArgStackValue::Type::LABEL, label_id); + } + if (label_id >= function_table.size()) { + dasm_arg = string_printf("label%04" PRIX32 " /* invalid */", label_id); } else { - auto& l = function_table.at(function_id); - dasm_arg = string_printf("label%04" PRIX32 " /* %04" PRIX32 " */", function_id, l->offset); + auto& l = function_table.at(label_id); + dasm_arg = string_printf("label%04" PRIX32 " /* %04" PRIX32 " */", label_id, l->offset); l->references.emplace(opcode_start_offset); l->add_data_type(arg.data_type); if (arg.data_type == Arg::DataType::SCRIPT) { @@ -1056,15 +1114,18 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion break; } case Type::LABEL16_SET: { + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + throw logic_error("LABEL16_SET cannot be pushed to arg stack"); + } uint8_t num_functions = cmd_r.get_u8(); for (size_t z = 0; z < num_functions; z++) { - dasm_arg += (dasm_arg.empty() ? "(" : ","); - uint32_t function_id = cmd_r.get_u16l(); - if (function_id >= function_table.size()) { - dasm_arg += string_printf("function%04" PRIX32 " /* invalid */", function_id); + dasm_arg += (dasm_arg.empty() ? "(" : ", "); + uint32_t label_id = cmd_r.get_u16l(); + if (label_id >= function_table.size()) { + dasm_arg += string_printf("function%04" PRIX32 " /* invalid */", label_id); } else { - auto& l = function_table.at(function_id); - dasm_arg = string_printf("label%04" PRIX32 " /* %04" PRIX32 " */", function_id, l->offset); + auto& l = function_table.at(label_id); + dasm_arg += string_printf("label%04" PRIX32 " /* %04" PRIX32 " */", label_id, l->offset); l->references.emplace(opcode_start_offset); l->add_data_type(arg.data_type); if (arg.data_type == Arg::DataType::SCRIPT) { @@ -1079,13 +1140,21 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion } break; } - case Type::REG: - dasm_arg = string_printf("r%hhu", cmd_r.get_u8()); + case Type::REG: { + uint8_t reg = cmd_r.get_u8(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back((def->opcode == 0x004C) ? ArgStackValue::Type::REG_PTR : ArgStackValue::Type::REG, reg); + } + dasm_arg = string_printf("r%hhu", reg); break; + } case Type::REG_SET: { + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + throw logic_error("REG_SET cannot be pushed to arg stack"); + } uint8_t num_regs = cmd_r.get_u8(); for (size_t z = 0; z < num_regs; z++) { - dasm_arg += string_printf("%cr%hhu", (dasm_arg.empty() ? '(' : ','), cmd_r.get_u8()); + dasm_arg += string_printf("%sr%hhu", (dasm_arg.empty() ? "(" : ", "), cmd_r.get_u8()); } if (dasm_arg.empty()) { dasm_arg = "()"; @@ -1096,30 +1165,60 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion } case Type::REG_SET_FIXED: { uint8_t first_reg = cmd_r.get_u8(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + throw logic_error("REG_SET_FIXED cannot be pushed to arg stack"); + } dasm_arg = string_printf("r%hhu-r%hhu", first_reg, static_cast(first_reg + arg.count - 1)); break; } - case Type::INT8: - dasm_arg = string_printf("0x%02hhX", cmd_r.get_u8()); + case Type::INT8: { + uint8_t v = cmd_r.get_u8(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); + } + dasm_arg = string_printf("0x%02hhX", v); break; - case Type::INT16: - dasm_arg = string_printf("0x%04hX", cmd_r.get_u16l()); + } + case Type::INT16: { + uint16_t v = cmd_r.get_u16l(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); + } + dasm_arg = string_printf("0x%04hX", v); break; - case Type::INT32: - dasm_arg = string_printf("0x%08" PRIX32, cmd_r.get_u32l()); + } + case Type::INT32: { + uint32_t v = cmd_r.get_u32l(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(ArgStackValue::Type::INT, v); + } + dasm_arg = string_printf("0x%08" PRIX32, v); break; - case Type::FLOAT32: - dasm_arg = string_printf("%g", cmd_r.get_f32l()); + } + case Type::FLOAT32: { + float v = cmd_r.get_f32l(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(ArgStackValue::Type::INT, *reinterpret_cast(&v)); + } + dasm_arg = string_printf("%g", v); break; + } case Type::CSTRING: if (use_wstrs) { u16string s; for (char16_t ch = cmd_r.get_u16l(); ch; ch = cmd_r.get_u16l()) { s.push_back(ch); } + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(encode_sjis(s)); + } dasm_arg = dasm_u16string(s.data(), s.size()); } else { - dasm_arg = format_data_string(cmd_r.get_cstr()); + string s = cmd_r.get_cstr(); + if (def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK) { + arg_stack_values.emplace_back(s); + } + dasm_arg = format_data_string(s); } break; default: @@ -1133,6 +1232,126 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion dasm_line += dasm_arg; } } + + if (!def->stack_args.empty()) { + if (!def->imm_args.empty()) { + throw logic_error("opcode has both imm_args and stack_args"); + } + dasm_line.resize(0x20, ' '); + dasm_line += "... "; + + if (def->stack_args.size() != arg_stack_values.size()) { + dasm_line += string_printf("/* matching error: expected %zu arguments, received %zu arguments */", + def->stack_args.size(), arg_stack_values.size()); + } else { + bool is_first_arg = true; + for (size_t z = 0; z < def->stack_args.size(); z++) { + const auto& arg_def = def->stack_args[z]; + const auto& arg_value = arg_stack_values[z]; + + string dasm_arg; + switch (arg_def.type) { + case Arg::Type::LABEL16: + case Arg::Type::LABEL32: + switch (arg_value.type) { + case ArgStackValue::Type::REG: + dasm_arg = string_printf("r%" PRIu32 "/* warning: cannot determine label data type */", arg_value.as_int); + break; + case ArgStackValue::Type::LABEL: + case ArgStackValue::Type::INT: + dasm_arg = string_printf("label%04" PRIX32, arg_value.as_int); + try { + auto l = function_table.at(arg_value.as_int); + l->add_data_type(arg_def.data_type); + l->references.emplace(opcode_start_offset); + } catch (const out_of_range&) { + } + break; + default: + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::REG: + case Arg::Type::REG32: + switch (arg_value.type) { + case ArgStackValue::Type::REG: + dasm_arg = string_printf("regs[r%" PRIu32 "]", arg_value.as_int); + break; + case ArgStackValue::Type::INT: + dasm_arg = string_printf("r%" PRIu32, arg_value.as_int); + break; + default: + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::REG_SET_FIXED: + case Arg::Type::REG32_SET_FIXED: + switch (arg_value.type) { + case ArgStackValue::Type::REG: + dasm_arg = string_printf("regs[r%" PRIu32 "]-regs[r%" PRIu32 "+%hhu]", arg_value.as_int, arg_value.as_int, static_cast(arg_def.count - 1)); + break; + case ArgStackValue::Type::INT: + dasm_arg = string_printf("r%" PRIu32 "-r%hhu", arg_value.as_int, static_cast(arg_value.as_int + arg_def.count - 1)); + break; + default: + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::INT8: + case Arg::Type::INT16: + case Arg::Type::INT32: + switch (arg_value.type) { + case ArgStackValue::Type::REG: + dasm_arg = string_printf("r%" PRIu32, arg_value.as_int); + break; + case ArgStackValue::Type::REG_PTR: + dasm_arg = string_printf("&r%" PRIu32, arg_value.as_int); + break; + case ArgStackValue::Type::INT: + dasm_arg = string_printf("0x%" PRIX32 " /* %" PRIu32 " */", arg_value.as_int, arg_value.as_int); + break; + default: + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::FLOAT32: + switch (arg_value.type) { + case ArgStackValue::Type::REG: + dasm_arg = string_printf("(float)r%" PRIu32, arg_value.as_int); + break; + case ArgStackValue::Type::INT: + dasm_arg = string_printf("%g", *reinterpret_cast(&arg_value.as_int)); + break; + default: + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::CSTRING: + if (arg_value.type == ArgStackValue::Type::CSTRING) { + dasm_arg = format_data_string(arg_value.as_string); + } else { + dasm_arg = "/* invalid-type */"; + } + break; + case Arg::Type::LABEL16_SET: + case Arg::Type::REG_SET: + default: + throw logic_error("set-type arg found on arg stack"); + } + + if (!is_first_arg) { + dasm_line += ", "; + } else { + is_first_arg = false; + } + dasm_line += dasm_arg; + } + } + } + + if (!(def->flags & QuestScriptOpcodeDefinition::Flag::PRESERVE_ARG_STACK)) { + arg_stack_values.clear(); + } } } catch (const exception& e) { dasm_line = string_printf(".failed (%s)", e.what()); @@ -1157,6 +1376,13 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion while (label_it != offset_to_label.end()) { auto l = label_it->second; label_it++; + size_t size = ((label_it == offset_to_label.end()) ? cmd_r.size() : label_it->second->offset) - l->offset; + if (size > 0) { + lines.emplace_back(); + } + if (l->function_id == 0) { + lines.emplace_back("start:"); + } lines.emplace_back(string_printf("label%04" PRIX32 ":", l->function_id)); if (l->references.size() == 1) { lines.emplace_back(string_printf(" // Referenced by instruction at %04zX", *l->references.begin())); @@ -1169,28 +1395,34 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion lines.emplace_back(" // Referenced by instructions at " + join(tokens, ", ")); } - size_t size = ((label_it == offset_to_label.end()) ? cmd_r.size() : label_it->second->offset) - l->offset; - auto print_as_struct = [&](function print_fn) { if (l->has_data_type(data_type)) { if (size >= sizeof(StructT)) { print_fn(cmd_r.pget(l->offset)); if (size > sizeof(StructT)) { + size_t struct_end_offset = l->offset + sizeof(StructT); size_t remaining_size = size - sizeof(StructT); - lines.emplace_back(" (after) " + format_data_string(cmd_r.pgetv(l->offset + sizeof(StructT), remaining_size), remaining_size)); + lines.emplace_back(" // Extra data after structure"); + lines.emplace_back(format_and_indent_data(cmd_r.pgetv(struct_end_offset, remaining_size), remaining_size, struct_end_offset)); } } else { lines.emplace_back(string_printf(" // As raw data (0x%zX bytes; too small for referenced type)", size)); - lines.emplace_back(" " + format_data_string(cmd_r.pgetv(l->offset, size), size)); + lines.emplace_back(format_and_indent_data(cmd_r.pgetv(l->offset, size), size, l->offset)); } } }; + if (l->type_flags == 0) { + lines.emplace_back(string_printf(" // Could not determine data type; disassembling as code and raw data")); + l->add_data_type(Arg::DataType::SCRIPT); + l->add_data_type(Arg::DataType::DATA); + } + // Print data interpretations of the label (if any) if (l->has_data_type(Arg::DataType::DATA)) { // TODO: We should produce a print_data-like view here lines.emplace_back(string_printf(" // As raw data (0x%zX bytes)", size)); - lines.emplace_back(format_data_string(cmd_r.pgetv(l->offset, size), size)); + lines.emplace_back(format_and_indent_data(cmd_r.pgetv(l->offset, size), size, l->offset)); } print_as_struct.template operator()([&](const PlayerVisualConfig& visual) -> void { lines.emplace_back(" // As PlayerVisualConfig"); @@ -1277,11 +1509,30 @@ std::string disassemble_quest_script(const void* data, size_t size, GameVersion } }); if (l->has_data_type(Arg::DataType::IMAGE_DATA)) { - string data = cmd_r.pread(l->offset, size); - string decompressed = prs_decompress(data); - lines.emplace_back(string_printf(" // As decompressed image data (0x%zX bytes)", decompressed.size())); - // TODO: Use format_data here, sigh - lines.emplace_back(" " + format_data_string(decompressed)); + const void* data = cmd_r.pgetv(l->offset, size); + auto decompressed = prs_decompress_with_meta(data, size); + lines.emplace_back(string_printf(" // As decompressed image data (0x%zX bytes)", decompressed.data.size())); + lines.emplace_back(format_and_indent_data(decompressed.data.data(), decompressed.data.size(), 0)); + if (decompressed.input_bytes_used < size) { + size_t compressed_end_offset = l->offset + decompressed.input_bytes_used; + size_t remaining_size = size - decompressed.input_bytes_used; + lines.emplace_back(" // Extra data after compressed data"); + lines.emplace_back(format_and_indent_data(cmd_r.pgetv(compressed_end_offset, remaining_size), remaining_size, compressed_end_offset)); + } + } + if (l->has_data_type(Arg::DataType::UNKNOWN_F8F2_DATA)) { + StringReader r = cmd_r.sub(l->offset, size); + lines.emplace_back(" // As F8F2 entries"); + while (r.remaining() >= sizeof(UnknownF8F2Entry)) { + const auto& e = r.get(); + lines.emplace_back(string_printf(" entry %g, %g, %g, %g", e.unknown_a1[0].load(), e.unknown_a1[1].load(), e.unknown_a1[2].load(), e.unknown_a1[3].load())); + } + if (r.remaining() > 0) { + size_t struct_end_offset = l->offset + r.where(); + size_t remaining_size = r.remaining(); + lines.emplace_back(" // Extra data after structures"); + lines.emplace_back(format_and_indent_data(r.getv(remaining_size), remaining_size, struct_end_offset)); + } } if (l->has_data_type(Arg::DataType::SCRIPT)) { for (size_t z = l->offset; z < l->offset + size;) {