diff --git a/README.md b/README.md index b564b633..939ccb76 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ See TODO.md for a list of known issues and future work I've curated, or go to th * [History](#history) * [Other server projects](#other-server-projects) * [Using newserv in other projects](#using-newserv-in-other-projects) + * [Developer information](#developer-information) * [Compatibility](#compatibility) * Setup * [Server setup](#server-setup) @@ -65,6 +66,20 @@ Independently of this project, there are many other PSO servers out there. Those * (2021) **[Phantasmal World](https://github.com/DaanVandenBosch/phantasmal-world)**: A set of PSO tools, including a web-based model viewer and quest builder, and a PSO server, written by Daan Vanden Bosch. * (2021) **[Elseware](http://git.sharnoth.com/jake/elseware)**: A PSOBB server written in Rust by Jake. +## Developer information + +There is a lot of code in this project that could be useful as a reference. Some of the more notable files are: +* **src/CommandFormats.hh**: Complete listing of all network commands used in all known versions of the game, and their formats +* **src/DCSerialNumbers.hh/cc**: PSO DC serial number validation algorithm and serial number generator +* **src/ItemData.hh**: Item format reference +* **src/ItemCreator.hh/cc**: Reverse-engineered item generator from Episodes 1&2 (used for all versions) +* **src/ItemParameterTable.hh**: Format of many structures in ItemPMT.prs +* **src/Map.hh/cc**: Map file (.dat) structure and reverse-engineered Challenge Mode random enemy generation algorithm +* **src/QuestScript.cc**: Complete listing of all quest opcodes on all versions, along with their arguments and behavior +* **src/SaveFileFormats.hh**: Definitions of save file structures for all versions +* **src/Episode3/DataIndexes.hh**: Episode 3 file structures, including card definition format and map/quest format +* **system/item-tables/names-v4.json**: Names of all items, indexed by the first 3 bytes of data1 + ## Using newserv in other projects There is a fair amount of code in this project that could potentially be useful to other projects. You are free to use code from newserv in your own open-source projects; the only condition is that the contents of the LICENSE file must be included in your project if you use code from newserv. Your project does not also have to use the MIT license; you can use any license you want. diff --git a/TODO.md b/TODO.md index 13c78367..a423353b 100644 --- a/TODO.md +++ b/TODO.md @@ -9,6 +9,7 @@ ## PSO DC - Investigate if https://crates.io/crates/blaze-ssl-async can be used to implement the HL check server +- Does DCv1 support battle mode? The flag exists on the client; try this out ## Episode 3 diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 50b2ef2b..3c2edda5 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -882,7 +882,7 @@ struct SC_GameGuardCheck_BB_0022 { struct S_ExchangeSecretLotteryTicketResult_BB_24 { // These fields map to unknown_a1 and unknown_a2 in the 6xDE command (but // their order is swapped here). - le_uint16_t function_id = 0; + le_uint16_t label = 0; uint8_t start_index = 0; uint8_t unused = 0; parray unknown_a3; @@ -892,7 +892,7 @@ struct S_ExchangeSecretLotteryTicketResult_BB_24 { // Sent in response to a 6xE1 command from the client. struct S_GallonPlanResult_BB_25 { - le_uint16_t function_id = 0; + le_uint16_t label = 0; uint8_t offset1 = 0; uint8_t offset2 = 0; uint8_t value1 = 0; @@ -1227,20 +1227,21 @@ struct S_JoinGameT_DC_PC { // field when sending this command to start an Episode 3 tournament game. This // can be misleading when reading old logs from those days, but the Episode 3 // client really does ignore it. - parray variations; + /* 0004 */ parray variations; // Unlike lobby join commands, these are filled in in their slot positions. // That is, if there's only one player in a game with ID 2, then the first two // of these are blank and the player's data is in the third entry here. - parray lobby_data; - uint8_t client_id = 0; - uint8_t leader_id = 0; - uint8_t disable_udp = 1; - uint8_t difficulty = 0; - uint8_t battle_mode = 0; - uint8_t event = 0; - uint8_t section_id = 0; - uint8_t challenge_mode = 0; - le_uint32_t rare_seed = 0; + /* 0084 */ parray lobby_data; + /* 0104 */ uint8_t client_id = 0; + /* 0105 */ uint8_t leader_id = 0; + /* 0106 */ uint8_t disable_udp = 1; + /* 0107 */ uint8_t difficulty = 0; + /* 0108 */ uint8_t battle_mode = 0; + /* 0109 */ uint8_t event = 0; + /* 010A */ uint8_t section_id = 0; + /* 010B */ uint8_t challenge_mode = 0; + /* 010C */ le_uint32_t rare_seed = 0; + /* 0110 */ } __packed__; struct S_JoinGame_DCNTE_64 { @@ -1454,6 +1455,8 @@ struct S_GenerateID_DC_PC_V3_80 { // On GC (and probably other versions too) the unused space after the text // contains uninitialized memory when the client sends this command. newserv // clears the uninitialized data for security reasons before forwarding. +// The maximum length of the message is 170 characters, despite the field being +// able to hold triple that amount. struct SC_SimpleMail_PC_81 { // If player_tag and from_guild_card_number are zero, the message cannot be @@ -2131,10 +2134,10 @@ struct S_QuestMenuEntry_BB_A2_A4 { // because the following command (AB) is definitely not valid on that version. struct C_SendQuestStatistic_V3_BB_AA { - le_uint16_t stat_id1 = 0; + le_uint16_t stat_id = 0; le_uint16_t unused = 0; - le_uint16_t function_id1 = 0; - le_uint16_t function_id2 = 0; + le_uint16_t label1 = 0; + le_uint16_t label2 = 0; parray params; } __packed_ws__(C_SendQuestStatistic_V3_BB_AA, 0x28); @@ -2146,7 +2149,7 @@ struct C_SendQuestStatistic_V3_BB_AA { // presumably use this command to call any function at any time during a quest. struct S_CallQuestFunction_V3_BB_AB { - le_uint16_t function_id = 0; + le_uint16_t label = 0; parray unused; } __packed_ws__(S_CallQuestFunction_V3_BB_AB, 4); @@ -4105,17 +4108,17 @@ struct G_DarkFalzActions_6x19 { // 6x1A: Invalid subcommand -// 6x1B: Unknown (not valid on Episode 3) (protected on V3/V4) +// 6x1B: Enable PK mode for player (not valid on Episode 3) (protected on V3/V4) -struct G_Unknown_6x1B { +struct G_EnablePKModeForPlayer_6x1B { G_ClientIDHeader header; -} __packed_ws__(G_Unknown_6x1B, 4); +} __packed_ws__(G_EnablePKModeForPlayer_6x1B, 4); -// 6x1C: Destroy NPC (protected on V3/V4) +// 6x1C: Disable PK mode for player (protected on V3/V4) -struct G_DestroyNPC_6x1C { +struct G_DisablePKModeForPlayer_6x1C { G_ClientIDHeader header; -} __packed_ws__(G_DestroyNPC_6x1C, 4); +} __packed_ws__(G_DisablePKModeForPlayer_6x1C, 4); // 6x1D: Invalid subcommand // 6x1E: Invalid subcommand @@ -4753,6 +4756,24 @@ struct G_SetTelepipeState_6x68 { // 6x69: NPC control // Note: NPCs cannot be destroyed with 6x69; 6x1C is used instead for that. +// For commands 0 and 3, the template indexes are: +// 0: NOL 1: CICIL 2: CICIL 3: MARACA 4: ELLY +// 5: SHINO 6: DONOPH 7: MOME 8: ALICIA 9: ASH +// 10: ASH 11: SUE 12: KIREEK 13: BERNIE 14: GILLIAM +// 15: ELENOR 16: ALICIA 17: MONTAGUE 18: RUPIKA 19: MATHA +// 20: ANNA 21: TONZLAR 22: TOBOKKE 23: GEKIGASKY 24: TYPE:O +// 25: TYPE:W 26: GIZEL 27: DACCI 28: HOPKINS 29: DORONBO +// 30: KROE 31: MUJO 32: RACTON 33: LIONEL 34: ZOKE +// 35: SUE 36: NADJA 37: ELENOR 38: KIREEK 39: BERNIE +// 40: CHRIS 41: RENEE 42: KAREN 43: BEIRON 44: NAKA +// 45: LEO 46: HOUND 47: MADELEINE 48: VALLETTA 49: BOGARDE +// 50: ULT 51: TYPE:I 52: TYPE:V 53: TACHIBANA 54: OSMAN +// 55: VIVIENNE 56: BP 57: SHINTARO 58: KEN 59: TAKUYA +// 60: SOKON 61: UKON 62: CANTONA 63: HASE +// Created NPCs have a base level according to their template index. On Hard, +// 25 is added to their base level; on Very Hard, 50 is added; on Ultimate, 150 +// is added. In all cases the NPC's level is clamped to [1, 199]. + struct G_NPCControl_6x69 { G_UnusedHeader header; le_uint16_t param1; // Commands 0/3: state; command 1: npc_entity_id; command 2: unknown @@ -5219,17 +5240,17 @@ struct G_TriggerTrap_6x80 { le_uint16_t unknown_a2 = 0; } __packed_ws__(G_TriggerTrap_6x80, 8); -// 6x81: Set drop weapon on death flag (protected on V3/V4) +// 6x81: Disable drop weapon on death (protected on V3/V4) -struct G_SetDropWeaponOnDeathFlag_6x81 { +struct G_DisableDropWeaponOnDeath_6x81 { G_ClientIDHeader header; -} __packed_ws__(G_SetDropWeaponOnDeathFlag_6x81, 4); +} __packed_ws__(G_DisableDropWeaponOnDeath_6x81, 4); -// 6x82: Clear drop weapon on death flag (protected on V3/V4) +// 6x82: Enable drop weapon on death (protected on V3/V4) -struct G_ClearDropWeaponOnDeathFlag_6x82 { +struct G_EnableDropWeaponOnDeath_6x82 { G_ClientIDHeader header; -} __packed_ws__(G_ClearDropWeaponOnDeathFlag_6x82, 4); +} __packed_ws__(G_EnableDropWeaponOnDeath_6x82, 4); // 6x83: Place trap (protected on V3/V4) @@ -5445,7 +5466,7 @@ struct G_Unknown_6x9C { struct G_Unknown_6x9D { G_UnusedHeader header; - le_uint32_t client_id2 = 0; + le_uint32_t client_id = 0; } __packed_ws__(G_Unknown_6x9D, 8); // 6x9E: Play camera shutter sound @@ -6084,8 +6105,8 @@ struct G_ExchangeItemInQuest_BB_6xD5 { G_ClientIDHeader header; ItemData find_item; // Only data1[0]-[2] are used ItemData replace_item; // Only data1[0]-[2] are used - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; + le_uint16_t success_label = 0; + le_uint16_t failure_label = 0; } __packed_ws__(G_ExchangeItemInQuest_BB_6xD5, 0x30); // 6xD6: Wrap item (BB; handled by server) @@ -6093,7 +6114,7 @@ struct G_ExchangeItemInQuest_BB_6xD5 { struct G_WrapItem_BB_6xD6 { G_ClientIDHeader header; ItemData item; - uint8_t unknown_a1 = 0; + uint8_t present_color = 0; // 00-0F parray unused; } __packed_ws__(G_WrapItem_BB_6xD6, 0x1C); @@ -6103,8 +6124,8 @@ struct G_WrapItem_BB_6xD6 { struct G_PaganiniPhotonDropExchange_BB_6xD7 { G_ClientIDHeader header; ItemData new_item; // Only data1[0]-[2] are used - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; + le_uint16_t success_label = 0; + le_uint16_t failure_label = 0; } __packed_ws__(G_PaganiniPhotonDropExchange_BB_6xD7, 0x1C); // 6xD8: Add S-rank weapon special (BB; handled by server) @@ -6115,8 +6136,8 @@ struct G_AddSRankWeaponSpecial_BB_6xD8 { ItemData unknown_a1; // Only data1[0]-[2] are used le_uint32_t item_id = 0; le_uint32_t special_type = 0; - le_uint16_t success_function_id = 0; - le_uint16_t failure_function_id = 0; + le_uint16_t success_label = 0; + le_uint16_t failure_label = 0; } __packed_ws__(G_AddSRankWeaponSpecial_BB_6xD8, 0x24); // 6xD9: Momoka item exchange (BB; handled by server) @@ -6128,8 +6149,8 @@ struct G_MomokaItemExchange_BB_6xD9 { ItemData replace_item; // Only data1[0]-[2] are used le_uint32_t unknown_a3 = 0; le_uint32_t unknown_a4 = 0; - le_uint16_t unknown_a5 = 0; - le_uint16_t unknown_a6 = 0; + le_uint16_t success_label = 0; + le_uint16_t failure_label = 0; } __packed_ws__(G_MomokaItemExchange_BB_6xD9, 0x38); // 6xDA: Upgrade weapon attribute (BB; handled by server) @@ -6137,13 +6158,13 @@ struct G_MomokaItemExchange_BB_6xD9 { struct G_UpgradeWeaponAttribute_BB_6xDA { G_ClientIDHeader header; - ItemData item; // Only data1[0-2] are used (argsA[1-3]) - le_uint32_t item_id = 0; // argsA[0] - le_uint32_t attribute = 0; // argsA[4] - le_uint32_t payment_count = 0; // Number of PD or PS (argsA[5]) + ItemData item; // Only data1[0-2] are used (valueB-valueD) + le_uint32_t item_id = 0; // valueA + le_uint32_t attribute = 0; // valueE + le_uint32_t payment_count = 0; // Number of PD or PS (valueF) le_uint32_t payment_type = 0; // 0 = Photon Drops, 1 = Photon Spheres - le_uint16_t success_function_id = 0; // argsA[6] - le_uint16_t failure_function_id = 0; // argsA[7] + le_uint16_t success_label = 0; // labelG + le_uint16_t failure_label = 0; // labelH } __packed_ws__(G_UpgradeWeaponAttribute_BB_6xDA, 0x2C); // 6xDB: Exchange item in quest (BB) @@ -6173,12 +6194,17 @@ struct G_SetEXPMultiplier_BB_6xDD { // 6xDE: Exchange Secret Lottery Ticket (BB; handled by server) // The client sends this when it executes an F95C quest opcode. +// There appears to be a bug in the client here: it sets the subcommand size to +// 2 instead of 3, so the last relevant field (failure_label) is not sent to +// the server. struct G_ExchangeSecretLotteryTicket_BB_6xDE { G_ClientIDHeader header; uint8_t index = 0; - uint8_t function_id1 = 0; - le_uint16_t function_id2 = 0; + uint8_t unknown_a1 = 0; + le_uint16_t success_label = 0; + // le_uint16_t failure_label = 0; + // parray unused; } __packed_ws__(G_ExchangeSecretLotteryTicket_BB_6xDE, 8); // 6xDF: Exchange Photon Crystals (BB; handled by server) @@ -6194,11 +6220,11 @@ struct G_ExchangePhotonCrystals_BB_6xDF { struct G_RequestItemDropFromQuest_BB_6xE0 { G_ClientIDHeader header; uint8_t floor = 0; - uint8_t type = 0; // argsA[0] + uint8_t type = 0; // valueA uint8_t unknown_a3 = 0; uint8_t unused = 0; - le_float x = 0.0f; // argsA[1] - le_float z = 0.0f; // argsA[2] + le_float x = 0.0f; // valueB + le_float z = 0.0f; // valueC } __packed_ws__(G_RequestItemDropFromQuest_BB_6xE0, 0x10); // 6xE1: Exchange Photon Tickets (BB; handled by server) @@ -6206,12 +6232,12 @@ struct G_RequestItemDropFromQuest_BB_6xE0 { struct G_ExchangePhotonTickets_BB_6xE1 { G_ClientIDHeader header; - uint8_t unknown_a1 = 0; // argsA[0] - uint8_t unknown_a2 = 0; // argsA[1] - uint8_t result_index = 0; // argsA[2] + uint8_t unknown_a1 = 0; // valueA + uint8_t unknown_a2 = 0; // valueB + uint8_t result_index = 0; // valueC uint8_t unused = 0; - le_uint16_t function_id1 = 0; // argsA[3] - le_uint16_t unknown_a5 = 0; // argsA[4] + le_uint16_t success_label = 0; // valueD + le_uint16_t failure_label = 0; // valueE } __packed_ws__(G_ExchangePhotonTickets_BB_6xE1, 0x0C); // 6xE2: Get Meseta slot prize (BB) diff --git a/src/ItemData.cc b/src/ItemData.cc index 15f563a1..8d1e8103 100644 --- a/src/ItemData.cc +++ b/src/ItemData.cc @@ -142,18 +142,22 @@ bool ItemData::is_wrapped(const StackLimits& limits) const { } } -void ItemData::wrap(const StackLimits& limits) { +void ItemData::wrap(const StackLimits& limits, uint8_t present_color) { switch (this->data1[0]) { case 0: - case 1: this->data1[4] |= 0x40; + this->data1[5] = (this->data1[5] & 0xF0) | (present_color & 0x0F); + break; + case 1: + this->data1[4] = (this->data1[4] & 0xF0) | 0x40 | (present_color & 0x0F); break; case 2: + // Mags cannot have custom present colors this->data2[2] |= 0x40; break; case 3: if (!this->is_stackable(limits)) { - this->data1[3] |= 0x40; + this->data1[3] = (this->data1[3] & 0xF0) | 0x40 | (present_color & 0x0F); } break; case 4: @@ -167,14 +171,14 @@ void ItemData::unwrap(const StackLimits& limits) { switch (this->data1[0]) { case 0: case 1: - this->data1[4] &= 0xBF; + this->data1[4] &= 0xB0; break; case 2: - this->data2[2] &= 0xBF; + this->data2[2] &= 0xB0; break; case 3: if (!this->is_stackable(limits)) { - this->data1[3] &= 0xBF; + this->data1[3] &= 0xB0; } break; case 4: diff --git a/src/ItemData.hh b/src/ItemData.hh index a0ce357d..3142bbc2 100644 --- a/src/ItemData.hh +++ b/src/ItemData.hh @@ -77,19 +77,19 @@ struct ItemData { // QUICK ITEM FORMAT REFERENCE // data1/0 data1/4 data1/8 data2 - // Weapon: 00ZZZZGG SS00AABB AABBAABB 00000000 + // Weapon: 00ZZZZGG SSNNAABB AABBAABB 00000000 // Armor: 0101ZZ00 FFTTDDDD EEEE0000 00000000 // Shield: 0102ZZ00 FFTTDDDD EEEE0000 00000000 // Unit: 0103ZZ00 FF00RRRR 00000000 00000000 // Mag: 02ZZLLWW HHHHIIII JJJJKKKK YYQQPPVV - // Tool: 03ZZZZFF 00CC0000 00000000 00000000 + // Tool: 03ZZZZUU 00CC0000 00000000 00000000 // Meseta: 04000000 00000000 00000000 MMMMMMMM // A = attribute type (for S-ranks, custom name) // B = attribute amount (for S-ranks, custom name) // C = stack size (for tools) // D = DEF bonus // E = EVP bonus - // F = flags (40=present; for tools, unused if item is stackable) + // F = armor/shield/unit flags (40=present; low 16 bits are present color) // G = weapon grind // H = mag DEF // I = mag POW @@ -97,11 +97,13 @@ struct ItemData { // K = mag MIND // L = mag level // M = meseta amount + // N = present color (weapon only; for other types this is in the flags field) // P = mag flags (40=present, 04=has left pb, 02=has right pb, 01=has center pb) // Q = mag IQ // R = unit modifier (little-endian) // S = weapon flags (80=unidentified, 40=present) and special (low 6 bits) // T = slot count + // U = tool flags (40=present; unused if item is stackable) // V = mag color // W = photon blasts // Y = mag synchro @@ -150,7 +152,7 @@ struct ItemData { uint32_t primary_identifier() const; bool is_wrapped(const StackLimits& limits) const; - void wrap(const StackLimits& limits); + void wrap(const StackLimits& limits, uint8_t present_color); void unwrap(const StackLimits& limits); bool is_stackable(const StackLimits& limits) const; diff --git a/src/ItemNameIndex.cc b/src/ItemNameIndex.cc index 305db5e8..bb188f90 100644 --- a/src/ItemNameIndex.cc +++ b/src/ItemNameIndex.cc @@ -135,7 +135,7 @@ std::string ItemNameIndex::describe_item(const ItemData& item, bool include_colo // Armors, shields, and units (0x01) can be wrapped, as can mags (0x02) and // non-stackable tools (0x03). However, each of these item classes has its // flags in a different location. - if (((item.data1[1] == 0x01) && (item.data1[4] & 0x40)) || + if (((item.data1[0] == 0x01) && (item.data1[4] & 0x40)) || ((item.data1[0] == 0x02) && (item.data2[2] & 0x40)) || ((item.data1[0] == 0x03) && !item.is_stackable(*this->limits) && (item.data1[3] & 0x40))) { ret_tokens.emplace_back("Wrapped"); diff --git a/src/PlayerSubordinates.hh b/src/PlayerSubordinates.hh index cd1096db..2be94481 100644 --- a/src/PlayerSubordinates.hh +++ b/src/PlayerSubordinates.hh @@ -881,78 +881,54 @@ struct BattleRules { LIMIT_LIVES = 2, }; - // Set by quest opcode F812, but values are remapped. - // F812 00 => FORBID_ALL - // F812 01 => ALLOW - // F812 02 => LIMIT_LEVEL + // Set by quest opcode F812, but values are remapped /* 00 */ TechDiskMode tech_disk_mode = TechDiskMode::ALLOW; - // Set by quest opcode F813, but values are remapped. - // F813 00 => FORBID_ALL - // F813 01 => ALLOW - // F813 02 => CLEAR_AND_ALLOW - // F813 03 => FORBID_RARES + // Set by quest opcode F813, but values are remapped /* 01 */ WeaponAndArmorMode weapon_and_armor_mode = WeaponAndArmorMode::ALLOW; - // Set by quest opcode F814, but values are remapped. - // F814 00 => FORBID_ALL - // F814 01 => ALLOW + // Set by quest opcode F814, but values are remapped /* 02 */ MagMode mag_mode = MagMode::ALLOW; - // Set by quest opcode F815, but values are remapped. - // F815 00 => FORBID_ALL - // F815 01 => ALLOW - // F815 02 => CLEAR_AND_ALLOW + // Set by quest opcode F815, but values are remapped /* 03 */ ToolMode tool_mode = ToolMode::ALLOW; - // Set by quest opcode F816. Values are not remapped. - // F816 00 => DEFAULT - // F816 01 => ALL_PLAYERS + // Set by quest opcode F816. Values are not remapped /* 04 */ TrapMode trap_mode = TrapMode::DEFAULT; // Set by quest opcode F817. Value appears to be unused in all PSO versions. /* 05 */ uint8_t unused_F817 = 0; - // Set by quest opcode F818, but values are remapped. - // F818 00 => 01 - // F818 01 => 00 - // F818 02 => 02 + // Set by quest opcode F818, but values are remapped /* 06 */ RespawnMode respawn_mode = RespawnMode::ALLOW; - // Set by quest opcode F819. + // Set by quest opcode F819 /* 07 */ uint8_t replace_char = 0; - // Set by quest opcode F81A, but value is inverted. + // Set by quest opcode F81A, but value is inverted /* 08 */ uint8_t drop_weapon = 0; - // Set by quest opcode F81B. + // Set by quest opcode F81B /* 09 */ uint8_t is_teams = 0; - // Set by quest opcode F852. + // Set by quest opcode F852 /* 0A */ uint8_t hide_target_reticle = 0; - // Set by quest opcode F81E. Values are not remapped. - // F81E 00 => ALLOW - // F81E 01 => FORBID_ALL - // F81E 02 => CLEAR_AND_ALLOW + // Set by quest opcode F81E. Values are not remapped /* 0B */ MesetaMode meseta_mode = MesetaMode::ALLOW; - // Set by quest opcode F81D. + // Set by quest opcode F81D /* 0C */ uint8_t death_level_up = 0; - // Set by quest opcode F851. The trap type is remapped: - // F851 00 XX => set count to XX for trap type 00 - // F851 01 XX => set count to XX for trap type 02 - // F851 02 XX => set count to XX for trap type 03 - // F851 03 XX => set count to XX for trap type 01 + // Set by quest opcode F851. The trap type is remapped /* 0D */ parray trap_counts; - // Set by quest opcode F85E. + // Set by quest opcode F85E /* 11 */ uint8_t enable_sonar = 0; - // Set by quest opcode F85F. + // Set by quest opcode F85F /* 12 */ uint8_t sonar_count = 0; - // Set by quest opcode F89E. + // Set by quest opcode F89E /* 13 */ uint8_t forbid_scape_dolls = 0; - // This value does not appear to be set by any quest opcode. + // This value does not appear to be set by any quest opcode /* 14 */ le_uint32_t unknown_a1 = 0; - // Set by quest opcode F86F. + // Set by quest opcode F86F /* 18 */ le_uint32_t lives = 0; - // Set by quest opcode F870. + // Set by quest opcode F870 /* 1C */ le_uint32_t max_tech_level = 0; - // Set by quest opcode F871. + // Set by quest opcode F871 /* 20 */ le_uint32_t char_level = 0; - // Set by quest opcode F872. + // Set by quest opcode F872 /* 24 */ le_uint32_t time_limit = 0; - // Set by quest opcode F8A8. + // Set by quest opcode F8A8 /* 28 */ le_uint16_t death_tech_level_up = 0; /* 2A */ parray unused; - // Set by quest opcode F86B. + // Set by quest opcode F86B /* 2C */ le_uint32_t box_drop_area = 0; /* 30 */ diff --git a/src/QuestScript.cc b/src/QuestScript.cc index 5321d503..3618b331 100644 --- a/src/QuestScript.cc +++ b/src/QuestScript.cc @@ -59,12 +59,12 @@ using AttackData = BattleParamsIndex::AttackData; using ResistData = BattleParamsIndex::ResistData; using MovementData = BattleParamsIndex::MovementData; -struct BezierCurveControlPoint { +struct Vector4F { le_float x; le_float y; le_float z; le_float t; -} __packed_ws__(BezierCurveControlPoint, 0x10); +} __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) @@ -289,6 +289,7 @@ static constexpr uint16_t F_V05_V4 = F_DC_112000 | F_DC_V1 | F_DC_V2 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; @@ -341,7 +342,7 @@ 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 AREA(INT32, 0, "area"); +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 @@ -358,667 +359,1569 @@ static const QuestScriptOpcodeDefinition opcode_defs[] = { // referred to as an array, as regsA[0], regsA[1], etc. // Does nothing - {0x0000, "nop", nullptr, {}, F_V0_V4}, + {0x00, "nop", nullptr, {}, F_V0_V4}, + // Pops new PC off stack - {0x0001, "ret", nullptr, {}, F_V0_V4 | F_RET}, + {0x01, "ret", nullptr, {}, F_V0_V4 | F_RET}, + // Stops execution for the current frame. Execution resumes immediately // after this opcode on the next frame. - {0x0002, "sync", nullptr, {}, F_V0_V4}, + {0x02, "sync", nullptr, {}, F_V0_V4}, + // Exits entirely - {0x0003, "exit", nullptr, {INT32}, F_V0_V4}, + {0x03, "exit", nullptr, {INT32}, F_V0_V4}, + // Starts a new thread at labelA - {0x0004, "thread", nullptr, {SCRIPT16}, F_V0_V4}, + {0x04, "thread", nullptr, {SCRIPT16}, F_V0_V4}, + // Pushes r1-r7 to the stack - {0x0005, "va_start", nullptr, {}, F_V3_V4}, + {0x05, "va_start", nullptr, {}, F_V3_V4}, + // Pops r7-r1 from the stack - {0x0006, "va_end", nullptr, {}, F_V3_V4}, + {0x06, "va_end", nullptr, {}, F_V3_V4}, + // Replaces r1-r7 with the args stack, then calls labelA - {0x0007, "va_call", nullptr, {SCRIPT16}, F_V3_V4}, + {0x07, "va_call", nullptr, {SCRIPT16}, F_V3_V4}, + // Copies a value from regB to regA - {0x0008, "let", nullptr, {REG, REG}, F_V0_V4}, + {0x08, "let", nullptr, {REG, REG}, F_V0_V4}, + // Sets regA to valueB - {0x0009, "leti", nullptr, {REG, INT32}, F_V0_V4}, + {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. - {0x000A, "leta", nullptr, {REG, REG}, F_V0_V2}, + {0x0A, "leta", nullptr, {REG, REG}, F_V0_V2}, + // Sets regA to valueB - {0x000A, "letb", nullptr, {REG, INT8}, F_V3_V4}, + {0x0A, "letb", nullptr, {REG, INT8}, F_V3_V4}, + // Sets regA to valueB - {0x000B, "letw", nullptr, {REG, INT16}, F_V3_V4}, + {0x0B, "letw", nullptr, {REG, INT16}, F_V3_V4}, + // Sets regA to the memory address of regB - {0x000C, "leta", nullptr, {REG, REG}, F_V3_V4}, + {0x0C, "leta", nullptr, {REG, REG}, F_V3_V4}, + // Sets regA to the address of labelB - {0x000D, "leto", nullptr, {REG, SCRIPT16}, F_V3_V4}, + {0x0D, "leto", nullptr, {REG, SCRIPT16}, F_V3_V4}, + // Sets regA to 1 - {0x0010, "set", nullptr, {REG}, F_V0_V4}, + {0x10, "set", nullptr, {REG}, F_V0_V4}, + // Sets regA to 0 - {0x0011, "clear", nullptr, {REG}, F_V0_V4}, + {0x11, "clear", nullptr, {REG}, F_V0_V4}, + // Sets a regA to 0 if it's nonzero and vice versa - {0x0012, "rev", nullptr, {REG}, F_V0_V4}, + {0x12, "rev", nullptr, {REG}, F_V0_V4}, + // Sets flagA to 1 - {0x0013, "gset", nullptr, {INT16}, F_V0_V4}, + {0x13, "gset", nullptr, {INT16}, F_V0_V4}, + // Clears flagA to 0 - {0x0014, "gclear", nullptr, {INT16}, F_V0_V4}, + {0x14, "gclear", nullptr, {INT16}, F_V0_V4}, + // Inverts flagA - {0x0015, "grev", nullptr, {INT16}, F_V0_V4}, + {0x15, "grev", nullptr, {INT16}, F_V0_V4}, + // If regB is nonzero, sets flagA; otherwise, clears it - {0x0016, "glet", nullptr, {INT16, REG}, F_V0_V4}, + {0x16, "glet", nullptr, {INT16, REG}, F_V0_V4}, + // Sets regB to the value of flagA - {0x0017, "gget", nullptr, {INT16, REG}, F_V0_V4}, + {0x17, "gget", nullptr, {INT16, REG}, F_V0_V4}, + // regA += regB - {0x0018, "add", nullptr, {REG, REG}, F_V0_V4}, + {0x18, "add", nullptr, {REG, REG}, F_V0_V4}, + // regA += valueB - {0x0019, "addi", nullptr, {REG, INT32}, F_V0_V4}, + {0x19, "addi", nullptr, {REG, INT32}, F_V0_V4}, + // regA -= regB - {0x001A, "sub", nullptr, {REG, REG}, F_V0_V4}, + {0x1A, "sub", nullptr, {REG, REG}, F_V0_V4}, + // regA -= valueB - {0x001B, "subi", nullptr, {REG, INT32}, F_V0_V4}, + {0x1B, "subi", nullptr, {REG, INT32}, F_V0_V4}, + // regA *= regB - {0x001C, "mul", nullptr, {REG, REG}, F_V0_V4}, + {0x1C, "mul", nullptr, {REG, REG}, F_V0_V4}, + // regA *= valueB - {0x001D, "muli", nullptr, {REG, INT32}, F_V0_V4}, + {0x1D, "muli", nullptr, {REG, INT32}, F_V0_V4}, + // regA /= regB - {0x001E, "div", nullptr, {REG, REG}, F_V0_V4}, + {0x1E, "div", nullptr, {REG, REG}, F_V0_V4}, + // regA /= valueB - {0x001F, "divi", nullptr, {REG, INT32}, F_V0_V4}, + {0x1F, "divi", nullptr, {REG, INT32}, F_V0_V4}, + // regA &= regB - {0x0020, "and", nullptr, {REG, REG}, F_V0_V4}, + {0x20, "and", nullptr, {REG, REG}, F_V0_V4}, + // regA &= valueB - {0x0021, "andi", nullptr, {REG, INT32}, F_V0_V4}, + {0x21, "andi", nullptr, {REG, INT32}, F_V0_V4}, + // regA |= regB - {0x0022, "or", nullptr, {REG, REG}, F_V0_V4}, + {0x22, "or", nullptr, {REG, REG}, F_V0_V4}, + // regA |= valueB - {0x0023, "ori", nullptr, {REG, INT32}, F_V0_V4}, + {0x23, "ori", nullptr, {REG, INT32}, F_V0_V4}, + // regA ^= regB - {0x0024, "xor", nullptr, {REG, REG}, F_V0_V4}, + {0x24, "xor", nullptr, {REG, REG}, F_V0_V4}, + // regA ^= valueB - {0x0025, "xori", nullptr, {REG, INT32}, F_V0_V4}, + {0x25, "xori", nullptr, {REG, INT32}, F_V0_V4}, + // regA %= regB - {0x0026, "mod", nullptr, {REG, REG}, F_V3_V4}, + {0x26, "mod", nullptr, {REG, REG}, F_V3_V4}, + // regA %= valueB - {0x0027, "modi", nullptr, {REG, INT32}, F_V3_V4}, + {0x27, "modi", nullptr, {REG, INT32}, F_V3_V4}, + // Jumps to labelA - {0x0028, "jmp", nullptr, {SCRIPT16}, F_V0_V4}, + {0x28, "jmp", nullptr, {SCRIPT16}, F_V0_V4}, + // Pushes the script offset immediately after this opcode and jumps to // labelA - {0x0029, "call", nullptr, {SCRIPT16}, F_V0_V4}, + {0x29, "call", nullptr, {SCRIPT16}, F_V0_V4}, + // If all values in regsB are nonzero, jumps to labelA - {0x002A, "jmp_on", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + {0x2A, "jmp_on", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + // If all values in regsB are zero, jumps to labelA - {0x002B, "jmp_off", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + {0x2B, "jmp_off", nullptr, {SCRIPT16, REG_SET}, F_V0_V4}, + // If regA == regB, jumps to labelC - {0x002C, "jmp_eq", "jmp_=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x2C, "jmp_eq", "jmp_=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA == regB, jumps to labelC - {0x002D, "jmpi_eq", "jmpi_=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x2D, "jmpi_eq", "jmpi_=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA != regB, jumps to labelC - {0x002E, "jmp_ne", "jmp_!=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x2E, "jmp_ne", "jmp_!=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA != regB, jumps to labelC - {0x002F, "jmpi_ne", "jmpi_!=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x2F, "jmpi_ne", "jmpi_!=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA > regB (unsigned), jumps to labelC - {0x0030, "ujmp_gt", "ujmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x30, "ujmp_gt", "ujmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA > regB (unsigned), jumps to labelC - {0x0031, "ujmpi_gt", "ujmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x31, "ujmpi_gt", "ujmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA > regB (signed), jumps to labelC - {0x0032, "jmp_gt", "jmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x32, "jmp_gt", "jmp_>", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA > regB (signed), jumps to labelC - {0x0033, "jmpi_gt", "jmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x33, "jmpi_gt", "jmpi_>", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA < regB (unsigned), jumps to labelC - {0x0034, "ujmp_lt", "ujmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x34, "ujmp_lt", "ujmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA < regB (unsigned), jumps to labelC - {0x0035, "ujmpi_lt", "ujmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x35, "ujmpi_lt", "ujmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA < regB (signed), jumps to labelC - {0x0036, "jmp_lt", "jmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x36, "jmp_lt", "jmp_<", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA < regB (signed), jumps to labelC - {0x0037, "jmpi_lt", "jmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x37, "jmpi_lt", "jmpi_<", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA >= regB (unsigned), jumps to labelC - {0x0038, "ujmp_ge", "ujmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x38, "ujmp_ge", "ujmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA >= regB (unsigned), jumps to labelC - {0x0039, "ujmpi_ge", "ujmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x39, "ujmpi_ge", "ujmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA >= regB (signed), jumps to labelC - {0x003A, "jmp_ge", "jmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3A, "jmp_ge", "jmp_>=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA >= regB (signed), jumps to labelC - {0x003B, "jmpi_ge", "jmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x3B, "jmpi_ge", "jmpi_>=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA <= regB (unsigned), jumps to labelC - {0x003C, "ujmp_le", "ujmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3C, "ujmp_le", "ujmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA <= regB (unsigned), jumps to labelC - {0x003D, "ujmpi_le", "ujmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x3D, "ujmpi_le", "ujmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // If regA <= regB (signed), jumps to labelC - {0x003E, "jmp_le", "jmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + {0x3E, "jmp_le", "jmp_<=", {REG, REG, SCRIPT16}, F_V0_V4}, + // If regA <= regB (signed), jumps to labelC - {0x003F, "jmpi_le", "jmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, + {0x3F, "jmpi_le", "jmpi_<=", {REG, INT32, SCRIPT16}, F_V0_V4}, + // Jumps to labelsB[regA] - {0x0040, "switch_jmp", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + {0x40, "switch_jmp", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + // Calls labelsB[regA] - {0x0041, "switch_call", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + {0x41, "switch_call", nullptr, {REG, SCRIPT16_SET}, F_V0_V4}, + // Does nothing - {0x0042, "nop_42", nullptr, {INT32}, F_V0_V2}, + {0x42, "nop_42", nullptr, {INT32}, F_V0_V2}, + // Pushes the value in regA to the stack - {0x0042, "stack_push", nullptr, {REG}, F_V3_V4}, + {0x42, "stack_push", nullptr, {REG}, F_V3_V4}, + // Pops a value from the stack and puts it into regA - {0x0043, "stack_pop", nullptr, {REG}, F_V3_V4}, + {0x43, "stack_pop", nullptr, {REG}, F_V3_V4}, + // Pushes (valueB) regs in increasing order starting at regA - {0x0044, "stack_pushm", nullptr, {REG, INT32}, F_V3_V4}, + {0x44, "stack_pushm", nullptr, {REG, INT32}, F_V3_V4}, + // Pops (valueB) regs in decreasing order ending at regA - {0x0045, "stack_popm", nullptr, {REG, INT32}, F_V3_V4}, + {0x45, "stack_popm", nullptr, {REG, INT32}, F_V3_V4}, + // Appends regA to the args list - {0x0048, "arg_pushr", nullptr, {REG}, F_V3_V4 | F_PASS}, + {0x48, "arg_pushr", nullptr, {REG}, F_V3_V4 | F_PASS}, + // Appends valueA to the args list - {0x0049, "arg_pushl", nullptr, {INT32}, F_V3_V4 | F_PASS}, + {0x49, "arg_pushl", nullptr, {INT32}, F_V3_V4 | F_PASS}, + // Appends valueA to the args list - {0x004A, "arg_pushb", nullptr, {INT8}, F_V3_V4 | F_PASS}, + {0x4A, "arg_pushb", nullptr, {INT8}, F_V3_V4 | F_PASS}, + // Appends valueA to the args list - {0x004B, "arg_pushw", nullptr, {INT16}, 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 - {0x004C, "arg_pusha", nullptr, {REG}, F_V3_V4 | F_PASS}, + {0x4C, "arg_pusha", nullptr, {REG}, F_V3_V4 | F_PASS}, + // Appends the script offset of labelA to the args list - {0x004D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PASS}, + {0x4D, "arg_pusho", nullptr, {LABEL16}, F_V3_V4 | F_PASS}, + // Appends the memory address of strA to the args list - {0x004E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PASS}, + {0x4E, "arg_pushs", nullptr, {CSTRING}, F_V3_V4 | F_PASS}, + // Creates a dialogue with object/NPC (valueA) starting with message strB - {0x0050, "message", nullptr, {INT32, CSTRING}, F_V0_V4 | F_ARGS}, + {0x50, "message", nullptr, {INT32, CSTRING}, F_V0_V4 | F_ARGS}, + // Prompts the player with a list of choices, returning the index of their // choice in regA - {0x0051, "list", nullptr, {REG, CSTRING}, F_V0_V4 | F_ARGS}, + {0x51, "list", nullptr, {REG, CSTRING}, F_V0_V4 | F_ARGS}, + // Fades from black - {0x0052, "fadein", nullptr, {}, F_V0_V4}, + {0x52, "fadein", nullptr, {}, F_V0_V4}, + // Fades to black - {0x0053, "fadeout", nullptr, {}, F_V0_V4}, + {0x53, "fadeout", nullptr, {}, F_V0_V4}, + // Plays a sound effect - {0x0054, "se", nullptr, {INT32}, F_V0_V4 | F_ARGS}, + {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. - {0x0055, "bgm", nullptr, {INT32}, F_V0_V4 | F_ARGS}, + {0x55, "bgm", nullptr, {INT32}, F_V0_V4 | F_ARGS}, + // Does nothing - {0x0056, "nop_56", nullptr, {}, F_V0_V2}, + {0x56, "nop_56", nullptr, {}, F_V0_V2}, + // Does nothing - {0x0057, "nop_57", nullptr, {}, F_V0_V2}, + {0x57, "nop_57", nullptr, {}, F_V0_V2}, + // Does nothing - {0x0058, "nop_58", "enable", {INT32}, F_V0_V2}, + {0x58, "nop_58", "enable", {INT32}, F_V0_V2}, + // Does nothing - {0x0059, "nop_59", "disable", {INT32}, F_V0_V2}, - // Displays a message - {0x005A, "window_msg", nullptr, {CSTRING}, F_V0_V4 | F_ARGS}, - // Adds a message to an existing window - {0x005B, "add_msg", nullptr, {CSTRING}, F_V0_V4 | F_ARGS}, - // Closes a message box - {0x005C, "mesend", nullptr, {}, F_V0_V4}, - // Gets the current time - {0x005D, "gettime", nullptr, {REG}, F_V0_V4}, + {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; X must be numeric, so + // does not work (supported on 11/2000 and later) + // => newline + // or => character's name + // or => character's class + //