diff --git a/src/Client.cc b/src/Client.cc index d7423d25..94708227 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -67,7 +67,7 @@ Client::Client( proxy_suppress_remote_login(false), proxy_zero_remote_guild_card(false), dol_base_addr(0) { - this->last_switch_enabled_command.subcommand = 0; + this->last_switch_enabled_command.header.subcommand = 0; memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); if (this->version() == GameVersion::BB) { diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index ba81e8ae..290e75a9 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -320,7 +320,8 @@ struct S_ServerInitDefault_DC_PC_V3_02_17_91_9B { }; template -struct S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B : S_ServerInitDefault_DC_PC_V3_02_17_91_9B { +struct S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B { + S_ServerInitDefault_DC_PC_V3_02_17_91_9B basic_cmd; // This field is not part of SEGA's implementation; the client ignores it. // newserv sends a message here disavowing the preceding copyright notice. ptext after_message; @@ -364,7 +365,8 @@ struct S_ServerInitDefault_BB_03_9B { }; template -struct S_ServerInitWithAfterMessage_BB_03_9B : S_ServerInitDefault_BB_03_9B { +struct S_ServerInitWithAfterMessage_BB_03_9B { + S_ServerInitDefault_BB_03_9B basic_cmd; // As in 02, this field is not part of SEGA's implementation. ptext after_message; }; @@ -566,21 +568,24 @@ struct C_MenuSelection_10_Flag00 { }; template -struct C_MenuSelection_10_Flag01 : C_MenuSelection_10_Flag00 { +struct C_MenuSelection_10_Flag01 { + C_MenuSelection_10_Flag00 basic_cmd; ptext unknown_a1; }; struct C_MenuSelection_DC_V3_10_Flag01 : C_MenuSelection_10_Flag01 { }; struct C_MenuSelection_PC_BB_10_Flag01 : C_MenuSelection_10_Flag01 { }; template -struct C_MenuSelection_10_Flag02 : C_MenuSelection_10_Flag00 { +struct C_MenuSelection_10_Flag02 { + C_MenuSelection_10_Flag00 basic_cmd; ptext password; }; struct C_MenuSelection_DC_V3_10_Flag02 : C_MenuSelection_10_Flag02 { }; struct C_MenuSelection_PC_BB_10_Flag02 : C_MenuSelection_10_Flag02 { }; template -struct C_MenuSelection_10_Flag03 : C_MenuSelection_10_Flag00 { +struct C_MenuSelection_10_Flag03 { + C_MenuSelection_10_Flag00 basic_cmd; ptext unknown_a1; ptext password; }; @@ -1809,7 +1814,9 @@ struct S_RankUpdate_GC_Ep3_B7 { // B8 (S->C): Update card definitions (Episode 3) // Contents is a single little-endian le_uint32_t specifying the size of the -// (PRS-compressed) data, followed immediately by the data. +// (PRS-compressed) data, followed immediately by the data. The maximum size of +// the compressed data is 0x9000 bytes, and the maximum size of the decompressed +// data is 0x36EC0 bytes. // Note: PSO BB accepts this command as well, but ignores it. // B8 (C->S): Confirm updated card definitions (Episode 3) @@ -2029,38 +2036,11 @@ struct C_SetBlockedSenders_BB_C6 : C_SetBlockedSenders_C6<28> { }; // Same as 60, but only send to Episode 3 clients. // CA (C->S): Server data request (Episode 3) -// The format is generally the same as the subcommand-based commands (60, 62, -// etc.), but the server is expected to respond to the command instead of -// forwarding it. (The client has no handler for CA commands at all.) -// Generally a CA command looks like this: -// CA 00 SS SS B3 TT 00 00 WW 00 00 00 ... -// S = command size -// T = subcommand size in uint32_ts (== (S / 4) - 1) -// W = subcommand number -// We refer to the various Episode 3 server data commands as CAxWW, where W -// comes from the format above. The server data commands are: -// CAx0B (T=05) - Unknown -// CAx0C (T=05) - Unknown -// CAx0D (T=07) - Unknown -// CAx0E (T=05) - Unknown -// CAx0F (T=07) - Unknown -// CAx10 (T=06) - Unknown -// CAx11 (T=1E) - Unknown -// CAx12 (T=05) - Unknown -// CAx13 (T=AF) - Update game state (?) -// CAx14 (T=1B) - Update playfield state (?) -// CAx1B (T=09) - Update names (?) -// CAx1D (T=04) - Unknown -// CAx21 (T=05) - Unknown -// CAx28 (T=05) - Unknown -// CAx2B (T=05) - Unknown -// CAx34 (T=05) - Unknown -// CAx3A (T=04) - Unknown -// CAx40 (T=04) - Map list request. See send_ep3_map_list for server response. -// CAx41 (T=05) - Map data request. See send_ep3_map_data for server response. -// CAx48 (T=05) - Unknown -// CAx49 (T=C1) - Unknown -// TODO: Document the above commands that are currently unknown. +// The CA command format is the same as that of the 6xB3 commands, and the +// subsubcommands formats are shared as well. Unlike the 6x commands, the server +// is expected to respond to the command appropriately instead of forwarding it. +// However, not all 6xB3 subsubcommands are also CA subcommands - those that are +// shared are noted in the structure names. (Search for "CAx" to find them.) // CB: Broadcast command (Episode 3) // Same as 60, but only send to Episode 3 clients. @@ -2864,52 +2844,75 @@ struct S_Unknown_BB_F0 { // commands must be used. The 60 and 62 commands exhibit undefined behavior if // this limit is exceeded. -// These structures are used by many commands (noted below) -struct G_ItemSubcommand { - uint8_t command; - uint8_t size; - le_uint16_t client_id; - le_uint32_t item_id; - le_uint32_t amount; -}; -struct G_ItemIDSubcommand { + +// These common structures are used my many subcommands. +struct G_ClientIDHeader { + uint8_t subcommand; + uint8_t size; + le_uint16_t client_id; // <= 12 +}; +struct G_EnemyIDHeader { + uint8_t subcommand; + uint8_t size; + le_uint16_t enemy_id; // In range [0x1000, 0x4000) +}; +struct G_ObjectIDHeader { + uint8_t subcommand; + uint8_t size; + le_uint16_t object_id; // >= 0x4000, != 0xFFFF +}; +struct G_UnusedHeader { uint8_t subcommand; uint8_t size; le_uint16_t unused; - le_uint32_t item_id; }; +template +struct G_ExtendedHeader { + HeaderT basic_header; + le_uint32_t size; +}; // 6x00: Invalid subcommand // 6x01: Invalid subcommand + // 6x02: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x03: Unknown (same handler as 02) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x04: Unknown +struct G_Unknown_6x04 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x05: Switch state changed // Some things that don't look like switches are implemented as switches using // this subcommand. For example, when all enemies in a room are defeated, this // subcommand is used to unlock the doors. struct G_SwitchStateChanged_6x05 { - uint8_t subcommand; - uint8_t size; - le_uint16_t switch_id; // 0xFFFF for room clear when all enemies defeated - parray unknown; + // Note: header.object_id is 0xFFFF for room clear when all enemies defeated + G_ObjectIDHeader header; + parray unknown_a1; + le_uint16_t unknown_a2; + parray unknown_a3; uint8_t area; - uint8_t enabled; + uint8_t flags; // Bit field, with 2 lowest bits having meaning }; // 6x06: Send guild card template struct G_SendGuildCard_DC_PC_V3 { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; le_uint32_t player_tag; le_uint32_t guild_card_number; ptext name; @@ -2928,9 +2931,7 @@ struct G_SendGuildCard_PC_6x06 : G_SendGuildCard_DC_PC_V3 { }; struct G_SendGuildCard_V3_6x06 : G_SendGuildCard_DC_PC_V3 { }; struct G_SendGuildCard_BB_6x06 { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; le_uint32_t guild_card_number; ptext name; ptext team_name; @@ -2944,9 +2945,11 @@ struct G_SendGuildCard_BB_6x06 { // 6x07: Symbol chat struct G_SymbolChat_6x07 { - uint8_t command; - uint8_t size; - le_uint16_t unused1; + G_UnusedHeader header; + // TODO: How does this format differ across PSO versions? The GC version + // treats some fields as unexpectedly large values (for example, face_spec + // through unused2 are byteswapped as an le_uint32_t), so we should verify + // that the order of these fields is the same on other versions. le_uint32_t client_id; // Bits: SSSCCCFF (S = sound, C = face color, F = face shape) uint8_t face_spec; @@ -2969,89 +2972,202 @@ struct G_SymbolChat_6x07 { }; // 6x08: Invalid subcommand + // 6x09: Unknown +struct G_Unknown_6x09 { + G_EnemyIDHeader header; +}; + // 6x0A: Enemy hit struct G_EnemyHitByPlayer_6x0A { - uint8_t command; - uint8_t size; + G_EnemyIDHeader header; + // Note: enemy_id (in header) is in the range [0x1000, 0x4000) le_uint16_t enemy_id2; - le_uint16_t enemy_id; le_uint16_t damage; - le_uint32_t flags; + be_uint32_t flags; }; // 6x0B: Box destroyed + +struct G_BoxDestroyed_6x0B { + G_ClientIDHeader header; + le_uint32_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6x0C: Add condition (poison/slow/etc.) + +struct G_AddOrRemoveCondition_6x0C_6x0D { + G_ClientIDHeader header; + le_uint32_t unknown_a1; // Probably condition type + le_uint32_t unknown_a2; +}; + // 6x0D: Remove condition (poison/slow/etc.) +// Same format as 6x0C + // 6x0E: Unknown + +struct G_Unknown_6x0E { + G_ClientIDHeader header; +}; + // 6x0F: Invalid subcommand + // 6x10: Unknown (not valid on Episode 3) + +struct G_Unknown_6x10_6x11_6x12_6x14 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + le_uint32_t unknown_a4; +}; + // 6x11: Unknown (not valid on Episode 3) +// Same format as 6x10 + // 6x12: Dragon boss actions (not valid on Episode 3) -// 6x13: Re Rol Le boss actions (not valid on Episode 3) +// Same format as 6x10 + +// 6x13: De Rol Le boss actions (not valid on Episode 3) + +struct G_DeRolLeBossActions_6x13 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; +}; + // 6x14: Unknown (supported; game only; not valid on Episode 3) +// Same format as 6x10 + // 6x15: Vol Opt boss actions (not valid on Episode 3) + +struct G_VolOptBossActions_6x15 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + le_uint16_t unknown_a4; + le_uint16_t unknown_a5; +}; + // 6x16: Vol Opt boss actions (not valid on Episode 3) + +struct G_VolOptBossActions_6x16 { + G_UnusedHeader header; + parray unknown_a2; + le_uint16_t unknown_a3; +}; + // 6x17: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x17 { + G_ClientIDHeader header; + le_float unknown_a2; + le_float unknown_a3; + le_float unknown_a4; + le_uint32_t unknown_a5; +}; + // 6x18: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x18 { + G_ClientIDHeader header; + parray unknown_a2; +}; + // 6x19: Dark Falz boss actions (not valid on Episode 3) + +struct G_DarkFalzActions_6x19 { + G_EnemyIDHeader header; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + le_uint32_t unknown_a4; + le_uint32_t unused; +}; + // 6x1A: Invalid subcommand + // 6x1B: Unknown (not valid on Episode 3) + +struct G_Unknown_6x1B { + G_ClientIDHeader header; +}; + // 6x1C: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x1C { + G_ClientIDHeader header; +}; + // 6x1D: Invalid subcommand // 6x1E: Invalid subcommand + // 6x1F: Unknown (supported; lobby & game) + +struct G_Unknown_6x1F { + G_ClientIDHeader header; + le_uint32_t area; +}; + // 6x20: Set position (existing clients send when a new client joins a lobby/game) + +struct G_Unknown_6x20 { + G_ClientIDHeader header; + le_uint32_t area; + le_float x; + le_float y; + le_float z; + le_uint32_t unknown_a1; +}; + // 6x21: Inter-level warp -// 6x22: Set player visibility -// 6x23: Set player visibility +struct G_InterLevelWarp_6x21 { + G_ClientIDHeader header; + le_uint32_t area; +}; + +// 6x22: Set player invisible +// 6x23: Set player visible struct G_SetPlayerVisibility_6x22_6x23 { - uint8_t subcommand; // 22 = invisible, 23 = visible - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; }; // 6x24: Unknown (supported; game only) +struct G_Unknown_6x24 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; + le_float x; + le_float y; + le_float z; +}; + // 6x25: Equip item -struct G_EquipItem_6x25 { - uint8_t command; - uint8_t size; - le_uint16_t client_id; +struct G_EquipOrUnequipItem_6x25_6x26 { + G_ClientIDHeader header; le_uint32_t item_id; - le_uint32_t equip_slot; + le_uint32_t equip_slot; // Unused for 6x26 (unequip item) }; // 6x26: Unequip item - -struct G_UnequipItem_6x26 { - uint8_t command; - uint8_t size; - le_uint16_t client_id; - le_uint32_t item_id; - le_uint32_t unused2; -}; +// Same format as 6x25 // 6x27: Use item struct G_UseItem_6x27 { - uint8_t command; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint32_t item_id; }; // 6x28: Feed MAG struct G_FeedMAG_6x28 { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint32_t mag_item_id; le_uint32_t fed_item_id; }; @@ -3059,15 +3175,18 @@ struct G_FeedMAG_6x28 { // 6x29: Delete inventory item (via bank deposit / sale / feeding MAG) // This subcommand is also used for reducing the size of stacks - if amount is // less than the stack count, the item is not deleted and its ID remains valid. -// Format is G_ItemSubcommand + +struct G_DeleteInventoryItem_6x29 { + G_ClientIDHeader header; + le_uint32_t item_id; + le_uint32_t amount; +}; // 6x2A: Drop item -struct G_PlayerDropItem_6x2A { - uint8_t command; - uint8_t size; - le_uint16_t client_id; - le_uint16_t unused2; // should be 1 +struct G_DropItem_6x2A { + G_ClientIDHeader header; + le_uint16_t unknown_a1; // Should be 1... maybe amount? le_uint16_t area; le_uint32_t item_id; le_float x; @@ -3077,58 +3196,139 @@ struct G_PlayerDropItem_6x2A { // 6x2B: Create item in inventory (e.g. via tekker or bank withdraw) -struct G_PlayerCreateInventoryItem_DC_6x2B { - uint8_t command; - uint8_t size; - le_uint16_t client_id; +struct G_CreateInventoryItem_DC_6x2B { + G_ClientIDHeader header; ItemData item; }; -struct G_PlayerCreateInventoryItem_PC_V3_BB_6x2B : G_PlayerCreateInventoryItem_DC_6x2B { - le_uint32_t unknown; +struct G_CreateInventoryItem_PC_V3_BB_6x2B { + G_CreateInventoryItem_DC_6x2B basic_cmd; + uint8_t unused1; + uint8_t unknown_a2; + le_uint16_t unused2; }; // 6x2C: Talk to NPC + +struct G_TalkToNPC_6x2C { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_float unknown_a3; + le_float unknown_a4; + le_float unknown_a5; +}; + // 6x2D: Done talking to NPC -// 6x2E: Unknown + +struct G_EndTalkToNPC_6x2D { + G_ClientIDHeader header; +}; + +// 6x2E: Set and/or clear player flags + +struct G_SetOrClearPlayerFlags_6x2E { + G_ClientIDHeader header; + le_uint32_t and_mask; + le_uint32_t or_mask; +}; + // 6x2F: Hit by enemy +struct G_HitByEnemy_6x2F { + G_ClientIDHeader header; + le_uint32_t hit_type; // 0 = set HP, 1 = add/subtract HP, 2 = add/sub fixed HP + le_uint16_t damage; + le_uint16_t client_id; +}; + // 6x30: Level up struct G_LevelUp_6x30 { - uint8_t command; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint16_t atp; le_uint16_t mst; le_uint16_t evp; le_uint16_t hp; le_uint16_t dfp; le_uint16_t ata; - le_uint32_t level; + le_uint16_t level; + le_uint16_t unknown_a1; // Must be 0 or 1 }; // 6x31: Medical center -// 6x32: Medical center + +struct G_UseMedicalCenter_6x31 { + G_ClientIDHeader header; +}; + +// 6x32: Unknown (occurs when using Medical Center) + +struct G_Unknown_6x32 { + G_UnusedHeader header; +}; + // 6x33: Revive player (e.g. with moon atomizer) + +struct G_RevivePlayer_6x33 { + G_ClientIDHeader header; + le_uint16_t client_id2; + le_uint16_t unused; +}; + // 6x34: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x35: Invalid subcommand + // 6x36: Unknown (supported; game only) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x37: Photon blast + +struct G_PhotonBlast_6x37 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x38: Unknown + +struct G_Unknown_6x38 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x39: Photon blast ready + +struct G_PhotonBlastReady_6x38 { + G_ClientIDHeader header; +}; + // 6x3A: Unknown (supported; game only) + +struct G_Unknown_6x3A { + G_ClientIDHeader header; +}; + // 6x3B: Unknown (supported; lobby & game) + +struct G_Unknown_6x3B { + G_ClientIDHeader header; +}; + // 6x3C: Invalid subcommand // 6x3D: Invalid subcommand // 6x3E: Stop moving struct G_StopAtPosition_6x3E { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; - uint64_t unknown; + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint16_t area; + le_uint16_t unknown_a3; le_float x; le_float y; le_float z; @@ -3137,11 +3337,11 @@ struct G_StopAtPosition_6x3E { // 6x3F: Set position struct G_SetPosition_6x3F { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; - le_uint32_t unknown; - le_uint32_t area; + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint16_t area; + le_uint16_t unknown_a3; le_float x; le_float y; le_float z; @@ -3150,55 +3350,201 @@ struct G_SetPosition_6x3F { // 6x40: Walk struct G_WalkToPosition_6x40 { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_float x; le_float z; - le_uint32_t unused; + le_uint32_t unknown_a1; }; // 6x41: Unknown +// This subcommand is completely ignored (at least, by PSO GC). // 6x42: Run struct G_RunToPosition_6x42 { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_float x; le_float z; }; // 6x43: First attack + +struct G_Attack_6x43_6x44_6x45 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; +}; + // 6x44: Second attack +// Same format as 6x43 + // 6x45: Third attack +// Same format as 6x43 + // 6x46: Attack finished (sent after each of 43, 44, and 45) + +struct G_AttackFinished_6x46 { + G_ClientIDHeader header; + le_uint32_t count; + struct Entry { + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + }; + Entry entries[11]; +}; + // 6x47: Cast technique + +struct G_CastTechnique_6x47 { + G_ClientIDHeader header; + uint8_t technique_number; + uint8_t unused; + // Note: The level here isn't the actual tech level that was cast, if the + // level is > 15. In that case, a 6x8D is sent first, which contains the + // additional level which is added to this level at cast time. They probably + // did this for legacy reasons when dealing with v1/v2 compatibility, and + // never cleaned it up. + uint8_t level; + uint8_t target_count; + struct TargetEntry { + le_uint16_t client_id; + le_uint16_t unknown_a2; + }; + TargetEntry targets[10]; +}; + // 6x48: Cast technique complete + +struct G_CastTechniqueComplete_6x48 { + G_ClientIDHeader header; + le_uint16_t technique_number; + // This level matches the level sent in the 6x47 command, even if that level + // was overridden by a preceding 6x8D command. + le_uint16_t level; +}; + // 6x49: Subtract PB energy + +struct G_SubtractPBEnergy_6x49 { + G_ClientIDHeader header; + uint8_t unknown_a1; + uint8_t unknown_a2; + le_uint16_t entry_count; + le_uint16_t unknown_a3; + le_uint16_t unknown_a4; + struct Entry { + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + }; + Entry entries[14]; +}; + // 6x4A: Fully shield attack + +struct G_ShieldAttack_6x4A { + G_ClientIDHeader header; +}; + // 6x4B: Hit by enemy + +struct G_HitByEnemy_6x4B_6x4C { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t damage; + le_float unknown_a2; + le_float unknown_a3; +}; + // 6x4C: Hit by enemy +// Same format as 6x4B + // 6x4D: Unknown (supported; lobby & game) + +struct G_Unknown_6x4D { + G_ClientIDHeader header; + le_uint32_t unknown_a1; +}; + // 6x4E: Unknown (supported; lobby & game) + +struct G_Unknown_6x4E { + G_ClientIDHeader header; +}; + // 6x4F: Unknown (supported; lobby & game) + +struct G_Unknown_6x4F { + G_ClientIDHeader header; +}; + // 6x50: Unknown (supported; lobby & game) + +struct G_Unknown_6x50 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; +}; + // 6x51: Invalid subcommand + // 6x52: Toggle shop/bank interaction + +struct G_Unknown_6x52 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6x53: Unknown (supported; game only) + +struct G_Unknown_6x53 { + G_ClientIDHeader header; +}; + // 6x54: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x55: Intra-map warp + +struct G_IntraMapWarp_6x55 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; + le_float x1; + le_float y1; + le_float z1; + le_float x2; + le_float y2; + le_float z2; +}; + // 6x56: Unknown (supported; lobby & game) + +struct G_Unknown_6x56 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; + le_float x; + le_float y; + le_float z; +}; + // 6x57: Unknown (supported; lobby & game) + +struct G_Unknown_6x57 { + G_ClientIDHeader header; +}; + // 6x58: Unknown (supported; game only) +struct G_Unknown_6x58 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x59: Pick up item struct G_PickUpItem_6x59 { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint16_t client_id2; le_uint16_t area; le_uint32_t item_id; @@ -3207,23 +3553,26 @@ struct G_PickUpItem_6x59 { // 6x5A: Request to pick up item struct G_PickUpItemRequest_6x5A { - uint8_t command; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint32_t item_id; - uint8_t area; - uint8_t unused2[3]; + le_uint16_t area; + le_uint16_t unused; }; // 6x5B: Invalid subcommand + // 6x5C: Unknown +struct G_Unknown_6x5C { + G_UnusedHeader header; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; +}; + // 6x5D: Drop meseta or stacked item struct G_DropStackedItem_DC_6x5D { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint16_t area; le_uint16_t unused2; le_float x; @@ -3231,83 +3580,150 @@ struct G_DropStackedItem_DC_6x5D { ItemData data; }; -struct G_DropStackedItem_PC_V3_BB_6x5D : G_DropStackedItem_DC_6x5D { +struct G_DropStackedItem_PC_V3_BB_6x5D { + G_DropStackedItem_DC_6x5D basic_cmd; le_uint32_t unused3; }; // 6x5E: Buy item at shop struct G_BuyShopItem_6x5E { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; ItemData item; }; // 6x5F: Drop item from box/enemy struct G_DropItem_DC_6x5F { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; uint8_t area; uint8_t from_enemy; le_uint16_t request_id; // < 0x0B50 if from_enemy != 0; otherwise < 0x0BA0 le_float x; le_float z; - le_uint32_t unused2; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; ItemData data; }; -struct G_DropItem_PC_V3_BB_6x5F : G_DropItem_DC_6x5F { +struct G_DropItem_PC_V3_BB_6x5F { + G_DropItem_DC_6x5F basic_cmd; le_uint32_t unused3; }; // 6x60: Request for item drop (handled by the server on BB) struct G_EnemyDropItemRequest_DC_6x60 { - uint8_t command; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; uint8_t area; uint8_t enemy_id; le_uint16_t request_id; le_float x; le_float z; - le_uint32_t unknown_a1; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; }; -struct G_EnemyDropItemRequest_PC_V3_BB_6x60 : G_EnemyDropItemRequest_DC_6x60 { +struct G_EnemyDropItemRequest_PC_V3_BB_6x60 { + G_EnemyDropItemRequest_DC_6x60 basic_cmd; le_uint32_t unknown_a2; }; // 6x61: Feed MAG + +struct G_FeedMAG_6x61 { + G_UnusedHeader header; + le_uint32_t mag_item_id; + le_uint32_t fed_item_id; +}; + // 6x62: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x63: Destroy item on the ground (used when too many items have been dropped) + +struct G_DestroyGroundItem_6x63 { + G_UnusedHeader header; + le_uint32_t item_id; + le_uint32_t area; +}; + // 6x64: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x65: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x66: Use star atomizer + +struct G_UseStarAtomizer_6x66 { + G_UnusedHeader header; + parray target_client_ids; +}; + // 6x67: Create enemy set + +struct G_CreateEnemySet_6x67 { + G_UnusedHeader header; + // unused1 could be area; the client checks this againset a global but the + // logic is the same in both branches + le_uint32_t unused1; + le_uint32_t unknown_a1; + le_uint32_t unused2; +}; + // 6x68: Telepipe/Ryuker + +struct G_CreateTelepipe_6x68 { + G_UnusedHeader header; + le_uint16_t client_id2; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + parray unknown_a3; + le_float x; + le_float y; + le_float z; + le_uint32_t unknown_a4; +}; + // 6x69: Unknown (supported; game only) + +struct G_Unknown_6x69 { + G_UnusedHeader header; + le_uint16_t client_id2; + le_uint16_t unknown_a1; + le_uint16_t what; // 0-3; logic is very different for each value + le_uint16_t unknown_a2; +}; + // 6x6A: Unknown (supported; game only; not valid on Episode 3) +struct G_Unknown_6x6A { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x6B: Sync enemy state (used while loading into game; same header format as 6E) // 6x6C: Sync object state (used while loading into game; same header format as 6E) // 6x6D: Sync item state (used while loading into game; same header format as 6E) // 6x6E: Sync flag state (used while loading into game) struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { - uint8_t subcommand; - parray unused; + G_ExtendedHeader header; le_uint32_t subcommand_size; - le_uint32_t unknown_a1; - le_uint32_t data_size; // Must be <= subcommand_size - 0x10 + le_uint32_t decompressed_size; + le_uint32_t compressed_size; // Must be <= subcommand_size - 0x10 // BC0-compressed data follows here (use bc0_decompress from Compression.hh) }; // 6x6F: Unknown (used while loading into game) +struct G_Unknown_6x6F { + G_UnusedHeader header; + parray unknown_a1; +}; + // 6x70: Sync player disp data and inventory (used while loading into game) // Annoyingly, they didn't use the same format as the 65/67/68 commands here, // and instead rearranged a bunch of things. @@ -3315,11 +3731,8 @@ struct G_SyncGameStateHeader_6x6B_6x6C_6x6D_6x6E { // sending player doesn't have 0 meseta, for example) struct G_Unknown_6x70 { + G_ExtendedHeader header; // Offsets in this struct are relative to the overall command header - /* 0004 */ uint8_t subcommand; // == 0x70 - /* 0005 */ uint8_t basic_size; // == 0 - /* 0006 */ le_uint16_t unused; - /* 0008 */ le_uint32_t subcommand_size; /* 000C */ parray unknown_a1; // [1] and [3] in this array (and maybe [2] also) appear to be le_floats; // they could be the player's current (x, y, z) coords @@ -3379,85 +3792,539 @@ struct G_Unknown_6x70 { }; // 6x71: Unknown (used while loading into game) + +struct G_Unknown_6x71 { + G_UnusedHeader header; +}; + // 6x72: Unknown (used while loading into game) -// 6x73: Invalid subcommand (but apparently valid on BB; function is unknown) + +struct G_Unknown_6x72 { + G_UnusedHeader header; +}; + +// 6x73: Unknown + +struct G_Unknown_6x73 { + G_UnusedHeader header; +}; + // 6x74: Word select -// 6x75: Unknown (supported; game only) + +struct G_WordSelect_6x74 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + parray entries; + le_uint32_t unknown_a3; + le_uint32_t unknown_a4; +}; + +// 6x75: Phase setup (supported; game only) + +// TODO: Not sure about PC format here. Is this first struct DC-only? +struct G_PhaseSetup_DC_PC_6x75 { + G_UnusedHeader header; + le_uint16_t phase; // Must be < 0x400 + le_uint16_t unknown_a1; // Must be 0 or 1 +}; + +struct G_PhaseSetup_V3_BB_6x75 { + G_PhaseSetup_DC_PC_6x75 basic_cmd; + le_uint16_t difficulty; + le_uint16_t unused; +}; + // 6x76: Enemy killed + +struct G_EnemyKilled_6x76 { + G_EnemyIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; // Flags of some sort +}; + // 6x77: Sync quest data + +struct G_SyncQuestData_6x77 { + G_UnusedHeader header; + le_uint16_t register_number; // Must be < 0x100 + le_uint16_t unused; + le_uint32_t value; +}; + // 6x78: Unknown -// 6x79: Lobby 14/15 soccer game + +struct G_Unknown_6x78 { + G_UnusedHeader header; + le_uint16_t client_id; // Must be < 12 + le_uint16_t unused1; + le_uint32_t unused2; +}; + +// 6x79: Lobby 14/15 gogo ball (soccer game) + +struct G_GogoBall_6x79 { + G_UnusedHeader header; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + le_float unknown_a3; + le_float unknown_a4; + uint8_t unknown_a5; + parray unused; +}; + // 6x7A: Unknown + +struct G_Unknown_6x7A { + G_ClientIDHeader header; +}; + // 6x7B: Unknown + +struct G_Unknown_6x7B { + G_ClientIDHeader header; +}; + // 6x7C: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x7C { + G_UnusedHeader header; + le_uint16_t client_id; + parray unknown_a1; + le_uint16_t unknown_a2; + parray unknown_a3; + parray unknown_a4; + parray unknown_a5; + le_uint16_t unknown_a6; + parray unknown_a7; + le_uint32_t unknown_a8; + le_uint32_t unknown_a9; + le_uint32_t unknown_a10; + le_uint32_t unknown_a11; + le_uint32_t unknown_a12; + parray unknown_a13; + struct Entry { + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + }; + Entry entries[3]; +}; + // 6x7D: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x7D { + G_UnusedHeader header; + uint8_t unknown_a1; // Must be < 7; used in jump table + parray unused; + parray unknown_a2; +}; + // 6x7E: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x7F: Unknown (not valid on Episode 3) + +struct G_Unknown_6x7F { + G_UnusedHeader header; + parray unknown_a1; +}; + // 6x80: Trigger trap (not valid on Episode 3) + +struct G_TriggerTrap_6x80 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; +}; + // 6x81: Unknown + +struct G_Unknown_6x81 { + G_ClientIDHeader header; +}; + // 6x82: Unknown + +struct G_Unknown_6x82 { + G_ClientIDHeader header; +}; + // 6x83: Place trap + +struct G_PlaceTrap_6x83 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; +}; + // 6x84: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x84 { + G_UnusedHeader header; + parray unknown_a1; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + le_uint16_t unused; +}; + // 6x85: Unknown (supported; game only; not valid on Episode 3) -// 6x86: Hit destructible wall (not valid on Episode 3) + +struct G_Unknown_6x85 { + G_UnusedHeader header; + le_uint16_t unknown_a1; // Command is ignored unless this is 0 + parray unknown_a2; // Only the first 3 appear to be used +}; + +// 6x86: Hit destructible object (not valid on Episode 3) + +struct G_HitDestructibleObject_6x86 { + G_ObjectIDHeader header; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + le_uint16_t unknown_a3; + le_uint16_t unknown_a4; +}; + // 6x87: Unknown + +struct G_Unknown_6x87 { + G_ClientIDHeader header; + le_float unknown_a1; +}; + // 6x88: Unknown (supported; game only) + +struct G_Unknown_6x88 { + G_ClientIDHeader header; +}; + // 6x89: Unknown (supported; game only) + +struct G_Unknown_6x89 { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unused; +}; + // 6x8A: Unknown (not valid on Episode 3) + +struct G_Unknown_6x8A { + G_ClientIDHeader header; + le_uint32_t unknown_a1; // Must be < 0x11 +}; + // 6x8B: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x8C: Unknown (not valid on Episode 3) -// 6x8D: Unknown (supported; lobby & game) +// This subcommand is completely ignored (at least, by PSO GC). + +// 6x8D: Set technique level override + +struct G_SetTechniqueLevelOverride_6x8D { + G_ClientIDHeader header; + uint8_t level_upgrade; + uint8_t unused1; + le_uint16_t unused2; +}; + // 6x8E: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x8F: Unknown (not valid on Episode 3) + +struct G_Unknown_6x8F { + G_ClientIDHeader header; + le_uint16_t client_id2; + le_uint16_t unknown_a1; +}; + // 6x90: Unknown (not valid on Episode 3) + +struct G_Unknown_6x90 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; +}; + // 6x91: Unknown (supported; game only) + +struct G_Unknown_6x91 { + G_ObjectIDHeader header; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + le_uint16_t unknown_a3; + le_uint16_t unknown_a4; + le_uint16_t unknown_a5; + parray unknown_a6; +}; + // 6x92: Unknown (not valid on Episode 3) + +struct G_Unknown_6x92 { + G_UnusedHeader header; + le_uint32_t unknown_a1; + le_float unknown_a2; +}; + // 6x93: Timed switch activated (not valid on Episode 3) + +struct G_TimedSwitchActivated_6x93 { + G_UnusedHeader header; + le_uint16_t area; + le_uint16_t switch_id; + uint8_t unknown_a1; // Logic is different if this is 1 vs. any other value + parray unused; +}; + // 6x94: Warp (not valid on Episode 3) + +struct G_InterLevelWarp_6x94 { + G_UnusedHeader header; + le_uint16_t area; + parray unused; +}; + // 6x95: Unknown (not valid on Episode 3) + +struct G_Unknown_6x95 { + G_UnusedHeader header; + le_uint32_t client_id; + le_uint32_t unknown_a1; + le_uint32_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6x96: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x97: Unknown (not valid on Episode 3) + +struct G_Unknown_6x97 { + G_UnusedHeader header; + le_uint32_t unused1; + le_uint32_t unknown_a1; // Must be 0 or 1 + le_uint32_t unused2; + le_uint32_t unused3; +}; + // 6x98: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x99: Unknown +// This subcommand is completely ignored (at least, by PSO GC). + // 6x9A: Update player stat (not valid on Episode 3) + +struct G_UpdatePlayerStat_6x9A { + G_ClientIDHeader header; + le_uint16_t client_id2; + // Values for what: + // 0 = subtract HP + // 1 = subtract TP + // 2 = subtract Meseta + // 3 = add HP + // 4 = add TP + uint8_t what; + uint8_t amount; +}; + // 6x9B: Unknown + +struct G_Unknown_6x9B { + G_UnusedHeader header; + uint8_t unknown_a1; + parray unused; +}; + // 6x9C: Unknown (supported; game only; not valid on Episode 3) + +struct G_Unknown_6x9C { + G_EnemyIDHeader header; + le_uint32_t unknown_a1; +}; + // 6x9D: Unknown (not valid on Episode 3) + +struct G_Unknown_6x9D { + G_UnusedHeader header; + le_uint32_t client_id2; +}; + // 6x9E: Unknown (not valid on Episode 3) +// This subcommand is completely ignored (at least, by PSO GC). + // 6x9F: Gal Gryphon actions (not valid on PC or Episode 3) + +struct G_GalGryphonActions_6x9F { + G_EnemyIDHeader header; + le_uint32_t unknown_a1; + le_float unknown_a2; + le_float unknown_a3; +}; + // 6xA0: Gal Gryphon actions (not valid on PC or Episode 3) + +struct G_GalGryphonActions_6xA0 { + G_EnemyIDHeader header; + le_float x; + le_float y; + le_float z; + le_uint32_t unknown_a1; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + parray unknown_a4; +}; + // 6xA1: Unknown (not valid on PC) +struct G_Unknown_6xA1 { + G_ClientIDHeader header; +}; + // 6xA2: Request for item drop from box (not valid on PC; handled by server on BB) struct G_BoxItemDropRequest_6xA2 { - uint8_t command; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; uint8_t area; - uint8_t unused2; + uint8_t unknown_a1; le_uint16_t request_id; le_float x; le_float z; - le_uint32_t unknown[6]; + le_uint16_t unknown_a2; + le_uint16_t unknown_a3; + parray unknown_a4; + le_uint32_t unknown_a5; + le_uint32_t unknown_a6; + le_uint32_t unknown_a7; + le_uint32_t unknown_a8; }; // 6xA3: Episode 2 boss actions (not valid on PC or Episode 3) + +struct G_Episode2BossActions_6xA3 { + G_EnemyIDHeader header; + uint8_t unknown_a1; + uint8_t unknown_a2; + parray unknown_a3; +}; + // 6xA4: Olga Flow phase 1 actions (not valid on PC or Episode 3) + +struct G_OlgaFlowPhase1Actions_6xA4 { + G_EnemyIDHeader header; + uint8_t what; + parray unknown_a3; +}; + // 6xA5: Olga Flow phase 2 actions (not valid on PC or Episode 3) + +struct G_OlgaFlowPhase2Actions_6xA5 { + G_EnemyIDHeader header; + uint8_t what; + parray unknown_a3; +}; + // 6xA6: Modify trade proposal (not valid on PC) + +struct G_ModifyTradeProposal_6xA6 { + G_ClientIDHeader header; + uint8_t unknown_a1; // Must be < 8 + uint8_t unknown_a2; + parray unknown_a3; + le_uint32_t unknown_a4; + le_uint32_t unknown_a5; +}; + // 6xA7: Unknown (not valid on PC) +// This subcommand is completely ignored (at least, by PSO GC). + // 6xA8: Gol Dragon actions (not valid on PC or Episode 3) + +struct G_GolDragonActions_6xA8 { + G_EnemyIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6xA9: Barba Ray actions (not valid on PC or Episode 3) + +struct G_BarbaRayActions_6xA9 { + G_EnemyIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; +}; + // 6xAA: Episode 2 boss actions (not valid on PC or Episode 3) + +struct G_Episode2BossActions_6xAA { + G_EnemyIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6xAB: Create lobby chair (not valid on PC) + +struct G_CreateLobbyChair_6xAB { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; +}; + // 6xAC: Unknown (not valid on PC) + +struct G_Unknown_6xAC { + G_ClientIDHeader header; + le_uint32_t num_items; + parray item_ids; +}; + // 6xAD: Unknown (not valid on PC, Episode 3, or GC Trial Edition) -// 6xAE: Set chair state? (sent by existing clients at join time; not valid on PC or GC Trial Edition) -// 6xAF: Turn in lobby chair (not valid on PC or GC Trial Edition) -// 6xB0: Move in lobby chair (not valid on PC or GC Trial Edition) + +struct G_Unknown_6xAD { + G_UnusedHeader header; + // The first byte in this array seems to have a special meaning + parray unknown_a1; +}; + +// 6xAE: Set lobby chair state (sent by existing clients at join time) +// This subcommand is not valid on DC, PC, or GC Trial Edition. + +struct G_SetLobbyChairState_6xAE { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; + le_uint32_t unknown_a4; +}; + +// 6xAF: Turn lobby chair (not valid on PC or GC Trial Edition) + +struct G_TurnLobbyChair_6xAF { + G_ClientIDHeader header; + le_uint32_t angle; // In range [0x0000, 0xFFFF] +}; + +// 6xB0: Move lobby chair (not valid on PC or GC Trial Edition) + +struct G_MoveLobbyChair_6xB0 { + G_ClientIDHeader header; + le_uint32_t unknown_a1; +}; + // 6xB1: Unknown (not valid on PC or GC Trial Edition) +// This subcommand is completely ignored (at least, by PSO GC). + // 6xB2: Unknown (not valid on PC or GC Trial Edition) + +struct G_Unknown_6xB2 { + G_UnusedHeader header; + parray unknown_a1; + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; +}; + // 6xB3: Unknown (XBOX) // 6xB3: CARD battle command (Episode 3) @@ -3465,17 +4332,14 @@ struct G_BoxItemDropRequest_6xA2 { // These commands have multiple subcommands; see the Episode 3 subsubcommand // table after this table. The common format is: struct G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - uint8_t subcommand; // 0xB3 - uint8_t size; - le_uint16_t header_w1; + G_UnusedHeader basic_header; uint8_t subsubcommand; // See 6xBx subcommand table (after this table) - uint8_t header_b1; - // If mask_key is nonzero, the remainder of the data is encrypted using a - // simple algorithm. See set_mask_for_ep3_game_command in SendCommands.cc for - // a description of this algorithm. + uint8_t unknown_a1; + // If mask_key is nonzero, the remainder of the data (after header_b2 in this + // struct) is encrypted using a simple algorithm, which is implemented in + // set_mask_for_ep3_game_command in SendCommands.cc. uint8_t mask_key; - uint8_t header_b2; - // The subsubcommand arguments follow here + uint8_t unused; }; // 6xB4: Unknown (XBOX) @@ -3483,6 +4347,11 @@ struct G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { // 6xB5: CARD battle command (Episode 3) - see 6xB3 (above) // 6xB5: BB shop request (handled by the server) +struct G_ShopContentsRequest_BB_6xB5 { + G_UnusedHeader header; + le_uint32_t shop_type; +}; + // 6xB6: Episode 3 map list and map contents (server->client only) // Unlike 6xB3-6xB5, these commands cannot be masked. Also unlike 6xB3-6xB5, // there are only two subsubcommands, so we list them inline here. @@ -3490,31 +4359,30 @@ struct G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { // command instead of the 60 command. struct G_MapSubsubcommand_GC_Ep3_6xB6 { - uint8_t subcommand; // 0xB6 - uint8_t size; // Unused; this command uses the extended (32-bit) size field - le_uint16_t unused; - le_uint32_t subcommand_size; - le_uint32_t subsubcommand; // 0x40 or 0x41 + G_ExtendedHeader header; + uint8_t subsubcommand; // 0x40 or 0x41 + parray unused; }; -struct G_MapList_GC_Ep3_6xB6x40 : G_MapSubsubcommand_GC_Ep3_6xB6 { - le_uint32_t compressed_data_size; +struct G_MapList_GC_Ep3_6xB6x40 { + G_MapSubsubcommand_GC_Ep3_6xB6 header; + le_uint16_t compressed_data_size; + le_uint16_t unused; // PRS-compressed map list follows (see Ep3DataIndex::get_compressed_map_list) }; -struct G_MapData_GC_Ep3_6xB6x41 : G_MapSubsubcommand_GC_Ep3_6xB6 { - le_int16_t map_number; +struct G_MapData_GC_Ep3_6xB6x41 { + G_MapSubsubcommand_GC_Ep3_6xB6 header; + le_uint32_t map_number; + le_uint16_t compressed_data_size; le_uint16_t unused; - le_uint32_t compressed_data_size; // PRS-compressed map data follows (which decompresses to an Ep3Map) }; // 6xB6: BB shop contents (server->client only) struct G_ShopContents_BB_6xB6 { - uint8_t subcommand; - uint8_t size; // always 2C regardless of the number of items?? - le_uint16_t params; // 037F + G_UnusedHeader header; uint8_t shop_type; uint8_t num_items; le_uint16_t unused; @@ -3526,9 +4394,7 @@ struct G_ShopContents_BB_6xB6 { // 6xB7: BB buy shop item (handled by the server) struct G_BuyShopItem_BB_6xB7 { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; le_uint32_t inventory_item_id; uint8_t shop_type; uint8_t item_index; @@ -3538,33 +4404,62 @@ struct G_BuyShopItem_BB_6xB7 { // 6xB8: Unknown (Episode 3 Trial Edition) // 6xB8: BB accept tekker result (handled by the server) -// Format is G_ItemIDSubcommand + +struct G_AcceptItemIdentification_BB_6xB8 { + G_UnusedHeader header; + le_uint32_t item_id; +}; // 6xB9: Unknown (Episode 3 Trial Edition) // 6xB9: BB provisional tekker result struct G_IdentifyResult_BB_6xB9 { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; ItemData item; }; // 6xBA: Unknown (Episode 3) +struct G_Unknown_GC_Ep3_6xBA { + G_ClientIDHeader header; + le_uint16_t unknown_a1; // Low byte must be < 9 + le_uint16_t unknown_a2; + le_uint32_t unknown_a3; + le_uint32_t unknown_a4; +}; + // 6xBA: BB accept tekker result (handled by the server) -// Format is G_ItemIDSubcommand + +struct G_AcceptItemIdentification_BB_6xBA { + G_UnusedHeader header; + le_uint32_t item_id; +}; // 6xBB: Unknown (Episode 3) + +struct G_Unknown_GC_Ep3_6xBB { + G_ClientIDHeader header; + le_uint16_t unknown_a1; // Low byte must be < 5 + le_uint16_t unknown_a2; + parray unknown_a3; +}; + // 6xBB: BB bank request (handled by the server) + // 6xBC: Unknown (Episode 3) + +struct G_Unknown_GC_Ep3_6xBC { + G_UnusedHeader header; + parray unused1; + // The length of this array strongly implies one flag or value per card type. + parray unknown_a1; + parray unused2; +}; + // 6xBC: BB bank contents (server->client only) struct G_BankContentsHeader_BB_6xBC { - uint8_t subcommand; - uint8_t unused1; - le_uint16_t unused2; - le_uint32_t size; // same as size in overall command header + G_ExtendedHeader header; le_uint32_t checksum; // can be random; client won't notice le_uint32_t numItems; le_uint32_t meseta; @@ -3572,12 +4467,22 @@ struct G_BankContentsHeader_BB_6xBC { }; // 6xBD: Unknown (Episode 3; not Trial Edition) + +// Note: This structure does not have a normal header - the client ID field is +// big-endian! +struct G_Unknown_GC_Ep3_6xBD { + G_ClientIDHeader header; + le_uint16_t unknown_a1; + le_uint16_t unknown_a2; + parray unknown_a3; + le_uint32_t unknown_a4; + le_uint32_t unknown_a5; +}; + // 6xBD: BB bank action (take/deposit meseta/item) (handled by the server) struct G_BankAction_BB_6xBD { - uint8_t subcommand; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; le_uint32_t item_id; // 0xFFFFFFFF = meseta; anything else = item le_uint32_t meseta_amount; uint8_t action; // 0 = deposit, 1 = take @@ -3586,28 +4491,42 @@ struct G_BankAction_BB_6xBD { }; // 6xBE: Sound chat (Episode 3; not Trial Edition) + +struct G_SoundChat_GC_Ep3_6xBE { + G_UnusedHeader header; + le_uint32_t sound_id; // Must be < 0x27 + be_uint32_t unknown_a1; +}; + // 6xBE: BB create inventory item (server->client only) struct G_CreateInventoryItem_BB_6xBE { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; ItemData item; le_uint32_t unused; }; // 6xBF: Change lobby music (Episode 3; not Trial Edition) + +struct G_ChangeLobbyMusic_GC_Ep3_6xBF { + G_UnusedHeader header; + le_uint32_t song_number; // Must be < 0x34 +}; + // 6xBF: Give EXP (BB) (server->client only) struct G_GiveExperience_BB_6xBF { - uint8_t subcommand; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint32_t amount; }; // 6xC0: BB sell item at shop -// Format is G_ItemSubcommand + +struct G_SellItemAtShop_BB_6xC0 { + G_UnusedHeader header; + le_uint32_t item_id; + le_uint32_t amount; +}; // 6xC1: Unknown // 6xC2: Unknown @@ -3617,9 +4536,7 @@ struct G_GiveExperience_BB_6xBF { // item drop subcommand is generated instead. struct G_SplitStackedItem_6xC3 { - uint8_t command; - uint8_t size; - le_uint16_t client_id; + G_ClientIDHeader header; le_uint16_t area; le_uint16_t unused2; le_float x; @@ -3631,9 +4548,7 @@ struct G_SplitStackedItem_6xC3 { // 6xC4: Sort inventory (handled by the server on BB) struct G_SortInventory_6xC4 { - uint8_t command; - uint8_t size; - le_uint16_t unused; + G_UnusedHeader header; le_uint32_t item_ids[30]; }; @@ -3644,10 +4559,8 @@ struct G_SortInventory_6xC4 { // 6xC8: Enemy killed (handled by the server on BB) struct G_EnemyKilled_6xC8 { - uint8_t command; - uint8_t size; + G_EnemyIDHeader header; le_uint16_t enemy_id2; - le_uint16_t enemy_id; le_uint16_t killer_client_id; le_uint32_t unused; }; @@ -3724,7 +4637,8 @@ struct DeckCardRef { // Note: The game treats these as le_uint16_ts // 6xB4x02: Update hands and equips -struct G_UpdateHand_GC_Ep3_6xB4x02 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_UpdateHand_GC_Ep3_6xB4x02 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t client_id; le_uint16_t unused3; parray unknown_a1; // Dice values are in here somewhere @@ -3743,7 +4657,8 @@ struct G_UpdateHand_GC_Ep3_6xB4x02 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_ // 6xB4x03: Set state flags -struct G_SetStateFlags_GC_Ep3_6xB4x03 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_SetStateFlags_GC_Ep3_6xB4x03 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t unknown_a1; parray unknown_a2; parray unknown_a3; @@ -3758,7 +4673,8 @@ struct G_SetStateFlags_GC_Ep3_6xB4x03 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6x // 6xB4x04: Update SC/FC stats -struct G_UpdateStats_GC_Ep3_6xB4x04 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_UpdateStats_GC_Ep3_6xB4x04 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t client_id; le_uint16_t unused; struct Entry { @@ -3777,7 +4693,8 @@ struct G_UpdateStats_GC_Ep3_6xB4x04 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4 // 6xB4x05: Update map state -struct G_UpdateMap_GC_Ep3_6xB4x05 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_UpdateMap_GC_Ep3_6xB4x05 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t width; le_uint16_t height; parray tiles; @@ -3799,7 +4716,8 @@ struct G_UpdateMap_GC_Ep3_6xB4x05 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6 // 6xB4x06: Unknown -struct G_Unknown_GC_Ep3_6xB4x06 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x06 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; parray unknown_a3; @@ -3807,7 +4725,8 @@ struct G_Unknown_GC_Ep3_6xB4x06 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x07: Update decks -struct G_UpdateDecks_GC_Ep3_6xB4x07 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_UpdateDecks_GC_Ep3_6xB4x07 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; struct Entry { ptext player_name; @@ -3838,7 +4757,8 @@ struct G_Unknown_GC_Ep3_6xB4x09 { // 6xB4x0A: Unknown -struct G_Unknown_GC_Ep3_6xB4x0A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x0A { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t client_id; int8_t unknown_a1; // must be between -1 and 8 inclusive uint8_t unknown_a2; @@ -3865,70 +4785,77 @@ struct G_Unknown_GC_Ep3_6xB4x0A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB parray unknown_a15; }; -// 6xB3x0B: Unknown +// 6xB3x0B / CAx0B: Unknown -struct G_Unknown_GC_Ep3_6xB3x0B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; - le_uint16_t client_id; - parray unknown_a2; - // Command may be longer than this structure -}; - -// 6xB3x0C: Unknown - -struct G_Unknown_GC_Ep3_6xB3x0C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; - le_uint16_t client_id; - parray unknown_a2; - // Command may be longer than this structure -}; - -// 6xB3x0D: Unknown - -struct G_Unknown_GC_Ep3_6xB3x0D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x0B_CAx0B { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; be_uint32_t unknown_a1; - parray unknown_a2; le_uint16_t client_id; - parray unknown_a3; - // Command may be longer than this structure + parray unknown_a2; }; -// 6xB3x0E: Unknown +// 6xB3x0C / CAx0C: Unknown -struct G_Unknown_GC_Ep3_6xB3x0E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_Unknown_GC_Ep3_6xB3x0C_CAx0C { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint16_t client_id; + parray unknown_a2; +}; + +// 6xB3x0D / CAx0D: Unknown + +struct G_Unknown_GC_Ep3_6xB3x0D_CAx0D { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint16_t client_id; + parray unknown_a2; +}; + +// 6xB3x0E / CAx0E: Unknown + +struct G_Unknown_GC_Ep3_6xB3x0E_CAx0E { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; le_uint16_t client_id; le_uint16_t unknown_a2; - // Command may be longer than this structure }; -// 6xB3x0F: Unknown +// 6xB3x0F / CAx0F: Unknown -struct G_Unknown_GC_Ep3_6xB3x0F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_Unknown_GC_Ep3_6xB3x0F_CAx0F { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; le_uint16_t client_id; parray unknown_a2; parray unknown_a3; parray unused; - // Command may be longer than this structure }; -// 6xB3x10: Unknown +// 6xB3x10 / CAx10: Unknown -struct G_Unknown_GC_Ep3_6xB3x10 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_Unknown_GC_Ep3_6xB3x10_CAx10 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; le_uint16_t client_id; le_uint16_t unknown_a2; // Note: This field's type is the same as the corresponding field in 6xB3x0F parray unknown_a3; parray unused; - // Command may be longer than this structure }; -// 6xB3x11: Unknown +// 6xB3x11 / CAx11: Unknown -struct G_Unknown_GC_Ep3_6xB3x11 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_Unknown_GC_Ep3_6xB3x11_CAx11 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; le_uint16_t client_id; parray unknown_a2; @@ -3944,19 +4871,22 @@ struct G_Unknown_GC_Ep3_6xB3x11 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB le_uint16_t unknown_a10; }; -// 6xB3x12: Unknown +// 6xB3x12 / CAx12: Unknown -struct G_Unknown_GC_Ep3_6xB3x12 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x12_CAx12 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; be_uint32_t unknown_a1; - parray unknown_a2; le_uint16_t client_id; parray unknown_a3; }; -// 6xB3x13: Unknown +// 6xB3x13 / CAx13: Update game state -struct G_Unknown_GC_Ep3_6xB3x13 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_UpdateGameState_GC_Ep3_6xB3x13_CAx13 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; // Note: This section's format is a guess based on the fact that the client // calls the same function to byteswap it as for the update map command above. @@ -3969,61 +4899,69 @@ struct G_Unknown_GC_Ep3_6xB3x13 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB uint8_t unknown_a4; parray unknown_a5; le_uint16_t unknown_a6; - // 120 from subcommand start parray unknown_a7; le_uint32_t map_number; le_uint32_t unknown_a8; Ep3BattleRules rules; parray unknown_a9; + parray overlay_tiles; parray unknown_a10; parray unknown_a11; parray unknown_a12; }; -// 6xB3x14: Unknown +// 6xB3x14 / CAx14: Update field state -struct G_Unknown_GC_Ep3_6xB3x14 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_UpdateFieldState_GC_Ep3_6xB3x14_CAx14 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; le_uint16_t client_id; parray unknown_a2; + parray name; + le_uint32_t unknown_a3; parray card_ids; - parray unknown_a3; - le_uint16_t unknown_a4; - parray unknown_a5; + parray unknown_a4; + le_uint16_t unknown_a5; + parray unknown_a6; }; // 6xB3x15: Unknown -struct G_Unknown_GC_Ep3_6xB3x15 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x15 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // No arguments }; // 6xB5x17: Unknown -struct G_Unknown_GC_Ep3_6xB5x17 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x17 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // No arguments }; // 6xB5x1A: Unknown -struct G_Unknown_GC_Ep3_6xB5x1A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x1A { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // No arguments }; -// 6xB3x1B: Unknown +// 6xB3x1B / CAx1B: Update names -struct G_Unknown_GC_Ep3_6xB3x1B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // The handler reads a byte from offset 0x20 after the subcommand start (which - // would be offset 0x18 in this struct), so the command must be at least 0x1C - // bytes in total. It is unlikely to be longer, but I have no examples. - parray unknown_a1; +struct G_UpdateNames_GC_Ep3_6xB3x1B_CAx1B { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + parray unknown_a2; }; // 6xB4x1C: Set player names -struct G_Unknown_GC_Ep3_6xB4x1C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x1C { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; struct Entry { ptext name; uint8_t client_id; // 0xFF if slot is empty @@ -4033,15 +4971,18 @@ struct G_Unknown_GC_Ep3_6xB4x1C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB Entry entries[4]; }; -// 6xB3x1D: Unknown +// 6xB3x1D / CAx1D: Unknown -struct G_Unknown_GC_Ep3_6xB3x1D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // No arguments +struct G_Unknown_GC_Ep3_6xB3x1D_CAx1D { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; }; // 6xB4x1E: Unknown -struct G_Unknown_GC_Ep3_6xB4x1E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x1E { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; be_uint32_t unknown_a1; uint8_t unknown_a2; uint8_t unknown_a3; @@ -4050,34 +4991,41 @@ struct G_Unknown_GC_Ep3_6xB4x1E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x1F: Unknown -struct G_Unknown_GC_Ep3_6xB4x1F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x1F { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint32_t unknown_a1; }; // 6xB5x20: Unknown -struct G_Unknown_GC_Ep3_6xB5x20 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x20 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint32_t player_tag; le_uint32_t guild_card_number; uint8_t client_id; parray unused; }; -// 6xB3x21: Unknown +// 6xB3x21 / CAx21: Unknown -struct G_Unknown_GC_Ep3_6xB3x21 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // No arguments +struct G_Unknown_GC_Ep3_6xB3x21_CAx21 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint32_t unknown_a2; }; // 6xB4x22: Unknown -struct G_Unknown_GC_Ep3_6xB4x22 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x22 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // No arguments }; // 6xB4x23: Unknown -struct G_Unknown_GC_Ep3_6xB4x23 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x23 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // This command does nothing at all. If unknown_a1 == 1, then it calls another // function, but that function also does nothing. uint8_t unknown_a1; @@ -4087,7 +5035,8 @@ struct G_Unknown_GC_Ep3_6xB4x23 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x27: Unknown -struct G_Unknown_GC_Ep3_6xB5x27 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x27 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1 as well, which looks like another client // ID (it must be < 4, though it does not always match unknown_a1 below). le_uint32_t unknown_a1; // Probably client ID (must be < 4) @@ -4096,18 +5045,21 @@ struct G_Unknown_GC_Ep3_6xB5x27 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB le_uint32_t unused; // Curiously, this usually contains a memory address }; -// 6xB3x28: Unknown +// 6xB3x28 / CAx28: Unknown -struct G_Unknown_GC_Ep3_6xB3x28 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x28_CAx28 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; be_uint32_t unknown_a1; - parray unused1; + uint8_t unused1; int8_t unknown_a2; parray unused2; }; // 6xB4x29: Unknown -struct G_Unknown_GC_Ep3_6xB4x29 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x29 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; // Note: This is byteswapped by the same function as the corresponding part of @@ -4124,22 +5076,27 @@ struct G_Unknown_GC_Ep3_6xB4x29 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x2A: Unknown -struct G_Unknown_GC_Ep3_6xB4x2A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x2A { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; le_uint16_t unknown_a2; parray unused; }; -// 6xB3x2B: Unknown +// 6xB3x2B / CAx2B: Unknown -struct G_Unknown_GC_Ep3_6xB3x2B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // This command is completely ignored by the client. Its structure (if any) is - // unknown. +struct G_Unknown_GC_Ep3_6xB3x2B_CAx2B { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint16_t unknown_a2; + parray unknown_a3; }; // 6xB4x2C: Unknown -struct G_Unknown_GC_Ep3_6xB4x2C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x2C { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; uint8_t unknown_a2; // Only used if the preceding field == 3 parray unknown_a3; @@ -4149,7 +5106,8 @@ struct G_Unknown_GC_Ep3_6xB4x2C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x2D: Unknown -struct G_Unknown_GC_Ep3_6xB5x2D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x2D { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // This array is indexed into by a global variable. I don't have any examples // of this command, so I don't know how long the array should be - 4 is a // probably-incorrect guess. @@ -4158,14 +5116,16 @@ struct G_Unknown_GC_Ep3_6xB5x2D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x2E: Unknown -struct G_Unknown_GC_Ep3_6xB5x2E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x2E { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; // Command ignored unless this is 0 or 1 parray unused; }; // 6xB5x2F: Unknown -struct G_Unknown_GC_Ep3_6xB5x2F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x2F { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; parray unknown_a2; @@ -4181,13 +5141,15 @@ struct G_Unknown_GC_Ep3_6xB5x2F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x30: Unknown -struct G_Unknown_GC_Ep3_6xB5x30 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x30 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // No arguments }; // 6xB5x31: Unknown -struct G_Unknown_GC_Ep3_6xB5x31 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x31 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1 for... something. uint8_t unknown_a1; // Must be 0 or 1 uint8_t unknown_a2; // Must be < 4 @@ -4199,7 +5161,8 @@ struct G_Unknown_GC_Ep3_6xB5x31 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x32: Unknown -struct G_Unknown_GC_Ep3_6xB5x32 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x32 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1 for... something. le_uint16_t unknown_a1; le_uint16_t unknown_a2; @@ -4208,16 +5171,19 @@ struct G_Unknown_GC_Ep3_6xB5x32 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x33: Unknown -struct G_Unknown_GC_Ep3_6xB4x33 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x33 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; uint8_t unused; le_uint16_t unknown_a2; }; -// 6xB3x34: Unknown +// 6xB3x34 / CAx34: Unknown -struct G_Unknown_GC_Ep3_6xB3x34 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - parray unknown_a1; +struct G_Unknown_GC_Ep3_6xB3x34_CAx34 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; uint8_t client_id; uint8_t unknown_a2; le_uint16_t unknown_a3; // Possibly DeckCardRef @@ -4225,7 +5191,8 @@ struct G_Unknown_GC_Ep3_6xB3x34 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x35: Unknown -struct G_Unknown_GC_Ep3_6xB4x35 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x35 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; uint8_t unknown_a2; le_uint16_t unknown_a3; @@ -4233,14 +5200,16 @@ struct G_Unknown_GC_Ep3_6xB4x35 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x36: Unknown -struct G_Unknown_GC_Ep3_6xB5x36 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x36 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; // Must be < 12 (maybe lobby or spectator team client ID) parray unused; }; // 6xB3x37: Unknown -struct G_Unknown_GC_Ep3_6xB3x37 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x37 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unused1; uint8_t unknown_a1; parray unused2; @@ -4248,7 +5217,8 @@ struct G_Unknown_GC_Ep3_6xB3x37 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x38: Unknown -struct G_Unknown_GC_Ep3_6xB5x38 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x38 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; uint8_t unknown_a2; parray unused; @@ -4256,7 +5226,8 @@ struct G_Unknown_GC_Ep3_6xB5x38 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x39: Unknown -struct G_Unknown_GC_Ep3_6xB4x39 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x39 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; struct Entry { parray unknown_a1; parray unknown_a2; @@ -4264,22 +5235,25 @@ struct G_Unknown_GC_Ep3_6xB4x39 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB Entry entries[4]; }; -// 6xB3x3A: Unknown +// 6xB3x3A / CAx3A: Unknown -struct G_Unknown_GC_Ep3_6xB3x3A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // This command is completely ignored by the client. Its structure (if any) is - // unknown. +struct G_Unknown_GC_Ep3_6xB3x3A_CAx3A { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; }; // 6xB4x3B: Unknown -struct G_Unknown_GC_Ep3_6xB4x3B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x3B { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unused; }; // 6xB5x3C: Unknown -struct G_Unknown_GC_Ep3_6xB5x3C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x3C { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1 for... something. uint8_t unknown_a1; parray unused; @@ -4287,7 +5261,8 @@ struct G_Unknown_GC_Ep3_6xB5x3C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x3D: Unknown -struct G_Unknown_GC_Ep3_6xB4x3D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x3D { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; struct Entry { uint8_t type; // 1 = human, 2 = COM @@ -4309,7 +5284,8 @@ struct G_Unknown_GC_Ep3_6xB4x3D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x3E: Unknown -struct G_Unknown_GC_Ep3_6xB5x3E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x3E { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1 for... something. uint8_t unknown_a1; uint8_t unknown_a2; @@ -4318,7 +5294,8 @@ struct G_Unknown_GC_Ep3_6xB5x3E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x3F: Unknown -struct G_Unknown_GC_Ep3_6xB5x3F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x3F { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; int8_t unknown_a1; // Must be in the range [-1, 0x14] uint8_t unknown_a2; // Must be < 4 parray unused1; @@ -4326,29 +5303,38 @@ struct G_Unknown_GC_Ep3_6xB5x3F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB parray unused2; }; -// 6xB3x40: Unknown +// 6xB3x40 / CAx40: Map list request +// If sent to a client as 6xB3x40, the client ignores the command completely. If +// sent to the server, the server should respond with a 6xB6x40 command. -struct G_Unknown_GC_Ep3_6xB3x40 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // This command is completely ignored by the client. Its structure (if any) is - // unknown. It's possible that this command was replaced by 6xB6x40. +struct G_MapListRequest_GC_Ep3_6xB3x40_CAx40 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; }; -// 6xB3x41: Unknown +// 6xB3x41 / CAx41: Map data request +// If sent to a client as 6xB3x41, the client ignores the command completely. If +// sent to the server, the server should respond with a 6xB6x41 command. -struct G_Unknown_GC_Ep3_6xB3x41 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // This command is completely ignored by the client. Its structure (if any) is - // unknown. It's possible that this command was replaced by 6xB6x41. +struct G_MapDataRequest_GC_Ep3_6xB3x41_CAx41 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint32_t map_number; }; // 6xB5x42: Unknown -struct G_Unknown_GC_Ep3_6xB5x42 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x42 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // This command uses header_b1, but has no other arguments. }; // 6xB5x43: Unknown -struct G_Unknown_GC_Ep3_6xB5x43 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x43 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; struct Entry { // Both fields here are masked. To get the actual values used by the game, // XOR the values here with 0x39AB. @@ -4360,21 +5346,24 @@ struct G_Unknown_GC_Ep3_6xB5x43 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x44: Unknown -struct G_Unknown_GC_Ep3_6xB5x44 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x44 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1, which must be < 4. parray unknown_a1; }; // 6xB5x45: Unknown -struct G_Unknown_GC_Ep3_6xB5x45 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x45 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1, which must be < 4. parray, 8> unknown_a1; }; // 6xB4x46: Start battle -struct G_Unknown_GC_Ep3_6xB4x46 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x46 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // In all of the examples (from Sega's servers) that I've seen of this // command, these fields have the following values: // version_signature = "[V1][FINAL2.0] 03/09/13 15:30 by K.Toya" @@ -4388,30 +5377,36 @@ struct G_Unknown_GC_Ep3_6xB4x46 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB5x47: Unknown -struct G_Unknown_GC_Ep3_6xB5x47 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB5x47 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: This command uses header_b1, which must be < 12. le_uint32_t unknown_a1; }; -// 6xB3x48: Unknown +// 6xB3x48 / CAx48: Unknown -struct G_Unknown_GC_Ep3_6xB3x48 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB3x48_CAx48 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; be_uint32_t unknown_a1; - parray unused1; uint8_t unknown_a2; parray unused2; }; -// 6xB3x49: Unknown +// 6xB3x49 / CAx49: Unknown -struct G_Unknown_GC_Ep3_6xB3x49 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { - // This command is completely ignored by the client. Its structure (if any) is - // unknown. It's possible that this command was replaced by 6xB6x41. +struct G_Unknown_GC_Ep3_6xB3x49_CAx49 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; + be_uint32_t sequence_number; + be_uint32_t unknown_a1; + le_uint32_t unknown_a2; + parray unknown_a3; }; // 6xB4x4A: Unknown -struct G_Unknown_GC_Ep3_6xB4x4A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4A { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: entry_count appears not to be bounds-checked; presumably the server // could send up to 0xFF entries, but those after the 8th would not be // byteswapped before the client handles them. @@ -4423,7 +5418,8 @@ struct G_Unknown_GC_Ep3_6xB4x4A : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x4B: Unknown -struct G_Unknown_GC_Ep3_6xB4x4B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4B { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // If any of the entries [0][1] through [0][10] or [1][1] through [1][10] are // < -99 or > 99, the entire command is ignored. parray, 2> unknown_a1; @@ -4431,7 +5427,8 @@ struct G_Unknown_GC_Ep3_6xB4x4B : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x4C: Unknown -struct G_Unknown_GC_Ep3_6xB4x4C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4C { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; // Must be < 4 int8_t unknown_a2; // Must be in [-1, 8] parray unknown_a3; @@ -4447,7 +5444,8 @@ struct G_Unknown_GC_Ep3_6xB4x4C : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x4D: Unknown -struct G_Unknown_GC_Ep3_6xB4x4D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4D { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; // Must be < 4 int8_t unknown_a2; // Must be in [-1, 8] parray unknown_a3; @@ -4462,7 +5460,8 @@ struct G_Unknown_GC_Ep3_6xB4x4D : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x4E: Unknown -struct G_Unknown_GC_Ep3_6xB4x4E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4E { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; // Must be < 4 int8_t unknown_a2; // Must be in [-1, 8] parray unknown_a3; @@ -4476,7 +5475,8 @@ struct G_Unknown_GC_Ep3_6xB4x4E : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x4F: Unknown -struct G_Unknown_GC_Ep3_6xB4x4F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x4F { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; uint8_t unknown_a1; uint8_t unused; le_uint16_t unknown_a2; // Bitmask; the 9 low-order bits are used @@ -4484,13 +5484,15 @@ struct G_Unknown_GC_Ep3_6xB4x4F : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x50: Unknown -struct G_Unknown_GC_Ep3_6xB4x50 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x50 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; parray unknown_a1; }; // 6xB4x51: Tournament match info -struct G_Unknown_GC_Ep3_6xB4x51 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x51 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; ptext match_description; struct Entry { ptext team_name; @@ -4509,7 +5511,8 @@ struct G_Unknown_GC_Ep3_6xB4x51 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x52: Unknown -struct G_Unknown_GC_Ep3_6xB4x52 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x52 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; le_uint16_t unknown_a1; le_uint16_t unknown_a2; le_uint16_t unknown_a3; @@ -4519,7 +5522,8 @@ struct G_Unknown_GC_Ep3_6xB4x52 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB // 6xB4x53: Unknown -struct G_Unknown_GC_Ep3_6xB4x53 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 { +struct G_Unknown_GC_Ep3_6xB4x53 { + G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5 header; // Note: It seems the client ignores everything in this structure. The command // just sets a global state flag, then returns immediately. @@ -4536,7 +5540,7 @@ struct G_Unknown_GC_Ep3_6xB4x53 : G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB le_uint16_t unknown_a6; parray unknown_a7; le_uint32_t map_number; - // Comamnd may be larger than this structure + // Command may be larger than this structure }; diff --git a/src/Compression.cc b/src/Compression.cc index 2ce559eb..80243481 100644 --- a/src/Compression.cc +++ b/src/Compression.cc @@ -156,9 +156,9 @@ static int16_t get_u8_or_eof(StringReader& r) { return r.eof() ? -1 : r.get_u8(); } -string prs_decompress(const string& data, size_t max_size) { +string prs_decompress(const void* data, size_t size, size_t max_output_size) { string output; - StringReader r(data.data(), data.size()); + StringReader r(data, size); int32_t r3, r5; int bitpos = 9; @@ -189,7 +189,7 @@ string prs_decompress(const string& data, size_t max_size) { return output; } output += static_cast(ch); - if (max_size && (output.size() > max_size)) { + if (max_output_size && (output.size() > max_output_size)) { throw runtime_error("maximum output size exceeded"); } continue; @@ -258,14 +258,18 @@ string prs_decompress(const string& data, size_t max_size) { t = r3; for (x = 0; x < t; x++) { output += output.at(output.size() + r5); - if (max_size && (output.size() > max_size)) { + if (max_output_size && (output.size() > max_output_size)) { throw runtime_error("maximum output size exceeded"); } } } } -size_t prs_decompress_size(const string& data, size_t max_size) { +string prs_decompress(const string& data, size_t max_output_size) { + return prs_decompress(data.data(), data.size(), max_output_size); +} + +size_t prs_decompress_size(const string& data, size_t max_output_size) { size_t output_size = 0; StringReader r(data.data(), data.size()); @@ -298,7 +302,7 @@ size_t prs_decompress_size(const string& data, size_t max_size) { return output_size; } output_size++; - if (max_size && (output_size > max_size)) { + if (max_output_size && (output_size > max_output_size)) { throw runtime_error("maximum output size exceeded"); } continue; @@ -363,7 +367,7 @@ size_t prs_decompress_size(const string& data, size_t max_size) { continue; } output_size += r3; - if (max_size && (output_size > max_size)) { + if (max_output_size && (output_size > max_output_size)) { throw runtime_error("maximum output size exceeded"); } } diff --git a/src/Compression.hh b/src/Compression.hh index 9fedfec0..200cf014 100644 --- a/src/Compression.hh +++ b/src/Compression.hh @@ -9,7 +9,8 @@ std::string prs_compress(const void* vdata, size_t size); std::string prs_compress(const std::string& data); -std::string prs_decompress(const std::string& data, size_t max_size = 0); -size_t prs_decompress_size(const std::string& data, size_t max_size = 0); +std::string prs_decompress(const void* data, size_t size, size_t max_output_size = 0); +std::string prs_decompress(const std::string& data, size_t max_output_size = 0); +size_t prs_decompress_size(const std::string& data, size_t max_output_size = 0); std::string bc0_decompress(const std::string& data); diff --git a/src/PSOProtocol.hh b/src/PSOProtocol.hh index 7be8064b..09535fdc 100644 --- a/src/PSOProtocol.hh +++ b/src/PSOProtocol.hh @@ -47,12 +47,6 @@ union PSOCommandHeader { PSOCommandHeader(); } __attribute__((packed)); -union PSOSubcommand { - uint8_t byte[4]; - le_uint16_t word[2]; - le_uint32_t dword; -} __attribute__((packed)); - // This function is used in a lot of places to check received command sizes and // cast them to the appropriate type template diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index af91c9ad..40efb0ef 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -836,13 +836,24 @@ static HandlerResult S_6x(shared_ptr, if (session.save_files) { if ((session.version == GameVersion::GC) && (data.size() >= 0x14)) { - PSOSubcommand* subs = &check_size_t(data, 0x14, 0xFFFF); - if (subs[0].dword == 0x000000B6 && subs[2].dword == 0x00000041) { - string filename = string_printf("map%08" PRIX32 ".%" PRIu64 ".mnmd", - subs[3].dword.load(), now()); - string map_data = prs_decompress(data.substr(0x14)); - save_file(filename, map_data); - session.log.warning("Wrote %zu bytes to %s", map_data.size(), filename.c_str()); + if (static_cast(data[0]) == 0xB6) { + const auto& header = check_size_t( + data, sizeof(G_MapSubsubcommand_GC_Ep3_6xB6), 0xFFFF); + if (header.subsubcommand == 0x00000041) { + const auto& cmd = check_size_t( + data, sizeof(G_MapData_GC_Ep3_6xB6x41), 0xFFFF); + string filename = string_printf("map%08" PRIX32 ".%" PRIu64 ".mnmd", + cmd.map_number.load(), now()); + string map_data = prs_decompress( + data.data() + sizeof(cmd), data.size() - sizeof(cmd)); + save_file(filename, map_data); + if (map_data.size() != sizeof(Ep3Map)) { + session.log.warning("Wrote %zu bytes to %s (expected %zu bytes; the file may be invalid)", + map_data.size(), filename.c_str(), sizeof(Ep3Map)); + } else { + session.log.info("Wrote %zu bytes to %s", map_data.size(), filename.c_str()); + } + } } } } @@ -850,7 +861,31 @@ static HandlerResult S_6x(shared_ptr, if (!data.empty() && session.next_drop_item.data.data1d[0] && (session.version != GameVersion::BB)) { - if (data[0] == 0x60) { + if (data[0] == 0x46) { + const auto& cmd = check_size_t(data, + offsetof(G_AttackFinished_6x46, entries), + sizeof(G_AttackFinished_6x46)); + if ((cmd.count > 11) || (cmd.count > cmd.header.size - 2)) { + session.log.warning("Blocking subcommand 6x46 with invalid count"); + return HandlerResult::Type::SUPPRESS; + } + } else if (data[0] == 0x47) { + const auto& cmd = check_size_t(data, + offsetof(G_CastTechnique_6x47, targets), + sizeof(G_CastTechnique_6x47)); + if ((cmd.target_count > 10) || (cmd.target_count > cmd.header.size - 2)) { + session.log.warning("Blocking subcommand 6x47 with invalid count"); + return HandlerResult::Type::SUPPRESS; + } + } else if (data[0] == 0x49) { + const auto& cmd = check_size_t(data, + offsetof(G_SubtractPBEnergy_6x49, entries), + sizeof(G_SubtractPBEnergy_6x49)); + if ((cmd.entry_count > 14) || (cmd.entry_count > cmd.header.size - 2)) { + session.log.warning("Blocking subcommand 6x49 with invalid count"); + return HandlerResult::Type::SUPPRESS; + } + } else if (data[0] == 0x60) { const auto& cmd = check_size_t(data, sizeof(G_EnemyDropItemRequest_DC_6x60), sizeof(G_EnemyDropItemRequest_PC_V3_BB_6x60)); @@ -1242,30 +1277,13 @@ static HandlerResult C_6x(shared_ptr s, if (!data.empty()) { if (data[0] == 0x2F || data[0] == 0x4B || data[0] == 0x4C) { if (session.infinite_hp) { - vector subs; - for (size_t amount = 1020; amount > 0;) { - auto& sub1 = subs.emplace_back(); - sub1.word[0] = 0x029A; - sub1.byte[2] = session.lobby_client_id; - sub1.byte[3] = 0x00; - auto& sub2 = subs.emplace_back(); - sub2.word[0] = 0x0000; - sub2.byte[2] = PlayerStatsChange::ADD_HP; - sub2.byte[3] = (amount > 0xFF) ? 0xFF : amount; - amount -= sub2.byte[3]; - } - session.client_channel.send(0x60, 0x00, subs.data(), subs.size() * sizeof(PSOSubcommand)); + send_player_stats_change(session.client_channel, + session.lobby_client_id, PlayerStatsChange::ADD_HP, 2550); } } else if (data[0] == 0x48) { if (session.infinite_tp) { - PSOSubcommand subs[2]; - subs[0].word[0] = 0x029A; - subs[0].byte[2] = session.lobby_client_id; - subs[0].byte[3] = 0x00; - subs[1].word[0] = 0x0000; - subs[1].byte[2] = PlayerStatsChange::ADD_TP; - subs[1].byte[3] = 0xFF; - session.client_channel.send(0x60, 0x00, &subs[0], sizeof(subs)); + send_player_stats_change(session.client_channel, + session.lobby_client_id, PlayerStatsChange::ADD_TP, 255); } } } @@ -1284,8 +1302,8 @@ HandlerResult C_6x(shared_ptr, if (!data.empty() && (data[0] == 0x05) && session.switch_assist) { auto& cmd = check_size_t(data); - if (cmd.enabled && cmd.switch_id != 0xFFFF) { - if (session.last_switch_enabled_command.subcommand == 0x05) { + if (cmd.flags && cmd.header.object_id != 0xFFFF) { + if (session.last_switch_enabled_command.header.subcommand == 0x05) { session.log.info("Switch assist: replaying previous enable command"); session.server_channel.send(0x60, 0x00, &session.last_switch_enabled_command, sizeof(session.last_switch_enabled_command)); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 73bef8d3..43a9886c 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -215,10 +215,16 @@ void ProxyServer::on_client_connect( auto cmd = prepare_server_init_contents_bb(server_key, client_key, 0); session->channel.send(0x03, 0x00, &cmd, sizeof(cmd)); session->detector_crypt.reset(new PSOBBMultiKeyDetectorEncryption( - this->state->bb_private_keys, bb_crypt_initial_client_commands, cmd.client_key.data(), sizeof(cmd.client_key))); + this->state->bb_private_keys, + bb_crypt_initial_client_commands, + cmd.basic_cmd.client_key.data(), + sizeof(cmd.basic_cmd.client_key))); session->channel.crypt_in = session->detector_crypt; session->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption( - session->detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true)); + session->detector_crypt, + cmd.basic_cmd.server_key.data(), + sizeof(cmd.basic_cmd.server_key), + true)); break; } default: @@ -491,7 +497,7 @@ ProxyServer::LinkedSession::LinkedSession( lobby_client_id(0), leader_client_id(0), is_in_game(false) { - this->last_switch_enabled_command.subcommand = 0; + this->last_switch_enabled_command.header.subcommand = 0; memset(this->prev_server_command_bytes, 0, sizeof(this->prev_server_command_bytes)); } diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 3715e18f..3adc5aa8 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -815,28 +815,35 @@ static void on_ep3_counter_state(shared_ptr, shared_ptr c, static void on_ep3_server_data_request(shared_ptr s, shared_ptr c, uint16_t, uint32_t, const string& data) { // CA - check_size_v(data.size(), 8, 0xFFFF); - const PSOSubcommand* cmds = reinterpret_cast(data.data()); - auto l = s->find_lobby(c->lobby_id); if (!l || !(l->flags & Lobby::Flag::EPISODE_3_ONLY) || !l->is_game()) { - c->should_disconnect = true; - return; + throw runtime_error("Episode 3 server data request sent in lobby or non-Episode 3 game"); } - if (cmds[0].byte[0] != 0xB3) { - c->should_disconnect = true; - return; + const auto& header = check_size_t( + data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF); + + // TODO: We can support this since set_mask_for_ep3_game_command already + // exists; I just don't want to make a copy of the data string + if (header.mask_key != 0) { + throw runtime_error("Episode 3 server data request has nonzero mask key"); } - switch (cmds[1].byte[0]) { + if (header.basic_header.subcommand != 0xB3) { + throw runtime_error("unknown Episode 3 server data request"); + } + + switch (header.subsubcommand) { // Phase 1: map select case 0x40: + check_size_t(data); send_ep3_map_list(s, l); break; - case 0x41: - send_ep3_map_data(s, l, cmds[4].dword); + case 0x41: { + const auto& cmd = check_size_t(data); + send_ep3_map_data(s, l, cmd.map_number); break; + } /* What follows is some raw code that has survived since the days of khyller * (approx. 2004). Much more research and engineering is needed to get @@ -902,7 +909,8 @@ static void on_ep3_server_data_request(shared_ptr s, shared_ptrlog.error("Unknown Episode III server data request: %02X", cmds[1].byte[0]); + c->log.error("Unknown Episode III server data request: %02X", + header.subsubcommand); } } @@ -1161,13 +1169,13 @@ static void on_menu_selection(shared_ptr s, shared_ptr c, if (uses_unicode) { const auto& cmd = check_size_t(data); password = cmd.password; - menu_id = cmd.menu_id; - item_id = cmd.item_id; + menu_id = cmd.basic_cmd.menu_id; + item_id = cmd.basic_cmd.item_id; } else { const auto& cmd = check_size_t(data); password = decode_sjis(cmd.password); - menu_id = cmd.menu_id; - item_id = cmd.item_id; + menu_id = cmd.basic_cmd.menu_id; + item_id = cmd.basic_cmd.item_id; } } else { const auto& cmd = check_size_t(data); diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index b851e5d0..aecac292 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -29,8 +29,8 @@ bool command_is_private(uint8_t command) { -template -const CmdT* check_size_sc( +template +const CmdT& check_size_sc( const string& data, size_t min_size = sizeof(CmdT), size_t max_size = sizeof(CmdT), @@ -38,21 +38,22 @@ const CmdT* check_size_sc( if (max_size < min_size) { max_size = min_size; } - const auto* cmd = &check_size_t(data, min_size, max_size); + const auto& cmd = check_size_t(data, min_size, max_size); if (check_size_field) { - const PSOSubcommand* sub = reinterpret_cast(data.data()); - if (sub[0].byte[1] == 0) { + if (data.size() < 4) { + throw runtime_error("subcommand is too short for header"); + } + const auto* header = reinterpret_cast(data.data()); + if (header->size == 0) { if (data.size() < 8) { throw runtime_error("subcommand has extended size but is shorter than 8 bytes"); } - if (sub[1].dword != data.size()) { + const auto* ext_header = reinterpret_cast*>(data.data()); + if (ext_header->size != data.size()) { throw runtime_error("invalid subcommand extended size field"); } } else { - if (data.size() < 4) { - throw runtime_error("subcommand is shorter than 4 bytes"); - } - if ((sub[0].byte[1] * 4) != data.size()) { + if ((header->size * 4) != data.size()) { throw runtime_error("invalid subcommand size field"); } } @@ -106,13 +107,109 @@ static void forward_subcommand(shared_ptr l, shared_ptr c, +static void on_subcommand_invalid(shared_ptr, + shared_ptr, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc( + data, sizeof(G_UnusedHeader), 0xFFFF); + if (command_is_private(command)) { + c->log.error("Invalid subcommand: %02hhX (private to %hhu)", + cmd.subcommand, flag); + } else { + c->log.error("Invalid subcommand: %02hhX (public)", cmd.subcommand); + } +} + +static void on_subcommand_unimplemented(shared_ptr, + shared_ptr, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc( + data, sizeof(G_UnusedHeader), 0xFFFF); + if (command_is_private(command)) { + c->log.warning("Unknown subcommand: %02hhX (private to %hhu)", + cmd.subcommand, flag); + } else { + c->log.warning("Unknown subcommand: %02hhX (public)", cmd.subcommand); + } +} + + + +static void on_subcommand_forward_check_size(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + check_size_sc(data, sizeof(G_UnusedHeader), 0xFFFF); + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_game(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + if (!l->is_game()) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_game_loading(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + if (!l->is_game() || !l->any_client_loading()) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_size_client(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc( + data, sizeof(G_ClientIDHeader), 0xFFFF); + if (cmd.client_id != c->lobby_client_id) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_size_game(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + check_size_sc(data, sizeof(G_UnusedHeader), 0xFFFF); + if (!l->is_game()) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_size_ep3_lobby(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + check_size_sc(data, sizeof(G_UnusedHeader), 0xFFFF); + if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + +static void on_subcommand_forward_check_size_ep3_game(shared_ptr, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + check_size_sc(data, sizeof(G_UnusedHeader), 0xFFFF); + if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { + return; + } + forward_subcommand(l, c, command, flag, data); +} + + + //////////////////////////////////////////////////////////////////////////////// // Ep3 subcommands static void on_subcommand_ep3_battle_subs(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& orig_data) { - check_size_sc( + check_size_sc( orig_data, sizeof(G_CardBattleCommandHeader_GC_Ep3_6xB3_6xB4_6xB5), 0xFFFF); if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { return; @@ -144,19 +241,19 @@ static void on_subcommand_send_guild_card(shared_ptr, switch (c->version()) { case GameVersion::DC: { - const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_description = cmd->description; + const auto& cmd = check_size_sc(data); + c->game_data.player()->guild_card_description = cmd.description; break; } case GameVersion::PC: { - const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_description = cmd->description; + const auto& cmd = check_size_sc(data); + c->game_data.player()->guild_card_description = cmd.description; break; } case GameVersion::GC: case GameVersion::XB: { - const auto* cmd = check_size_sc(data); - c->game_data.player()->guild_card_description = cmd->description; + const auto& cmd = check_size_sc(data); + c->game_data.player()->guild_card_description = cmd.description; break; } case GameVersion::BB: @@ -174,9 +271,9 @@ static void on_subcommand_send_guild_card(shared_ptr, static void on_subcommand_symbol_chat(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, 0x08, 0xFFFF); + const auto& cmd = check_size_sc(data); - if (!c->can_chat || (p[1].byte[0] != c->lobby_client_id)) { + if (!c->can_chat || (cmd.client_id != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); @@ -186,23 +283,12 @@ static void on_subcommand_symbol_chat(shared_ptr, static void on_subcommand_word_select(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, 0x20, 0xFFFF); + const auto& cmd = check_size_sc(data); - if (!c->can_chat || (p[0].byte[2] != c->lobby_client_id)) { + if (!c->can_chat || (cmd.header.client_id != c->lobby_client_id)) { return; } - // TODO: bring this back if it turns out to be important; I suspect it's not - //p->byte[2] = p->byte[3] = p->byte[(c->version() == GameVersion::BB) ? 2 : 3]; - - for (size_t x = 1; x < 8; x++) { - if ((p[x].word[0] > 0x1863) && (p[x].word[0] != 0xFFFF)) { - return; - } - if ((p[x].word[1] > 0x1863) && (p[x].word[1] != 0xFFFF)) { - return; - } - } forward_subcommand(l, c, command, flag, data); } @@ -210,9 +296,9 @@ static void on_subcommand_word_select(shared_ptr, static void on_subcommand_set_player_visibility(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto& p = check_size_sc(data, 4); + const auto& cmd = check_size_sc(data); - if (p[0].byte[2] != c->lobby_client_id) { + if (cmd.header.client_id != c->lobby_client_id) { return; } @@ -226,15 +312,14 @@ static void on_subcommand_set_player_visibility(shared_ptr, //////////////////////////////////////////////////////////////////////////////// // Game commands used by cheat mechanisms -// need to process changing areas since we keep track of where players are static void on_subcommand_change_area(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, 0x08, 0xFFFF); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } - c->area = p[1].dword; + c->area = cmd.area; forward_subcommand(l, c, command, flag, data); } @@ -242,22 +327,22 @@ static void on_subcommand_change_area(shared_ptr, static void on_subcommand_hit_by_enemy(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, 0x04, 0xFFFF); - if (!l->is_game() || (p->byte[2] != c->lobby_client_id)) { + const auto& cmd = check_size_sc(data, sizeof(G_ClientIDHeader), 0xFFFF); + if (!l->is_game() || (cmd.client_id != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->infinite_hp) { - send_player_stats_change(l, c, PlayerStatsChange::ADD_HP, 1020); + send_player_stats_change(l, c, PlayerStatsChange::ADD_HP, 2550); } } // when a player casts a tech, restore TP if infinite TP is enabled -static void on_subcommand_use_technique(shared_ptr, +static void on_subcommand_cast_technique_finished(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, 0x04, 0xFFFF); - if (!l->is_game() || (p->byte[2] != c->lobby_client_id)) { + const auto& cmd = check_size_sc(data); + if (!l->is_game() || (cmd.header.client_id != c->lobby_client_id)) { return; } forward_subcommand(l, c, command, flag, data); @@ -266,6 +351,48 @@ static void on_subcommand_use_technique(shared_ptr, } } +static void on_subcommand_attack_finished(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc(data, + offsetof(G_AttackFinished_6x46, entries), sizeof(G_AttackFinished_6x46)); + // The PSO GC client doesn't check bounds correctly when handling this + // command, which could cause it to byteswap parts of the stack, including the + // next frame pointer and return address. This of course leads to a crash. + if ((cmd.count > 11) || (cmd.count > cmd.header.size - 2)) { + throw runtime_error("invalid attack finished command"); + } + on_subcommand_forward_check_size_client(s, l, c, command, flag, data); +} + +static void on_subcommand_cast_technique(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc(data, + offsetof(G_CastTechnique_6x47, targets), sizeof(G_CastTechnique_6x47)); + // The PSO GC client doesn't check bounds correctly when handling this + // command, which could cause it to byteswap parts of the stack, including the + // next frame pointer and return address. This of course leads to a crash. + if ((cmd.target_count > 10) || (cmd.target_count > cmd.header.size - 2)) { + throw runtime_error("invalid cast technique command"); + } + on_subcommand_forward_check_size_client(s, l, c, command, flag, data); +} + +static void on_subcommand_subtract_pb_energy(shared_ptr s, + shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, + const string& data) { + const auto& cmd = check_size_sc(data, + offsetof(G_SubtractPBEnergy_6x49, entries), sizeof(G_SubtractPBEnergy_6x49)); + // The PSO GC client doesn't check bounds correctly when handling this + // command, which could cause it to byteswap parts of the stack, including the + // next frame pointer and return address. This of course leads to a crash. + if ((cmd.entry_count > 14) || (cmd.entry_count > cmd.header.size - 3)) { + throw runtime_error("invalid subtract PB energy command"); + } + on_subcommand_forward_check_size_client(s, l, c, command, flag, data); +} + static void on_subcommand_switch_state_changed(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { @@ -274,9 +401,9 @@ static void on_subcommand_switch_state_changed(shared_ptr, return; } forward_subcommand(l, c, command, flag, data); - if (cmd.enabled && cmd.switch_id != 0xFFFF) { + if (cmd.flags && cmd.header.object_id != 0xFFFF) { if ((l->flags & Lobby::Flag::CHEATS_ENABLED) && c->switch_assist && - (c->last_switch_enabled_command.subcommand == 0x05)) { + (c->last_switch_enabled_command.header.subcommand == 0x05)) { c->log.info("[Switch assist] Replaying previous enable command"); forward_subcommand(l, c, command, flag, &c->last_switch_enabled_command, sizeof(c->last_switch_enabled_command)); @@ -292,14 +419,14 @@ template void on_subcommand_movement(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if (cmd->client_id != c->lobby_client_id) { + if (cmd.header.client_id != c->lobby_client_id) { return; } - c->x = cmd->x; - c->z = cmd->z; + c->x = cmd.x; + c->z = cmd.z; forward_subcommand(l, c, command, flag, data); } @@ -310,18 +437,19 @@ void on_subcommand_movement(shared_ptr, static void on_subcommand_player_drop_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if ((cmd->client_id != c->lobby_client_id)) { + if ((cmd.header.client_id != c->lobby_client_id)) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - l->add_item(c->game_data.player()->remove_item(cmd->item_id, 0), - cmd->area, cmd->x, cmd->z); + l->add_item(c->game_data.player()->remove_item(cmd.item_id, 0), cmd.area, + cmd.x, cmd.z); l->log.info("Player %hu dropped item %08" PRIX32 " at %hu:(%g, %g)", - cmd->client_id.load(), cmd->item_id.load(), cmd->area.load(), cmd->x.load(), cmd->z.load()); + cmd.header.client_id.load(), cmd.item_id.load(), cmd.area.load(), + cmd.x.load(), cmd.z.load()); c->game_data.player()->print_inventory(stderr); } @@ -331,10 +459,10 @@ static void on_subcommand_player_drop_item(shared_ptr, static void on_subcommand_create_inventory_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data, - sizeof(G_PlayerCreateInventoryItem_DC_6x2B), sizeof(G_PlayerCreateInventoryItem_PC_V3_BB_6x2B)); + const auto& cmd = check_size_sc(data, + sizeof(G_CreateInventoryItem_DC_6x2B), sizeof(G_CreateInventoryItem_PC_V3_BB_6x2B)); - if ((cmd->client_id != c->lobby_client_id)) { + if ((cmd.header.client_id != c->lobby_client_id)) { return; } if (c->version() == GameVersion::BB) { @@ -347,11 +475,11 @@ static void on_subcommand_create_inventory_item(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd->item; + item.data = cmd.item; c->game_data.player()->add_item(item); l->log.info("Player %hu created inventory item %08" PRIX32, - cmd->client_id.load(), cmd->item.id.load()); + cmd.header.client_id.load(), cmd.item.id.load()); c->game_data.player()->print_inventory(stderr); } @@ -361,7 +489,7 @@ static void on_subcommand_create_inventory_item(shared_ptr, static void on_subcommand_drop_partial_stack(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data, + const auto& cmd = check_size_sc(data, sizeof(G_DropStackedItem_DC_6x5D), sizeof(G_DropStackedItem_PC_V3_BB_6x5D)); // TODO: Should we check the client ID here too? @@ -378,11 +506,11 @@ static void on_subcommand_drop_partial_stack(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd->data; - l->add_item(item, cmd->area, cmd->x, cmd->z); + item.data = cmd.data; + l->add_item(item, cmd.area, cmd.x, cmd.z); l->log.info("Player %hu split stack to create ground item %08" PRIX32 " at %hu:(%g, %g)", - cmd->client_id.load(), item.data.id.load(), cmd->area.load(), cmd->x.load(), cmd->z.load()); + cmd.header.client_id.load(), item.data.id.load(), cmd.area.load(), cmd.x.load(), cmd.z.load()); c->game_data.player()->print_inventory(stderr); } @@ -393,9 +521,9 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if (!l->is_game() || (cmd->client_id != c->lobby_client_id)) { + if (!l->is_game() || (cmd.header.client_id != c->lobby_client_id)) { return; } @@ -403,7 +531,7 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr, throw logic_error("item tracking not enabled in BB game"); } - auto item = c->game_data.player()->remove_item(cmd->item_id, cmd->amount); + auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.amount); // if a stack was split, the original item still exists, so the dropped item // needs a new ID. remove_item signals this by returning an item with id=-1 @@ -416,14 +544,14 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr, // removed again by the 6x29 handler) c->game_data.player()->add_item(item); - l->add_item(item, cmd->area, cmd->x, cmd->z); + l->add_item(item, cmd.area, cmd.x, cmd.z); l->log.info("Player %hu split stack %08" PRIX32 " (%" PRIu32 " of them) at %hu:(%g, %g)", - cmd->client_id.load(), cmd->item_id.load(), cmd->amount.load(), - cmd->area.load(), cmd->x.load(), cmd->z.load()); + cmd.header.client_id.load(), cmd.item_id.load(), cmd.amount.load(), + cmd.area.load(), cmd.x.load(), cmd.z.load()); c->game_data.player()->print_inventory(stderr); - send_drop_stacked_item(l, item.data, cmd->area, cmd->x, cmd->z); + send_drop_stacked_item(l, item.data, cmd.area, cmd.x, cmd.z); } else { forward_subcommand(l, c, command, flag, data); @@ -433,9 +561,9 @@ static void on_subcommand_drop_partial_stack_bb(shared_ptr, static void on_subcommand_buy_shop_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if (!l->is_game() || (cmd->client_id != c->lobby_client_id)) { + if (!l->is_game() || (cmd.header.client_id != c->lobby_client_id)) { return; } if (l->version == GameVersion::BB) { @@ -446,11 +574,11 @@ static void on_subcommand_buy_shop_item(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd->item; + item.data = cmd.item; c->game_data.player()->add_item(item); l->log.info("Player %hu bought item %08" PRIX32 " from shop", - cmd->client_id.load(), item.data.id.load()); + cmd.header.client_id.load(), item.data.id.load()); c->game_data.player()->print_inventory(stderr); } @@ -460,7 +588,7 @@ static void on_subcommand_buy_shop_item(shared_ptr, static void on_subcommand_box_or_enemy_item_drop(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data, + const auto& cmd = check_size_sc(data, sizeof(G_DropItem_DC_6x5F), sizeof(G_DropItem_PC_V3_BB_6x5F)); if (!l->is_game() || (c->lobby_client_id != l->leader_id)) { @@ -473,11 +601,11 @@ static void on_subcommand_box_or_enemy_item_drop(shared_ptr, PlayerInventoryItem item; item.present = 1; item.flags = 0; - item.data = cmd->data; - l->add_item(item, cmd->area, cmd->x, cmd->z); + item.data = cmd.data; + l->add_item(item, cmd.area, cmd.x, cmd.z); l->log.info("Leader created ground item %08" PRIX32 " at %hhu:(%g, %g)", - item.data.id.load(), cmd->area, cmd->x.load(), cmd->z.load()); + item.data.id.load(), cmd.area, cmd.x.load(), cmd.z.load()); forward_subcommand(l, c, command, flag, data); } @@ -486,7 +614,7 @@ static void on_subcommand_box_or_enemy_item_drop(shared_ptr, static void on_subcommand_pick_up_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - auto* cmd = check_size_sc(data); + auto& cmd = check_size_sc(data); if (!l->is_game()) { return; @@ -496,15 +624,15 @@ static void on_subcommand_pick_up_item(shared_ptr, return; } - auto effective_c = l->clients.at(cmd->client_id); + auto effective_c = l->clients.at(cmd.header.client_id); if (!effective_c.get()) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - effective_c->game_data.player()->add_item(l->remove_item(cmd->item_id)); + effective_c->game_data.player()->add_item(l->remove_item(cmd.item_id)); l->log.info("Player %hu picked up %08" PRIX32, - cmd->client_id.load(), cmd->item_id.load()); + cmd.header.client_id.load(), cmd.item_id.load()); effective_c->game_data.player()->print_inventory(stderr); } @@ -516,9 +644,9 @@ static void on_subcommand_pick_up_item_request(shared_ptr, const string& data) { // This is handled by the server on BB, and by the leader on other versions if (l->version == GameVersion::BB) { - auto* cmd = check_size_sc(data); + auto& cmd = check_size_sc(data); - if (!l->is_game() || (cmd->client_id != c->lobby_client_id)) { + if (!l->is_game() || (cmd.header.client_id != c->lobby_client_id)) { return; } @@ -526,13 +654,13 @@ static void on_subcommand_pick_up_item_request(shared_ptr, throw logic_error("item tracking not enabled in BB game"); } - c->game_data.player()->add_item(l->remove_item(cmd->item_id)); + c->game_data.player()->add_item(l->remove_item(cmd.item_id)); l->log.info("Player %hu picked up %08" PRIX32, - cmd->client_id.load(), cmd->item_id.load()); + cmd.header.client_id.load(), cmd.item_id.load()); c->game_data.player()->print_inventory(stderr); - send_pick_up_item(l, c, cmd->item_id, cmd->area); + send_pick_up_item(l, c, cmd.item_id, cmd.area); } else { forward_subcommand(l, c, command, flag, data); @@ -542,17 +670,17 @@ static void on_subcommand_pick_up_item_request(shared_ptr, static void on_subcommand_equip_unequip_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if (cmd->client_id != c->lobby_client_id) { + if (cmd.header.client_id != c->lobby_client_id) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - size_t index = c->game_data.player()->inventory.find_item(cmd->item_id); - if (cmd->command == 0x25) { // equip + size_t index = c->game_data.player()->inventory.find_item(cmd.item_id); + if (cmd.header.subcommand == 0x25) { // Equip c->game_data.player()->inventory.items[index].flags |= 0x00000008; - } else { // unequip + } else { // Unequip c->game_data.player()->inventory.items[index].flags &= 0xFFFFFFF7; } } else if (l->version == GameVersion::BB) { @@ -567,18 +695,18 @@ static void on_subcommand_equip_unequip_item(shared_ptr, static void on_subcommand_use_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); - if (cmd->client_id != c->lobby_client_id) { + if (cmd.header.client_id != c->lobby_client_id) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - size_t index = c->game_data.player()->inventory.find_item(cmd->item_id); + size_t index = c->game_data.player()->inventory.find_item(cmd.item_id); player_use_item(c, index); l->log.info("Player used item %hu:%08" PRIX32, - cmd->client_id.load(), cmd->item_id.load()); + cmd.header.client_id.load(), cmd.item_id.load()); c->game_data.player()->print_inventory(stderr); } @@ -595,19 +723,17 @@ static void on_subcommand_open_shop_bb_or_ep3_battle_subs(shared_ptr(data, 0x08); if ((l->version == GameVersion::BB) && l->is_game()) { size_t num_items = 9 + (rand() % 4); c->game_data.shop_contents.clear(); while (c->game_data.shop_contents.size() < num_items) { ItemData item_data; - if (shop_type == 0) { // tool shop + if (cmd.shop_type == 0) { // tool shop item_data = l->common_item_creator->create_shop_item(l->difficulty, 3); - } else if (shop_type == 1) { // weapon shop + } else if (cmd.shop_type == 1) { // weapon shop item_data = l->common_item_creator->create_shop_item(l->difficulty, 0); - } else if (shop_type == 2) { // guards shop + } else if (cmd.shop_type == 2) { // guards shop item_data = l->common_item_creator->create_shop_item(l->difficulty, 1); } else { // unknown shop... just leave it blank I guess break; @@ -617,7 +743,7 @@ static void on_subcommand_open_shop_bb_or_ep3_battle_subs(shared_ptrgame_data.shop_contents.emplace_back(item_data); } - send_shop(c, shop_type); + send_shop(c, cmd.shop_type); } } } @@ -634,7 +760,7 @@ static void on_subcommand_open_bank_bb_or_card_trade_counter_ep3(shared_ptr, shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; @@ -644,33 +770,33 @@ static void on_subcommand_bank_action_bb(shared_ptr, throw logic_error("item tracking not enabled in BB game"); } - if (cmd->action == 0) { // deposit - if (cmd->item_id == 0xFFFFFFFF) { // meseta - if (cmd->meseta_amount > c->game_data.player()->disp.meseta) { + if (cmd.action == 0) { // deposit + if (cmd.item_id == 0xFFFFFFFF) { // meseta + if (cmd.meseta_amount > c->game_data.player()->disp.meseta) { return; } - if ((c->game_data.player()->bank.meseta + cmd->meseta_amount) > 999999) { + if ((c->game_data.player()->bank.meseta + cmd.meseta_amount) > 999999) { return; } - c->game_data.player()->bank.meseta += cmd->meseta_amount; - c->game_data.player()->disp.meseta -= cmd->meseta_amount; + c->game_data.player()->bank.meseta += cmd.meseta_amount; + c->game_data.player()->disp.meseta -= cmd.meseta_amount; } else { // item - auto item = c->game_data.player()->remove_item(cmd->item_id, cmd->item_amount); + auto item = c->game_data.player()->remove_item(cmd.item_id, cmd.item_amount); c->game_data.player()->bank.add_item(item); - send_destroy_item(l, c, cmd->item_id, cmd->item_amount); + send_destroy_item(l, c, cmd.item_id, cmd.item_amount); } - } else if (cmd->action == 1) { // take - if (cmd->item_id == 0xFFFFFFFF) { // meseta - if (cmd->meseta_amount > c->game_data.player()->bank.meseta) { + } else if (cmd.action == 1) { // take + if (cmd.item_id == 0xFFFFFFFF) { // meseta + if (cmd.meseta_amount > c->game_data.player()->bank.meseta) { return; } - if ((c->game_data.player()->disp.meseta + cmd->meseta_amount) > 999999) { + if ((c->game_data.player()->disp.meseta + cmd.meseta_amount) > 999999) { return; } - c->game_data.player()->bank.meseta -= cmd->meseta_amount; - c->game_data.player()->disp.meseta += cmd->meseta_amount; + c->game_data.player()->bank.meseta -= cmd.meseta_amount; + c->game_data.player()->disp.meseta += cmd.meseta_amount; } else { // item - auto bank_item = c->game_data.player()->bank.remove_item(cmd->item_id, cmd->item_amount); + auto bank_item = c->game_data.player()->bank.remove_item(cmd.item_id, cmd.item_amount); PlayerInventoryItem item = bank_item; item.data.id = l->generate_item_id(0xFF); c->game_data.player()->add_item(item); @@ -684,7 +810,7 @@ static void on_subcommand_sort_inventory_bb(shared_ptr, shared_ptr l, shared_ptr c, uint8_t, uint8_t, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); @@ -693,10 +819,10 @@ static void on_subcommand_sort_inventory_bb(shared_ptr, PlayerInventory sorted; for (size_t x = 0; x < 30; x++) { - if (cmd->item_ids[x] == 0xFFFFFFFF) { + if (cmd.item_ids[x] == 0xFFFFFFFF) { sorted.items[x].data.id = 0xFFFFFFFF; } else { - size_t index = c->game_data.player()->inventory.find_item(cmd->item_ids[x]); + size_t index = c->game_data.player()->inventory.find_item(cmd.item_ids[x]); sorted.items[x] = c->game_data.player()->inventory.items[index]; } } @@ -796,10 +922,10 @@ static void on_subcommand_enemy_drop_item_request(shared_ptr s, return; } - const auto* cmd = check_size_sc(data, + const auto& cmd = check_size_sc(data, sizeof(G_EnemyDropItemRequest_DC_6x60), sizeof(G_EnemyDropItemRequest_PC_V3_BB_6x60)); - if (!drop_item(s, l, cmd->enemy_id, cmd->area, cmd->x, cmd->z, cmd->request_id)) { + if (!drop_item(s, l, cmd.enemy_id, cmd.area, cmd.x, cmd.z, cmd.request_id)) { forward_subcommand(l, c, command, flag, data); } } @@ -811,8 +937,8 @@ static void on_subcommand_box_drop_item_request(shared_ptr s, return; } - const auto* cmd = check_size_sc(data); - if (!drop_item(s, l, -1, cmd->area, cmd->x, cmd->z, cmd->request_id)) { + const auto& cmd = check_size_sc(data); + if (!drop_item(s, l, -1, cmd.area, cmd.x, cmd.z, cmd.request_id)) { forward_subcommand(l, c, command, flag, data); } } @@ -820,23 +946,28 @@ static void on_subcommand_box_drop_item_request(shared_ptr s, static void on_subcommand_phase_setup(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); + if (c->version() == GameVersion::DC || c->version() == GameVersion::PC) { + forward_subcommand(l, c, command, flag, data); + return; + } + + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } forward_subcommand(l, c, command, flag, data); bool should_send_boss_drop_req = false; - if (p[2].dword == l->difficulty) { + if (cmd.difficulty == l->difficulty) { if ((l->episode == 1) && (c->area == 0x0E)) { // On Normal, Dark Falz does not have a third phase, so send the drop // request after the end of the second phase. On all other difficulty // levels, send it after the third phase. - if (((l->difficulty == 0) && (p[1].dword == 0x00000035)) || - ((l->difficulty != 0) && (p[1].dword == 0x00000037))) { + if (((l->difficulty == 0) && (cmd.basic_cmd.phase == 0x00000035)) || + ((l->difficulty != 0) && (cmd.basic_cmd.phase == 0x00000037))) { should_send_boss_drop_req = true; } - } else if ((l->episode == 2) && (p[1].dword == 0x00000057) && (c->area == 0x0D)) { + } else if ((l->episode == 2) && (cmd.basic_cmd.phase == 0x00000057) && (c->area == 0x0D)) { should_send_boss_drop_req = true; } } @@ -846,15 +977,18 @@ static void on_subcommand_phase_setup(shared_ptr, if (c) { G_EnemyDropItemRequest_PC_V3_BB_6x60 req = { { - 0x60, - 0x06, - 0x1090, + { + 0x60, + 0x06, + 0x0000, + }, static_cast(c->area), static_cast((l->episode == 2) ? 0x4E : 0x2F), 0x0B4F, (l->episode == 2) ? -9999.0f : 10160.58984375f, 0.0f, - 0x00000002, + 2, + 0, }, 0xE0AEDC01, }; @@ -868,20 +1002,20 @@ static void on_subcommand_enemy_hit(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } - if (cmd->enemy_id >= l->enemies.size()) { + if (cmd.header.enemy_id >= l->enemies.size()) { return; } - if (l->enemies[cmd->enemy_id].hit_flags & 0x80) { + if (l->enemies[cmd.header.enemy_id].hit_flags & 0x80) { return; } - l->enemies[cmd->enemy_id].hit_flags |= (1 << c->lobby_client_id); - l->enemies[cmd->enemy_id].last_hit = c->lobby_client_id; + l->enemies[cmd.header.enemy_id].hit_flags |= (1 << c->lobby_client_id); + l->enemies[cmd.header.enemy_id].last_hit = c->lobby_client_id; } forward_subcommand(l, c, command, flag, data); @@ -893,26 +1027,26 @@ static void on_subcommand_enemy_killed(shared_ptr s, forward_subcommand(l, c, command, flag, data); if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { throw runtime_error("client should not kill enemies outside of games"); } - if (cmd->enemy_id >= l->enemies.size()) { + if (cmd.header.enemy_id >= l->enemies.size()) { send_text_message(c, u"$C6Missing enemy killed"); return; } - string e_str = l->enemies[cmd->enemy_id].str(); - c->log.info("Enemy killed: entry %hu => %s", cmd->enemy_id.load(), e_str.c_str()); - if (l->enemies[cmd->enemy_id].hit_flags & 0x80) { + string e_str = l->enemies[cmd.header.enemy_id].str(); + c->log.info("Enemy killed: entry %hu => %s", cmd.header.enemy_id.load(), e_str.c_str()); + if (l->enemies[cmd.header.enemy_id].hit_flags & 0x80) { return; // Enemy is already dead } - if (l->enemies[cmd->enemy_id].experience == 0xFFFFFFFF) { + if (l->enemies[cmd.header.enemy_id].experience == 0xFFFFFFFF) { send_text_message(c, u"$C6Unknown enemy type killed"); return; } - auto& enemy = l->enemies[cmd->enemy_id]; + auto& enemy = l->enemies[cmd.header.enemy_id]; enemy.hit_flags |= 0x80; for (size_t x = 0; x < l->max_clients; x++) { if (!((enemy.hit_flags >> x) & 1)) { @@ -960,17 +1094,17 @@ static void on_subcommand_enemy_killed(shared_ptr s, static void on_subcommand_destroy_inventory_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } - if (cmd->client_id != c->lobby_client_id) { + if (cmd.header.client_id != c->lobby_client_id) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - c->game_data.player()->remove_item(cmd->item_id, cmd->amount); + c->game_data.player()->remove_item(cmd.item_id, cmd.amount); l->log.info("Inventory item %hu:%08" PRIX32 " destroyed (%" PRIX32 " of them)", - cmd->client_id.load(), cmd->item_id.load(), cmd->amount.load()); + cmd.header.client_id.load(), cmd.item_id.load(), cmd.amount.load()); c->game_data.player()->print_inventory(stderr); forward_subcommand(l, c, command, flag, data); } @@ -979,14 +1113,13 @@ static void on_subcommand_destroy_inventory_item(shared_ptr, static void on_subcommand_destroy_ground_item(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } if (l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED) { - l->remove_item(cmd->item_id); - l->log.info("Ground item %08" PRIX32 " destroyed (%" PRIX32 " of them)", - cmd->item_id.load(), cmd->amount.load()); + l->remove_item(cmd.item_id); + l->log.info("Ground item %08" PRIX32 " destroyed", cmd.item_id.load()); forward_subcommand(l, c, command, flag, data); } } @@ -995,7 +1128,7 @@ static void on_subcommand_identify_item_bb(shared_ptr, shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!l->is_game()) { return; } @@ -1003,7 +1136,7 @@ static void on_subcommand_identify_item_bb(shared_ptr, throw logic_error("item tracking not enabled in BB game"); } - size_t x = c->game_data.player()->inventory.find_item(cmd->item_id); + size_t x = c->game_data.player()->inventory.find_item(cmd.item_id); if (c->game_data.player()->inventory.items[x].data.data1[0] != 0) { return; // only weapons can be identified } @@ -1014,9 +1147,9 @@ static void on_subcommand_identify_item_bb(shared_ptr, // TODO: move this into a SendCommands.cc function G_IdentifyResult_BB_6xB9 res; - res.subcommand = 0xB9; - res.size = sizeof(res) / 4; - res.client_id = c->lobby_client_id; + res.header.subcommand = 0xB9; + res.header.size = sizeof(res) / 4; + res.header.client_id = c->lobby_client_id; res.item = c->game_data.identify_result.data; send_command_t(l, 0x60, 0x00, res); @@ -1030,7 +1163,7 @@ static void on_subcommand_accept_identify_item_bb(shared_ptr, const string& data) { if (l->version == GameVersion::BB) { - const auto* cmd = check_size_sc(data); + const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); @@ -1039,7 +1172,7 @@ static void on_subcommand_accept_identify_item_bb(shared_ptr, if (!c->game_data.identify_result.data.id) { throw runtime_error("no identify result present"); } - if (c->game_data.identify_result.data.id != cmd->item_id) { + if (c->game_data.identify_result.data.id != cmd.item_id) { throw runtime_error("accepted item ID does not match previous identify request"); } c->game_data.player()->add_item(c->game_data.identify_result); @@ -1055,7 +1188,7 @@ static void on_subcommand_sell_item_at_shop_bb(shared_ptr, shared_ptr l, shared_ptr, uint8_t, uint8_t, const string&) { if (l->version == GameVersion::BB) { - // const auto* cmd = check_size_sc(data); + // const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); @@ -1072,7 +1205,7 @@ static void on_subcommand_buy_shop_item_bb(shared_ptr, shared_ptr l, shared_ptr, uint8_t, uint8_t, const string&) { if (l->version == GameVersion::BB) { - // const auto* cmd = check_size_sc(data); + // const auto& cmd = check_size_sc(data); if (!(l->flags & Lobby::Flag::ITEM_TRACKING_ENABLED)) { throw logic_error("item tracking not enabled in BB game"); @@ -1098,95 +1231,6 @@ static void on_subcommand_medical_center_bb(shared_ptr, //////////////////////////////////////////////////////////////////////////////// -static void on_subcommand_forward_check_size(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_game(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - if (!l->is_game()) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_game_loading(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - if (!l->is_game() || !l->any_client_loading()) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_size_client(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (p->byte[2] != c->lobby_client_id) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_size_game(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (!l->is_game()) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_size_ep3_lobby(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_forward_check_size_ep3_game(shared_ptr, - shared_ptr l, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (!l->is_game() || !(l->flags & Lobby::Flag::EPISODE_3_ONLY)) { - return; - } - forward_subcommand(l, c, command, flag, data); -} - -static void on_subcommand_invalid(shared_ptr, - shared_ptr, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (command_is_private(command)) { - c->log.error("Invalid subcommand: %02hhX (private to %hhu)", p->byte[0], flag); - } else { - c->log.error("Invalid subcommand: %02hhX (public)", p->byte[0]); - } -} - -static void on_subcommand_unimplemented(shared_ptr, - shared_ptr, shared_ptr c, uint8_t command, uint8_t flag, - const string& data) { - const auto* p = check_size_sc(data, sizeof(PSOSubcommand), 0xFFFF); - if (command_is_private(command)) { - c->log.warning("Unknown subcommand: %02hhX (private to %hhu)", p->byte[0], flag); - } else { - c->log.warning("Unknown subcommand: %02hhX (public)", p->byte[0]); - } -} - -//////////////////////////////////////////////////////////////////////////////// - // Subcommands are described by four fields: the minimum size and maximum size (in DWORDs), // the handler function, and flags that tell when to allow the command. See command-input-subs.h // for more information on flags. The maximum size is not enforced if it's zero. @@ -1265,10 +1309,10 @@ subcommand_handler_t subcommand_handlers[0x100] = { /* 43 */ on_subcommand_forward_check_size_client, /* 44 */ on_subcommand_forward_check_size_client, /* 45 */ on_subcommand_forward_check_size_client, - /* 46 */ on_subcommand_forward_check_size_client, - /* 47 */ on_subcommand_forward_check_size_client, - /* 48 */ on_subcommand_use_technique, - /* 49 */ on_subcommand_forward_check_size_client, + /* 46 */ on_subcommand_attack_finished, + /* 47 */ on_subcommand_cast_technique, + /* 48 */ on_subcommand_cast_technique_finished, + /* 49 */ on_subcommand_subtract_pb_energy, /* 4A */ on_subcommand_forward_check_size_client, /* 4B */ on_subcommand_hit_by_enemy, /* 4C */ on_subcommand_hit_by_enemy, diff --git a/src/SendCommands.cc b/src/SendCommands.cc index a2fd1eea..f6b9ea63 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -121,10 +121,10 @@ prepare_server_init_contents_console( uint32_t server_key, uint32_t client_key, uint8_t flags) { bool initial_connection = (flags & SendServerInitFlag::IS_INITIAL_CONNECTION); S_ServerInitWithAfterMessage_DC_PC_V3_02_17_91_9B<0xB4> cmd; - cmd.copyright = initial_connection + cmd.basic_cmd.copyright = initial_connection ? dc_port_map_copyright : dc_lobby_server_copyright; - cmd.server_key = server_key; - cmd.client_key = client_key; + cmd.basic_cmd.server_key = server_key; + cmd.basic_cmd.client_key = client_key; cmd.after_message = anti_copyright; return cmd; } @@ -165,9 +165,9 @@ prepare_server_init_contents_bb( uint8_t flags) { bool use_secondary_message = (flags & SendServerInitFlag::USE_SECONDARY_MESSAGE); S_ServerInitWithAfterMessage_BB_03_9B<0xB4> cmd; - cmd.copyright = use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright; - cmd.server_key = server_key; - cmd.client_key = client_key; + cmd.basic_cmd.copyright = use_secondary_message ? bb_pm_server_copyright : bb_game_server_copyright; + cmd.basic_cmd.server_key = server_key; + cmd.basic_cmd.client_key = client_key; cmd.after_message = anti_copyright; return cmd; } @@ -187,11 +187,12 @@ void send_server_init_bb(shared_ptr s, shared_ptr c, shared_ptr detector_crypt(new PSOBBMultiKeyDetectorEncryption( s->bb_private_keys, bb_crypt_initial_client_commands, - cmd.client_key.data(), - sizeof(cmd.client_key))); + cmd.basic_cmd.client_key.data(), + sizeof(cmd.basic_cmd.client_key))); c->channel.crypt_in = detector_crypt; c->channel.crypt_out.reset(new PSOBBMultiKeyImitatorEncryption( - detector_crypt, cmd.server_key.data(), sizeof(cmd.server_key), true)); + detector_crypt, cmd.basic_cmd.server_key.data(), + sizeof(cmd.basic_cmd.server_key), true)); } void send_server_init_patch(shared_ptr c) { @@ -778,9 +779,9 @@ void send_guild_card_dc_pc_v3_t( uint8_t section_id, uint8_t char_class) { CmdT cmd; - cmd.subcommand = 0x06; - cmd.size = sizeof(CmdT) / 4; - cmd.unused = 0x0000; + cmd.header.subcommand = 0x06; + cmd.header.size = sizeof(CmdT) / 4; + cmd.header.unused = 0x0000; cmd.player_tag = 0x00010000; cmd.guild_card_number = guild_card_number; cmd.name = name; @@ -802,9 +803,9 @@ static void send_guild_card_bb( uint8_t section_id, uint8_t char_class) { G_SendGuildCard_BB_6x06 cmd; - cmd.subcommand = 0x06; - cmd.size = sizeof(cmd) / 4; - cmd.unused = 0x0000; + cmd.header.subcommand = 0x06; + cmd.header.size = sizeof(cmd) / 4; + cmd.header.unused = 0x0000; cmd.guild_card_number = guild_card_number; cmd.name = remove_language_marker(name); cmd.team_name = remove_language_marker(team_name); @@ -1378,84 +1379,54 @@ void send_resume_game(shared_ptr l, shared_ptr ready_client) { //////////////////////////////////////////////////////////////////////////////// // Game/cheat commands -// sends an HP/TP/Meseta modifying command (see flag definitions in command-functions.h) +static vector generate_stats_change_subcommands( + uint16_t client_id, PlayerStatsChange stat, uint32_t amount) { + if (amount > (0x7BF8 * 0xFF) / sizeof(G_UpdatePlayerStat_6x9A)) { + throw runtime_error("stats change command is too large"); + } + + uint8_t stat_ch = static_cast(stat); + vector subs; + while (amount > 0) { + uint8_t sub_amount = min(amount, 0xFF); + subs.emplace_back(G_UpdatePlayerStat_6x9A{{0x9A, 0x02, client_id}, 0, stat_ch, sub_amount}); + amount -= sub_amount; + } + return subs; +} + void send_player_stats_change(shared_ptr l, shared_ptr c, PlayerStatsChange stat, uint32_t amount) { + auto subs = generate_stats_change_subcommands(c->lobby_client_id, stat, amount); + send_command_vt(l, (subs.size() > 0x400 / sizeof(G_UpdatePlayerStat_6x9A)) ? 0x6C : 0x60, 0x00, subs); +} - if (amount > 2550) { - throw invalid_argument("amount cannot be larger than 2550"); - } - - vector subs; - while (amount > 0) { - { - auto& sub = subs.emplace_back(); - sub.byte[0] = 0x9A; - sub.byte[1] = 0x02; - sub.byte[2] = c->lobby_client_id; - sub.byte[3] = 0x00; - } - { - auto& sub = subs.emplace_back(); - sub.byte[0] = 0x00; - sub.byte[1] = 0x00; - sub.byte[2] = stat; - sub.byte[3] = (amount > 0xFF) ? 0xFF : amount; - amount -= sub.byte[3]; - } - } - - send_command_vt(l, 0x60, 0x00, subs); +void send_player_stats_change(Channel& ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount) { + auto subs = generate_stats_change_subcommands(client_id, stat, amount); + send_command_vt(ch, (subs.size() > 0x400 / sizeof(G_UpdatePlayerStat_6x9A)) ? 0x6C : 0x60, 0x00, subs); } void send_warp(Channel& ch, uint8_t client_id, uint32_t area) { - PSOSubcommand cmds[2]; - cmds[0].byte[0] = 0x94; - cmds[0].byte[1] = 0x02; - cmds[0].byte[2] = client_id; - cmds[0].byte[3] = 0x00; - cmds[1].dword = area; - ch.send(0x62, client_id, cmds, 8); + G_InterLevelWarp_6x94 cmd = {{0x94, 0x02, 0}, area, {}}; + ch.send(0x62, client_id, &cmd, sizeof(cmd)); } void send_warp(shared_ptr c, uint32_t area) { - PSOSubcommand cmds[2]; - cmds[0].byte[0] = 0x94; - cmds[0].byte[1] = 0x02; - cmds[0].byte[2] = c->lobby_client_id; - cmds[0].byte[3] = 0x00; - cmds[1].dword = area; - send_command(c, 0x62, c->lobby_client_id, cmds, 8); + send_warp(c->channel, c->lobby_client_id, area); c->area = area; } void send_ep3_change_music(shared_ptr c, uint32_t song) { - PSOSubcommand cmds[2]; - cmds[0].byte[0] = 0xBF; - cmds[0].byte[1] = 0x02; - cmds[0].byte[2] = c->lobby_client_id; - cmds[0].byte[3] = 0x00; - cmds[1].dword = song; - send_command(c, 0x60, 0x00, cmds, 8); + G_ChangeLobbyMusic_GC_Ep3_6xBF cmd = {{0xBF, 0x02, 0}, song}; + send_command_t(c, 0x60, 0x00, cmd); } void send_set_player_visibility(shared_ptr l, shared_ptr c, bool visible) { - PSOSubcommand cmd; - cmd.byte[0] = visible ? 0x23 : 0x22; - cmd.byte[1] = 0x01; - cmd.byte[2] = c->lobby_client_id; - cmd.byte[3] = 0x00; - send_command(l, 0x60, 0x00, &cmd, 4); -} - -void send_revive_player(shared_ptr l, shared_ptr c) { - PSOSubcommand cmd; - cmd.byte[0] = 0x31; - cmd.byte[1] = 0x01; - cmd.byte[2] = c->lobby_client_id; - cmd.byte[3] = 0x00; - send_command(l, 0x60, 0x00, &cmd, 4); + uint8_t subcmd = visible ? 0x23 : 0x22; + uint16_t client_id = c->lobby_client_id; + G_SetPlayerVisibility_6x22_6x23 cmd = {{subcmd, 0x01, client_id}}; + send_command_t(l, 0x60, 0x00, cmd); } @@ -1466,14 +1437,14 @@ void send_revive_player(shared_ptr l, shared_ptr c) { void send_drop_item(Channel& ch, const ItemData& item, bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { G_DropItem_PC_V3_BB_6x5F cmd = { - {0x5F, 0x0B, 0x0000, area, from_enemy, request_id, x, z, 0, item}, 0}; + {{0x5F, 0x0B, 0x0000}, area, from_enemy, request_id, x, z, 0, 0, item}, 0}; ch.send(0x60, 0x00, &cmd, sizeof(cmd)); } void send_drop_item(shared_ptr l, const ItemData& item, bool from_enemy, uint8_t area, float x, float z, uint16_t request_id) { G_DropItem_PC_V3_BB_6x5F cmd = { - {0x5F, 0x0B, 0x0000, area, from_enemy, request_id, x, z, 0, item}, 0}; + {{0x5F, 0x0B, 0x0000}, area, from_enemy, request_id, x, z, 0, 0, item}, 0}; send_command_t(l, 0x60, 0x00, cmd); } @@ -1484,14 +1455,15 @@ void send_drop_stacked_item(shared_ptr l, const ItemData& item, // TODO: Is this order correct? The original code sent {item, 0}, but it seems // GC sends {0, item} (the last two fields in the struct are switched). G_DropStackedItem_PC_V3_BB_6x5D cmd = { - {0x5D, 0x0A, 0x0000, area, 0, x, z, item}, 0}; + {{0x5D, 0x0A, 0x0000}, area, 0, x, z, item}, 0}; send_command_t(l, 0x60, 0x00, cmd); } void send_pick_up_item(shared_ptr l, shared_ptr c, uint32_t item_id, uint8_t area) { + uint16_t client_id = c->lobby_client_id; G_PickUpItem_6x59 cmd = { - 0x59, 0x03, c->lobby_client_id, c->lobby_client_id, area, item_id}; + {0x59, 0x03, client_id}, client_id, area, item_id}; send_command_t(l, 0x60, 0x00, cmd); } @@ -1499,16 +1471,16 @@ void send_pick_up_item(shared_ptr l, shared_ptr c, // bank) void send_create_inventory_item(shared_ptr l, shared_ptr c, const ItemData& item) { - G_CreateInventoryItem_BB_6xBE cmd = { - 0xBE, 0x07, c->lobby_client_id, item, 0}; + uint16_t client_id = c->lobby_client_id; + G_CreateInventoryItem_BB_6xBE cmd = {{0xBE, 0x07, client_id}, item, 0}; send_command_t(l, 0x60, 0x00, cmd); } // destroys an item void send_destroy_item(shared_ptr l, shared_ptr c, uint32_t item_id, uint32_t amount) { - G_ItemSubcommand cmd = { - 0x29, 0x03, c->lobby_client_id, item_id, amount}; + uint16_t client_id = c->lobby_client_id; + G_DeleteInventoryItem_6x29 cmd = {{0x29, 0x03, client_id}, item_id, amount}; send_command_t(l, 0x60, 0x00, cmd); } @@ -1519,10 +1491,10 @@ void send_bank(shared_ptr c) { uint32_t checksum = random_object(); G_BankContentsHeader_BB_6xBC cmd = { - 0xBC, 0, 0, 0, checksum, c->game_data.player()->bank.num_items, c->game_data.player()->bank.meseta}; + {{0xBC, 0, 0}, 0}, checksum, c->game_data.player()->bank.num_items, c->game_data.player()->bank.meseta}; size_t size = 8 + sizeof(cmd) + items.size() * sizeof(PlayerBankItem); - cmd.size = size; + cmd.header.size = size; send_command_t_vt(c, 0x6C, 0x00, cmd, items); } @@ -1530,9 +1502,7 @@ void send_bank(shared_ptr c) { // sends the player a shop's contents void send_shop(shared_ptr c, uint8_t shop_type) { G_ShopContents_BB_6xB6 cmd = { - 0xB6, - 0x2C, - 0x037F, + {0xB6, 0x2C, 0x037F}, shop_type, static_cast(c->game_data.shop_contents.size()), 0, @@ -1566,24 +1536,24 @@ void send_level_up(shared_ptr l, shared_ptr c) { } G_LevelUp_6x30 cmd = { - 0x30, - sizeof(G_LevelUp_6x30) / 4, - c->lobby_client_id, + {0x30, sizeof(G_LevelUp_6x30) / 4, c->lobby_client_id}, stats.atp, stats.mst, stats.evp, stats.hp, stats.dfp, stats.ata, - c->game_data.player()->disp.level}; + c->game_data.player()->disp.level.load(), + 0}; send_command_t(l, 0x60, 0x00, cmd); } // gives a player EXP void send_give_experience(shared_ptr l, shared_ptr c, uint32_t amount) { + uint16_t client_id = c->lobby_client_id; G_GiveExperience_BB_6xBF cmd = { - 0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, c->lobby_client_id, amount}; + {0xBF, sizeof(G_GiveExperience_BB_6xBF) / 4, client_id}, amount}; send_command_t(l, 0x60, 0x00, cmd); } @@ -1633,7 +1603,8 @@ void send_ep3_map_list(shared_ptr s, shared_ptr l) { StringWriter w; uint32_t subcommand_size = (data.size() + sizeof(G_MapList_GC_Ep3_6xB6x40) + 3) & (~3); - w.put({{0xB6, 0, 0, subcommand_size, 0x40}, data.size()}); + w.put( + G_MapList_GC_Ep3_6xB6x40{{{{0xB6, 0, 0}, subcommand_size}, 0x40, {}}, data.size(), 0}); w.write(data); send_command(l, 0x6C, 0x00, w.str()); } @@ -1644,7 +1615,7 @@ void send_ep3_map_data(shared_ptr s, shared_ptr l, uint32_t StringWriter w; uint32_t subcommand_size = (compressed.size() + sizeof(G_MapData_GC_Ep3_6xB6x41) + 3) & (~3); - w.put({{0xB6, 0, 0, subcommand_size, 0x41}, entry->map.map_number.load(), 0, compressed.size()}); + w.put({{{{0xB6, 0, 0}, subcommand_size}, 0x41, {}}, entry->map.map_number.load(), compressed.size(), 0}); w.write(compressed); send_command(l, 0x6C, 0x00, w.str()); } @@ -1687,7 +1658,7 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) { } auto* header = reinterpret_cast(vdata); - size_t command_bytes = header->size * 4; + size_t command_bytes = header->basic_header.size * 4; if (command_bytes != size) { throw runtime_error("command size field does not match actual size"); } diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 56eca602..66c629f2 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -84,6 +84,12 @@ void send_command_vt(std::shared_ptr c, uint16_t command, send_command(c, command, flag, data.data(), data.size() * sizeof(StructT)); } +template +void send_command_vt(Channel& ch, uint16_t command, uint32_t flag, + const std::vector& data) { + ch.send(command, flag, data.data(), data.size() * sizeof(StructT)); +} + template void send_command_t_vt(std::shared_ptr c, uint16_t command, uint32_t flag, const StructT& data, const std::vector& array_data) { @@ -226,7 +232,9 @@ enum PlayerStatsChange { }; void send_player_stats_change(std::shared_ptr l, std::shared_ptr c, - PlayerStatsChange which, uint32_t amount); + PlayerStatsChange stat, uint32_t amount); +void send_player_stats_change( + Channel& ch, uint16_t client_id, PlayerStatsChange stat, uint32_t amount); void send_warp(Channel& ch, uint8_t client_id, uint32_t area); void send_warp(std::shared_ptr c, uint32_t area); diff --git a/src/ServerShell.cc b/src/ServerShell.cc index 23bcaafc..79a568f2 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -327,13 +327,9 @@ Proxy commands (these will only work when exactly one client is connected):\n\ } else if (command_name == "warp") { auto session = this->get_proxy_session(); - PSOSubcommand cmds[2]; - cmds[0].word[0] = 0x0294; - cmds[0].word[1] = session->lobby_client_id; - cmds[1].dword = stoul(command_args); - - session->client_channel.send(0x60, 0x00, &cmds, sizeof(cmds)); - session->server_channel.send(0x60, 0x00, &cmds, sizeof(cmds)); + uint8_t area = stoul(command_args); + send_warp(session->client_channel, session->lobby_client_id, area); + send_warp(session->server_channel, session->lobby_client_id, area); } else if ((command_name == "info-board") || (command_name == "info-board-data")) { auto session = this->get_proxy_session();