#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" // 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.) Each character has independent sets of quest flags for each difficulty level. // - 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.) Unlike quest flags, there is only one set of quest counters, used for all difficulties. // - 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 TextEncoding encoding_for_language(Language language) { return ((language == Language::JAPANESE) ? TextEncoding::SJIS : TextEncoding::ISO8859); } static std::string escape_string(const std::string& data, TextEncoding encoding = TextEncoding::UTF8) { std::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 std::runtime_error&) { return phosg::format_data_string(data); } std::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 if (ch == '\\') { ret += "\\\\"; } else { ret += ch; } } ret += "\""; return ret; } static std::string format_and_indent_data(const void* data, size_t size, uint64_t start_address) { std::string ret = " "; auto write_fn = [&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]); } } }; phosg::format_data_custom(write_fn, data, size, start_address, phosg::FormatDataFlags::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, VECTOR4F_LIST, }; 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; // By convention, the first of these names is the canonical name (used when disassembling) and the last is the Qedit // name. When assembling, all names in this list are considered valid. std::vector names; std::vector args; uint32_t flags; std::string str() const { return std::format("{:04X}: {} flags={:04X}", this->opcode, phosg::join(this->names, "/"), this->flags); } }; 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 uint32_t F_PUSH_ARG = 0x00010000; static constexpr uint32_t F_CLEAR_ARGS = 0x00020000; // F_ARGS means this opcode uses the argument list on v3 and later; it has no effect on v2 and earlier 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 uint32_t F_DC_NTE = 0x00000004; // Version::DC_NTE static constexpr uint32_t F_DC_112000 = 0x00000008; // Version::DC_11_2000 static constexpr uint32_t F_DC_V1 = 0x00000010; // Version::DC_V1 static constexpr uint32_t F_DC_V2 = 0x00000020; // Version::DC_V2 static constexpr uint32_t F_PC_NTE = 0x00000040; // Version::PC_NTE static constexpr uint32_t F_PC_V2 = 0x00000080; // Version::PC_V2 static constexpr uint32_t F_GC_NTE = 0x00000100; // Version::GC_NTE static constexpr uint32_t F_GC_V3 = 0x00000200; // Version::GC_V3 static constexpr uint32_t F_GC_EP3TE = 0x00000400; // Version::GC_EP3_NTE static constexpr uint32_t F_GC_EP3 = 0x00000800; // Version::GC_EP3 static constexpr uint32_t F_XB_V3 = 0x00001000; // Version::XB_V3 static constexpr uint32_t F_BB_V4 = 0x00002000; // 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 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 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. // 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 W_REG or R_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"}, {}, F_V0_V4}, // Pops new PC off stack {0x01, {"ret"}, {}, F_V0_V4 | F_TERMINATOR}, // Stops execution for the current frame. Execution resumes immediately after this opcode on the next frame. {0x02, {"sync"}, {}, F_V0_V4}, // Exits entirely {0x03, {"exit"}, {I32}, F_V0_V4 | F_TERMINATOR}, // Starts a new thread at labelA {0x04, {"thread"}, {SCRIPT16}, F_V0_V4}, // Pushes r1-r7 to the stack {0x05, {"va_start"}, {}, F_V3_V4 | F_CLEAR_ARGS}, // Pops r7-r1 from the stack {0x06, {"va_end"}, {}, F_V3_V4}, // Replaces r1-r7 with the args list, then calls labelA. This opcode doesn't directly clear the args list, but we // assume during disassembly that the code being called does so. {0x07, {"va_call"}, {SCRIPT16}, F_V3_V4 | F_CLEAR_ARGS}, // Copies a value from regB to regA {0x08, {"let"}, {W_REG, R_REG}, F_V0_V4}, // Sets regA to valueB {0x09, {"leti"}, {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"}, {W_REG, R_REG}, F_V0_V2}, // Sets regA to valueB {0x0A, {"letb"}, {W_REG, I8}, F_V3_V4}, // Sets regA to valueB {0x0B, {"letw"}, {W_REG, I16}, F_V3_V4}, // Sets regA to the memory address of regB {0x0C, {"leta"}, {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"}, {W_REG, LABEL16}, F_V3_V4}, // Sets regA to 1 {0x10, {"set"}, {W_REG}, F_V0_V4}, // Sets regA to 0 {0x11, {"clear"}, {W_REG}, F_V0_V4}, // Sets a regA to 0 if it's nonzero and vice versa {0x12, {"rev"}, {W_REG}, F_V0_V4}, // Sets flagA to 1. Sends 6x75. {0x13, {"gset"}, {I16}, F_V0_V4}, // Clears flagA to 0. Sends 6x75 on BB, but does not send anything on other versions. {0x14, {"gclear"}, {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"}, {I16}, F_V0_V4}, // If regB is nonzero, sets flagA; otherwise, clears it {0x16, {"glet"}, {I16, R_REG}, F_V0_V4}, // Sets regB to the value of flagA {0x17, {"gget"}, {I16, R_REG}, F_V0_V4}, // regA += regB {0x18, {"add"}, {W_REG, R_REG}, F_V0_V4}, // regA += valueB {0x19, {"addi"}, {W_REG, I32}, F_V0_V4}, // regA -= regB {0x1A, {"sub"}, {W_REG, R_REG}, F_V0_V4}, // regA -= valueB {0x1B, {"subi"}, {W_REG, I32}, F_V0_V4}, // regA *= regB {0x1C, {"mul"}, {W_REG, R_REG}, F_V0_V4}, // regA *= valueB {0x1D, {"muli"}, {W_REG, I32}, F_V0_V4}, // regA /= regB {0x1E, {"div"}, {W_REG, R_REG}, F_V0_V4}, // regA /= valueB {0x1F, {"divi"}, {W_REG, I32}, F_V0_V4}, // regA &= regB {0x20, {"and"}, {W_REG, R_REG}, F_V0_V4}, // regA &= valueB {0x21, {"andi"}, {W_REG, I32}, F_V0_V4}, // regA |= regB {0x22, {"or"}, {W_REG, R_REG}, F_V0_V4}, // regA |= valueB {0x23, {"ori"}, {W_REG, I32}, F_V0_V4}, // regA ^= regB {0x24, {"xor"}, {W_REG, R_REG}, F_V0_V4}, // regA ^= valueB {0x25, {"xori"}, {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"}, {W_REG, R_REG}, F_V3_V4}, // regA %= valueB // Note: Unlike mod, this does unsigned division. {0x27, {"modi"}, {W_REG, I32}, F_V3_V4}, // Jumps to labelA {0x28, {"jmp"}, {SCRIPT16}, F_V0_V4 | F_TERMINATOR}, // Pushes the script offset immediately after this opcode and jumps to labelA // Note: This opcode doesn't directly clear the args list, but we assume during disassembly that the code being // called does so. {0x29, {"call"}, {SCRIPT16}, F_V0_V4 | F_CLEAR_ARGS}, // If all values in regsB are nonzero, jumps to labelA {0x2A, {"jmp_on"}, {SCRIPT16, R_REG_SET}, F_V0_V4}, // If all values in regsB are zero, jumps to labelA {0x2B, {"jmp_off"}, {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]; if regA is out of range of labelsB, does nothing {0x40, {"switch_jmp"}, {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"}, {R_REG, SCRIPT16_SET}, F_V0_V4 | F_CLEAR_ARGS}, // Does nothing {0x42, {"nop_42"}, {I32}, F_V0_V2}, // Pushes the value in regA to the stack {0x42, {"stack_push"}, {R_REG}, F_V3_V4}, // Pops a value from the stack and puts it into regA {0x43, {"stack_pop"}, {W_REG}, F_V3_V4}, // Pushes (valueB) regs in increasing order starting at regA {0x44, {"stack_pushm"}, {R_REG, I32}, F_V3_V4}, // Pops (valueB) regs in decreasing order ending at regA {0x45, {"stack_popm"}, {W_REG, I32}, F_V3_V4}, // Appends regA to the args list {0x48, {"arg_pushr"}, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends valueA to the args list {0x49, {"arg_pushl"}, {I32}, F_V3_V4 | F_PUSH_ARG}, {0x4A, {"arg_pushb"}, {I8}, F_V3_V4 | F_PUSH_ARG}, {0x4B, {"arg_pushw"}, {I16}, F_V3_V4 | F_PUSH_ARG}, // Appends the memory address of regA to the args list {0x4C, {"arg_pusha"}, {R_REG}, F_V3_V4 | F_PUSH_ARG}, // Appends the script offset of labelA to the args list {0x4D, {"arg_pusho"}, {LABEL16}, F_V3_V4 | F_PUSH_ARG}, // Appends strA to the args list {0x4E, {"arg_pushs"}, {CSTRING}, F_V3_V4 | F_PUSH_ARG}, // Creates dialogue with an object/NPC (valueA) starting with message strB {0x50, {"message"}, {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"}, {W_REG, CSTRING}, F_V0_V4 | F_ARGS}, // Fades from black {0x52, {"fadein"}, {}, F_V0_V4}, // Fades to black {0x53, {"fadeout"}, {}, 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). There is no bounds check on this; values // other than 0 or 1 will result in undefined behavior. {0x55, {"bgm"}, {I32}, F_V0_V4 | F_ARGS}, // Does nothing {0x56, {"nop_56"}, {}, F_V0_V2}, {0x57, {"nop_57"}, {}, 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 //