#include "QuestScript.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "BattleParamsIndex.hh" #include "CommandFormats.hh" #include "Compression.hh" #include "StaticGameData.hh" using namespace std; // This file documents PSO's quest script execution system. // The quest execution system has several relevant data structures: // - The quest script is a stream of binary data containing opcodes (as defined // below), each followed by their arguments. The offset of the code section // of this stream is defined here. // - The execution state specifies what the client should do on every frame. // There are many possible states here, such as waiting for the player to // dismiss a chat bubble, choose an item from a menu, etc. // - The function table is a list of offsets into the quest script which can be // used as targets for jumps and calls, as well as references to large data // structures that don't fit in quest opcode arguments. // - The quest registers are 32-bit integers referred to as r0-r255. In later // versions, registers may contain floating-point values, in which case // they're referred to as f0-f255 (but they still occupy the same memory as // r0-255). // - The args list is a list of up to 8 32-bit values used for many quest // opcodes in v3 and later. These opcodes are preceded by one or more // arg_push opcodes, which allow scripts the ability to pass values from // immediate data, registers, labels, or even pointers to registers. Opcodes // that use the args list are tagged with F_ARGS below. // - The stack is an array of 32-bit integers (16 of them on v1/v2, 64 of them // on v3/v4), which is used by the call and ret opcodes (which push and pop // offsets into the quest script), but may also be used by the stack_push and // stack_pop opcodes to work with arbitrary data. There is protection from // stack underflows (the caller receives the value 0, or the thread // terminates in case of the ret opcode), but there is no protection from // overflows. // - The quest flags are a per-character array of 1024 single-bit flags saved // with the character data. (On Episode 3, there are 8192 instead.) // - The quest counters are a per-character array of 16 32-bit values saved // with the character data. (On Episode 3, there are 48 instead.) // - The event flags are an array of 0x100 bytes stored in the system file (not // the character file). using AttackData = BattleParamsIndex::AttackData; using ResistData = BattleParamsIndex::ResistData; using MovementData = BattleParamsIndex::MovementData; static const char* name_for_header_episode_number(uint8_t episode) { static const array names = {"Episode1", "Episode2", "Episode4"}; try { return names.at(episode); } catch (const out_of_range&) { return "Episode1 # invalid value in header"; } } static TextEncoding encoding_for_language(Language language) { return ((language == Language::JAPANESE) ? TextEncoding::SJIS : TextEncoding::ISO8859); } static string escape_string(const string& data, TextEncoding encoding = TextEncoding::UTF8) { string decoded; try { switch (encoding) { case TextEncoding::UTF8: decoded = data; break; case TextEncoding::UTF16: case TextEncoding::UTF16_ALWAYS_MARKED: decoded = tt_utf16_to_utf8(data); break; case TextEncoding::SJIS: decoded = tt_sega_sjis_to_utf8(data); break; case TextEncoding::ISO8859: decoded = tt_8859_to_utf8(data); break; case TextEncoding::ASCII: decoded = tt_ascii_to_utf8(data); break; default: return phosg::format_data_string(data); } } catch (const runtime_error&) { return phosg::format_data_string(data); } string ret = "\""; for (char ch : decoded) { if (ch == '\n') { ret += "\\n"; } else if (ch == '\r') { ret += "\\r"; } else if (ch == '\t') { ret += "\\t"; } else if (static_cast(ch) < 0x20) { ret += std::format("\\x{:02X}", ch); } else if (ch == '\'') { ret += "\\\'"; } else if (ch == '\"') { ret += "\\\""; } else { ret += ch; } } ret += "\""; return ret; } 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 = " "; phosg::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, phosg::PrintDataFlags::PRINT_ASCII); phosg::strip_trailing_whitespace(ret); return ret; } struct QuestScriptOpcodeDefinition { struct Argument { enum class Type { LABEL16 = 0, LABEL16_SET, LABEL32, R_REG, W_REG, R_REG_SET, R_REG_SET_FIXED, // Sequence of N consecutive regs W_REG_SET_FIXED, // Sequence of N consecutive regs R_REG32, W_REG32, R_REG32_SET_FIXED, // Sequence of N consecutive regs W_REG32_SET_FIXED, // Sequence of N consecutive regs I8, I16, I32, FLOAT32, CSTRING, }; enum class DataType { NONE = 0, SCRIPT, DATA, CSTRING, PLAYER_STATS, PLAYER_VISUAL_CONFIG, RESIST_DATA, ATTACK_DATA, MOVEMENT_DATA, IMAGE_DATA, BEZIER_CONTROL_POINT_DATA, }; Type type; size_t count; DataType data_type; const char* name; Argument(Type type, size_t count = 0, const char* name = nullptr) : type(type), count(count), data_type(DataType::NONE), name(name) {} Argument(Type type, DataType data_type, const char* name = nullptr) : type(type), count(0), data_type(data_type), name(name) {} }; uint16_t opcode; const char* name; const char* qedit_name; std::vector args; uint16_t flags; QuestScriptOpcodeDefinition( uint16_t opcode, const char* name, const char* qedit_name, std::vector args, uint16_t flags) : opcode(opcode), name(name), qedit_name(qedit_name), args(args), flags(flags) {} std::string str() const { string name_str = this->qedit_name ? std::format("{} (qedit: {})", this->name, this->qedit_name) : this->name; return std::format("{:04X}: {} flags={:04X}", this->opcode, name_str, this->flags); } }; constexpr uint16_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) // 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) // 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_assert(F_DC_NTE == v_flag(Version::DC_NTE)); static_assert(F_DC_112000 == v_flag(Version::DC_11_2000)); static_assert(F_DC_V1 == v_flag(Version::DC_V1)); static_assert(F_DC_V2 == v_flag(Version::DC_V2)); static_assert(F_PC_NTE == v_flag(Version::PC_NTE)); static_assert(F_PC_V2 == v_flag(Version::PC_V2)); static_assert(F_GC_NTE == v_flag(Version::GC_NTE)); static_assert(F_GC_V3 == v_flag(Version::GC_V3)); static_assert(F_GC_EP3TE == v_flag(Version::GC_EP3_NTE)); static_assert(F_GC_EP3 == v_flag(Version::GC_EP3)); static_assert(F_XB_V3 == v_flag(Version::XB_V3)); 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; // clang-format on static constexpr uint16_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. // LABEL16 is a 16-bit index into the function table static constexpr auto LABEL16 = Arg::Type::LABEL16; // LABEL16_SET is a single byte specifying how many labels follow, followed by // that many 16-bit indexes into the function table. static constexpr auto LABEL16_SET = Arg::Type::LABEL16_SET; // LABEL32 is a 32-bit index into the function table static constexpr auto LABEL32 = Arg::Type::LABEL32; // R_REG is a single byte specifying a register number (rXX or fXX) which is // read by the opcode and not modified static constexpr auto R_REG = Arg::Type::R_REG; // W_REG is a single byte specifying a register number (rXX or fXX) which is // written by the opcode (and maybe also read beforehand) static constexpr auto W_REG = Arg::Type::W_REG; // R_REG_SET is a single byte specifying how many registers follow, followed by // that many bytes specifying individual register numbers. static constexpr auto R_REG_SET = Arg::Type::R_REG_SET; // R_REG_SET_FIXED is a single byte specifying a register number, but the // opcode implicitly reads the following registers as well. For example, if an // opcode takes a {REG_SET_FIXED, 4} and the value 100 was passed to that // opcode, only the byte 0x64 would appear in the script data, but the opcode // would use r100, r101, r102, and r103. static constexpr auto R_REG_SET_FIXED = Arg::Type::R_REG_SET_FIXED; // W_REG_SET_FIXED is like R_REG_SET_FIXED, but is used for registers that are // written (and maybe read beforehand) by the opcode. static constexpr auto W_REG_SET_FIXED = Arg::Type::W_REG_SET_FIXED; // [R/W]_REG32 is a 32-bit register number. The high 24 bits are unused. static constexpr auto R_REG32 = Arg::Type::R_REG32; static constexpr auto W_REG32 = Arg::Type::W_REG32; // [RW]_REG32_SET_FIXED is like [RW]_REG_SET_FIXED, but uses a 32-bit register // number. The high 24 bits are unused. static constexpr auto R_REG32_SET_FIXED = Arg::Type::R_REG32_SET_FIXED; static constexpr auto W_REG32_SET_FIXED = Arg::Type::W_REG32_SET_FIXED; // I8, I16, and I32 are unsigned integers of various sizes static constexpr auto I8 = Arg::Type::I8; static constexpr auto I16 = Arg::Type::I16; static constexpr auto I32 = Arg::Type::I32; // FLOAT32 is a standard 32-bit float static constexpr auto FLOAT32 = Arg::Type::FLOAT32; // CSTRING is a sequence of nonzero bytes ending with a zero byte static constexpr auto CSTRING = Arg::Type::CSTRING; // These are shortcuts for the above types with some extra metadata, which the // disassembler uses to annotate arguments or data sections. static const Arg SCRIPT16(LABEL16, Arg::DataType::SCRIPT); static const Arg SCRIPT16_SET(LABEL16_SET, Arg::DataType::SCRIPT); static const Arg SCRIPT32(LABEL32, Arg::DataType::SCRIPT); static const Arg DATA16(LABEL16, Arg::DataType::DATA); static const Arg CSTRING_LABEL16(LABEL16, Arg::DataType::CSTRING); static const Arg CLIENT_ID(I32, 0, "client_id"); static const Arg ITEM_ID(I32, 0, "item_id"); static const Arg FLOOR(I32, 0, "floor"); static const QuestScriptOpcodeDefinition opcode_defs[] = { // The quest opcodes are defined below. Two-byte opcodes begin with F8 or // F9; all other opcodes are one byte. Unlike network commands and // subcommands, all versions use the same values for almost all opcodes // (there is one exception), but not all opcodes are supported on all // versions. The flags denote which versions support each opcode; opcodes // are defined multiple times below if their call signatures are different // across versions. // In the comments below, arguments are referred to with letters. The first // argument to an opcode would be regA (if it's a REG), the second is regB, // etc. The individual registers within a REG_SET_FIXED argument are // referred to as an array, as regsA[0], regsA[1], etc. // Does nothing {0x00, "nop", nullptr, {}, F_V0_V4}, // Pops new PC off stack {0x01, "ret", nullptr, {}, F_V0_V4}, // 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}, // 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}, // 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}, // Copies a value from regB to regA {0x08, "let", nullptr, {W_REG, R_REG}, F_V0_V4}, // Sets regA to valueB {0x09, "leti", nullptr, {W_REG, I32}, F_V0_V4}, // Sets regA to the memory address of regB. Note that this opcode was moved // to 0C in v3 and later. {0x0A, "leta", nullptr, {W_REG, R_REG}, F_V0_V2}, // Sets regA to valueB {0x0A, "letb", nullptr, {W_REG, I8}, F_V3_V4}, // Sets regA to valueB {0x0B, "letw", nullptr, {W_REG, I16}, F_V3_V4}, // Sets regA to the memory address of regB {0x0C, "leta", nullptr, {W_REG, R_REG}, F_V3_V4}, // 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}, // Sets regA to 1 {0x10, "set", nullptr, {W_REG}, F_V0_V4}, // Sets regA to 0 {0x11, "clear", nullptr, {W_REG}, F_V0_V4}, // Sets a regA to 0 if it's nonzero and vice versa {0x12, "rev", nullptr, {W_REG}, F_V0_V4}, // Sets flagA to 1. Sends 6x75. {0x13, "gset", nullptr, {I16}, F_V0_V4}, // Clears flagA to 0. Sends 6x75 on BB, but does not send anything on other // versions. {0x14, "gclear", nullptr, {I16}, F_V0_V4}, // Inverts flagA. Like the above two opcodes, sends 6x75 if the flag is set // by this opcode. Only BB sends 6x75 if the flag is cleared by this // opcode. {0x15, "grev", nullptr, {I16}, F_V0_V4}, // If regB is nonzero, sets flagA; otherwise, clears it {0x16, "glet", nullptr, {I16, R_REG}, F_V0_V4}, // Sets regB to the value of flagA {0x17, "gget", nullptr, {I16, R_REG}, F_V0_V4}, // regA += regB {0x18, "add", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA += valueB {0x19, "addi", nullptr, {W_REG, I32}, F_V0_V4}, // regA -= regB {0x1A, "sub", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA -= valueB {0x1B, "subi", nullptr, {W_REG, I32}, F_V0_V4}, // regA *= regB {0x1C, "mul", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA *= valueB {0x1D, "muli", nullptr, {W_REG, I32}, F_V0_V4}, // regA /= regB {0x1E, "div", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA /= valueB {0x1F, "divi", nullptr, {W_REG, I32}, F_V0_V4}, // regA &= regB {0x20, "and", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA &= valueB {0x21, "andi", nullptr, {W_REG, I32}, F_V0_V4}, // regA |= regB {0x22, "or", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA |= valueB {0x23, "ori", nullptr, {W_REG, I32}, F_V0_V4}, // regA ^= regB {0x24, "xor", nullptr, {W_REG, R_REG}, F_V0_V4}, // regA ^= valueB {0x25, "xori", nullptr, {W_REG, I32}, F_V0_V4}, // regA %= regB // Note: This does signed division, so if the value is negative, you might // get unexpected results. {0x26, "mod", nullptr, {W_REG, R_REG}, F_V3_V4}, // regA %= valueB // Note: Unlike mod, this does unsigned division. {0x27, "modi", nullptr, {W_REG, I32}, F_V3_V4}, // Jumps to labelA {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4}, // Pushes the script offset immediately after this opcode and jumps to // labelA {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4}, // If all values in regsB are nonzero, jumps to labelA {0x2A, "jmp_on", nullptr, {SCRIPT16, R_REG_SET}, F_V0_V4}, // If all values in regsB are zero, jumps to labelA {0x2B, "jmp_off", nullptr, {SCRIPT16, R_REG_SET}, F_V0_V4}, // If regA == regB, jumps to labelC {0x2C, "jmp_eq", "jmp_=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA == valueB, jumps to labelC {0x2D, "jmpi_eq", "jmpi_=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA != regB, jumps to labelC {0x2E, "jmp_ne", "jmp_!=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA != valueB, jumps to labelC {0x2F, "jmpi_ne", "jmpi_!=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA > regB (unsigned), jumps to labelC {0x30, "ujmp_gt", "ujmp_>", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (unsigned), jumps to labelC {0x31, "ujmpi_gt", "ujmpi_>", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA > regB (signed), jumps to labelC {0x32, "jmp_gt", "jmp_>", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (signed), jumps to labelC {0x33, "jmpi_gt", "jmpi_>", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA < regB (unsigned), jumps to labelC {0x34, "ujmp_lt", "ujmp_<", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (unsigned), jumps to labelC {0x35, "ujmpi_lt", "ujmpi_<", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA < regB (signed), jumps to labelC {0x36, "jmp_lt", "jmp_<", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (signed), jumps to labelC {0x37, "jmpi_lt", "jmpi_<", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA >= regB (unsigned), jumps to labelC {0x38, "ujmp_ge", "ujmp_>=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (unsigned), jumps to labelC {0x39, "ujmpi_ge", "ujmpi_>=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA >= regB (signed), jumps to labelC {0x3A, "jmp_ge", "jmp_>=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (signed), jumps to labelC {0x3B, "jmpi_ge", "jmpi_>=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA <= regB (unsigned), jumps to labelC {0x3C, "ujmp_le", "ujmp_<=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (unsigned), jumps to labelC {0x3D, "ujmpi_le", "ujmpi_<=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // If regA <= regB (signed), jumps to labelC {0x3E, "jmp_le", "jmp_<=", {R_REG, R_REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (signed), jumps to labelC {0x3F, "jmpi_le", "jmpi_<=", {R_REG, I32, SCRIPT16}, F_V0_V4}, // Jumps to labelsB[regA] {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}, // Does nothing {0x42, "nop_42", nullptr, {I32}, F_V0_V2}, // Pushes the value in regA to the stack {0x42, "stack_push", nullptr, {R_REG}, F_V3_V4}, // Pops a value from the stack and puts it into regA {0x43, "stack_pop", nullptr, {W_REG}, F_V3_V4}, // Pushes (valueB) regs in increasing order starting at regA {0x44, "stack_pushm", nullptr, {R_REG, I32}, F_V3_V4}, // Pops (valueB) regs in decreasing order ending at regA {0x45, "stack_popm", nullptr, {W_REG, I32}, F_V3_V4}, // Appends regA to the args list {0x48, "arg_pushr", nullptr, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends valueA to the args list {0x49, "arg_pushl", nullptr, {I32}, F_V3_V4 | F_PUSH_ARG}, {0x4A, "arg_pushb", nullptr, {I8}, F_V3_V4 | F_PUSH_ARG}, {0x4B, "arg_pushw", nullptr, {I16}, F_V3_V4 | F_PUSH_ARG}, // Appends the memory address of regA to the args list {0x4C, "arg_pusha", nullptr, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends the script offset of labelA to the args list {0x4D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PUSH_ARG}, // Appends strA to the args list {0x4E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PUSH_ARG}, // Creates dialogue with an object/NPC (valueA) starting with message strB {0x50, "message", nullptr, {I32, CSTRING}, F_V0_V4 | F_ARGS}, // Prompts the player with a list of choices (strB; items separated by // newlines) and returns the index of their choice in regA {0x51, "list", nullptr, {W_REG, CSTRING}, F_V0_V4 | F_ARGS}, // Fades from black {0x52, "fadein", nullptr, {}, F_V0_V4}, // Fades to black {0x53, "fadeout", nullptr, {}, F_V0_V4}, // Plays a sound effect {0x54, "sound_effect", "se", {I32}, F_V0_V4 | F_ARGS}, // Plays a fanfare (clear.adx if valueA is 0, or miniclear.adx if it's 1). // Note: There is no bounds check on this; values other than 0 or 1 will // result in undefined behavior. {0x55, "bgm", nullptr, {I32}, F_V0_V4 | F_ARGS}, // Does nothing {0x56, "nop_56", nullptr, {}, F_V0_V2}, {0x57, "nop_57", nullptr, {}, F_V0_V2}, {0x58, "nop_58", "enable", {I32}, F_V0_V2}, {0x59, "nop_59", "disable", {I32}, F_V0_V2}, // Displays a message. Special tokens are interpolated within the string. // These special tokens are: // => value of rXX as %d (signed integer) // => value of rXX as %f (floating-point) (v3 and later) // => changes text color like $CX would (supported on 11/2000 and // later); X must be numeric and in the range 0-7, so , , and do not work (though \tC8, \tC9, and \tCG can be used // directly in the text, and do work) // => newline // or => character's name // or => character's class //