#include "QuestScript.hh" #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_RESOURCE_FILE #include #include #include #endif #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. This is a stream of binary data containing opcodes (as // defined below), each followed by their arguments. // - The function table. This 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 registers. There are 256 registers, 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. This is a list of up to 8 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. This is an array of 64 32-bit integers, 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. // - Quest flags. These are a per-character array of 1024 single-bit flags // saved with the character data. (On Episode 3, there are 8192 instead.) // - Quest counters. These are a per-character array of 16 32-bit values saved // with the character data. (On Episode 3, there are 48 instead.) // - Event flags. These 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; struct Vector4F { le_float x; le_float y; le_float z; le_float t; } __packed_ws__(Vector4F, 0x10); // bit_cast isn't in the standard place on macOS (it is apparently implicitly // included by resource_dasm, but newserv can be built without resource_dasm) // and I'm too lazy to go find the right header to include template ToT as_type(const FromT& v) { static_assert(sizeof(FromT) == sizeof(ToT), "types are not the same size"); ToT ret; memcpy(&ret, &v, sizeof(ToT)); return ret; } 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(uint8_t language) { return (language ? TextEncoding::ISO8859 : TextEncoding::SJIS); } 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 += phosg::string_printf("\\x%02hhX", 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, REG, REG_SET, REG_SET_FIXED, // Sequence of N consecutive regs REG32, REG32_SET_FIXED, // Sequence of N consecutive regs INT8, INT16, INT32, 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 ? phosg::string_printf("%s (qedit: %s)", this->name, this->qedit_name) : this->name; return phosg::string_printf("%04hX: %s flags=%04hX", this->opcode, name_str.c_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"); // F_PASS means the argument list isn't cleared after this opcode executes static constexpr uint16_t F_PASS = 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_V1_11_2000_PROTOTYPE 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 // This flag specifies that the opcode ends a function (returns). static constexpr uint16_t F_RET = 0x4000; // This flag specifies that the opcode sets the current episode. This is used // to automatically detect a quest's episode from its script. static constexpr uint16_t F_SET_EPISODE = 0x8000; static_assert(F_DC_NTE == v_flag(Version::DC_NTE)); static_assert(F_DC_112000 == v_flag(Version::DC_V1_11_2000_PROTOTYPE)); 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; // REG is a single byte specifying a register number (rXX or fXX) static constexpr auto REG = Arg::Type::REG; // REG_SET is a single byte specifying how many registers follow, followed by // that many bytes specifying individual register numbers. static constexpr auto REG_SET = Arg::Type::REG_SET; // REG_SET_FIXED is a single byte specifying a register number, but the opcode // implicitly uses 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 REG_SET_FIXED = Arg::Type::REG_SET_FIXED; // REG32 is a 32-bit register number. The high 24 bits are unused. static constexpr auto REG32 = Arg::Type::REG32; // REG32_SET_FIXED is like REG_SET_FIXED, but uses a 32-bit register number. // The high 24 bits are unused. static constexpr auto REG32_SET_FIXED = Arg::Type::REG32_SET_FIXED; // INT8, INT16, and INT32 are unsigned integers of various sizes static constexpr auto INT8 = Arg::Type::INT8; static constexpr auto INT16 = Arg::Type::INT16; static constexpr auto INT32 = Arg::Type::INT32; // 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(INT32, 0, "client_id"); static const Arg ITEM_ID(INT32, 0, "item_id"); static const Arg FLOOR(INT32, 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 | F_RET}, // 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, {INT32}, 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 stack, then calls labelA {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4}, // Copies a value from regB to regA {0x08, "let", nullptr, {REG, REG}, F_V0_V4}, // Sets regA to valueB {0x09, "leti", nullptr, {REG, INT32}, 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, {REG, REG}, F_V0_V2}, // Sets regA to valueB {0x0A, "letb", nullptr, {REG, INT8}, F_V3_V4}, // Sets regA to valueB {0x0B, "letw", nullptr, {REG, INT16}, F_V3_V4}, // Sets regA to the memory address of regB {0x0C, "leta", nullptr, {REG, REG}, F_V3_V4}, // Sets regA to the address of labelB {0x0D, "leto", nullptr, {REG, SCRIPT16}, F_V3_V4}, // Sets regA to 1 {0x10, "set", nullptr, {REG}, F_V0_V4}, // Sets regA to 0 {0x11, "clear", nullptr, {REG}, F_V0_V4}, // Sets a regA to 0 if it's nonzero and vice versa {0x12, "rev", nullptr, {REG}, F_V0_V4}, // Sets flagA to 1 {0x13, "gset", nullptr, {INT16}, F_V0_V4}, // Clears flagA to 0 {0x14, "gclear", nullptr, {INT16}, F_V0_V4}, // Inverts flagA {0x15, "grev", nullptr, {INT16}, F_V0_V4}, // If regB is nonzero, sets flagA; otherwise, clears it {0x16, "glet", nullptr, {INT16, REG}, F_V0_V4}, // Sets regB to the value of flagA {0x17, "gget", nullptr, {INT16, REG}, F_V0_V4}, // regA += regB {0x18, "add", nullptr, {REG, REG}, F_V0_V4}, // regA += valueB {0x19, "addi", nullptr, {REG, INT32}, F_V0_V4}, // regA -= regB {0x1A, "sub", nullptr, {REG, REG}, F_V0_V4}, // regA -= valueB {0x1B, "subi", nullptr, {REG, INT32}, F_V0_V4}, // regA *= regB {0x1C, "mul", nullptr, {REG, REG}, F_V0_V4}, // regA *= valueB {0x1D, "muli", nullptr, {REG, INT32}, F_V0_V4}, // regA /= regB {0x1E, "div", nullptr, {REG, REG}, F_V0_V4}, // regA /= valueB {0x1F, "divi", nullptr, {REG, INT32}, F_V0_V4}, // regA &= regB {0x20, "and", nullptr, {REG, REG}, F_V0_V4}, // regA &= valueB {0x21, "andi", nullptr, {REG, INT32}, F_V0_V4}, // regA |= regB {0x22, "or", nullptr, {REG, REG}, F_V0_V4}, // regA |= valueB {0x23, "ori", nullptr, {REG, INT32}, F_V0_V4}, // regA ^= regB {0x24, "xor", nullptr, {REG, REG}, F_V0_V4}, // regA ^= valueB {0x25, "xori", nullptr, {REG, INT32}, F_V0_V4}, // regA %= regB {0x26, "mod", nullptr, {REG, REG}, F_V3_V4}, // regA %= valueB {0x27, "modi", nullptr, {REG, INT32}, 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, REG_SET}, F_V0_V4}, // If all values in regsB are zero, jumps to labelA {0x2B, "jmp_off", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, // If regA == regB, jumps to labelC {0x2C, "jmp_eq", "jmp_=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA == valueB, jumps to labelC {0x2D, "jmpi_eq", "jmpi_=", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA != regB, jumps to labelC {0x2E, "jmp_ne", "jmp_!=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA != valueB, jumps to labelC {0x2F, "jmpi_ne", "jmpi_!=", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA > regB (unsigned), jumps to labelC {0x30, "ujmp_gt", "ujmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (unsigned), jumps to labelC {0x31, "ujmpi_gt", "ujmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA > regB (signed), jumps to labelC {0x32, "jmp_gt", "jmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA > valueB (signed), jumps to labelC {0x33, "jmpi_gt", "jmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA < regB (unsigned), jumps to labelC {0x34, "ujmp_lt", "ujmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (unsigned), jumps to labelC {0x35, "ujmpi_lt", "ujmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA < regB (signed), jumps to labelC {0x36, "jmp_lt", "jmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA < valueB (signed), jumps to labelC {0x37, "jmpi_lt", "jmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA >= regB (unsigned), jumps to labelC {0x38, "ujmp_ge", "ujmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (unsigned), jumps to labelC {0x39, "ujmpi_ge", "ujmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA >= regB (signed), jumps to labelC {0x3A, "jmp_ge", "jmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA >= valueB (signed), jumps to labelC {0x3B, "jmpi_ge", "jmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA <= regB (unsigned), jumps to labelC {0x3C, "ujmp_le", "ujmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (unsigned), jumps to labelC {0x3D, "ujmpi_le", "ujmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, // If regA <= regB (signed), jumps to labelC {0x3E, "jmp_le", "jmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, // If regA <= valueB (signed), jumps to labelC {0x3F, "jmpi_le", "jmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, // Jumps to labelsB[regA] {0x40, "switch_jmp", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, // Calls labelsB[regA] {0x41, "switch_call", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, // Does nothing {0x42, "nop_42", nullptr, {INT32}, F_V0_V2}, // Pushes the value in regA to the stack {0x42, "stack_push", nullptr, {REG}, F_V3_V4}, // Pops a value from the stack and puts it into regA {0x43, "stack_pop", nullptr, {REG}, F_V3_V4}, // Pushes (valueB) regs in increasing order starting at regA {0x44, "stack_pushm", nullptr, {REG, INT32}, F_V3_V4}, // Pops (valueB) regs in decreasing order ending at regA {0x45, "stack_popm", nullptr, {REG, INT32}, F_V3_V4}, // Appends regA to the args list {0x48, "arg_pushr", nullptr, {REG}, F_V3_V4 | F_PASS}, // Appends valueA to the args list {0x49, "arg_pushl", nullptr, {INT32}, F_V3_V4 | F_PASS}, {0x4A, "arg_pushb", nullptr, {INT8}, F_V3_V4 | F_PASS}, {0x4B, "arg_pushw", nullptr, {INT16}, F_V3_V4 | F_PASS}, // Appends the memory address of regA to the args list {0x4C, "arg_pusha", nullptr, {REG}, F_V3_V4 | F_PASS}, // Appends the script offset of labelA to the args list {0x4D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PASS}, // Appends strA to the args list {0x4E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PASS}, // Creates dialogue with an object/NPC (valueA) starting with message strB {0x50, "message", nullptr, {INT32, 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, {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", {INT32}, 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, {INT32}, 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", {INT32}, F_V0_V2}, {0x59, "nop_59", "disable", {INT32}, 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, so does not work // => newline // or => character's name // or => character's class //