From 0c93275e8835d13d1af97abcdd049e98cc33688b Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sun, 24 Aug 2025 22:35:15 -0700 Subject: [PATCH] describe some esoteric NTE and 11/2000 commands --- src/CommandFormats.hh | 77 +++++++++++++++++++++++++++----- src/ReceiveSubcommands.cc | 92 ++++++++++++++++++--------------------- 2 files changed, 108 insertions(+), 61 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index dcc1673c..a0baa219 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -4221,8 +4221,34 @@ struct G_DisablePKModeForPlayer_6x1C { G_ClientIDHeader header; } __packed_ws__(G_DisablePKModeForPlayer_6x1C, 4); -// 6x1D: Invalid subcommand -// 6x1E: Invalid subcommand +// 6x1D: Request partial player data (pre-v1 only) +// The subcommand number 6x1D is not used in any final version of PSO; this +// number is assigned based on what the command number would be if it were. On +// DC NTE, this is subcommand 6x19; on 11/2000, it's 6x1B. +// This command does not appear to ever be sent by the client; however, it will +// respond with 6x1E if it receives this command. + +struct G_RequestPartialPlayerData_DCProtos_6x1D { + G_UnusedHeader header; +} __packed_ws__(G_RequestPartialPlayerData_DCProtos_6x1D, 4); + +// 6x1E: Partial player data (pre-v1 only) +// The subcommand number 6x1E is not used in any final version of PSO; this +// number is assigned based on what the command number would be if it were. On +// DC NTE, this is subcommand 6x1A; on 11/2000, it's 6x1C. +// The command is truncated after the last valid item in the inventory (that +// is, there will be less than 0x360 bytes if the player has fewer than 30 +// items on hand). + +struct G_PartialPlayerData_DCProtos_6x1E { + /* 0000 */ G_ClientIDHeader header; + /* 0004 */ le_uint16_t floor; + /* 0006 */ le_uint16_t num_items; + /* 0008 */ VectorXYZF pos; + /* 0014 */ le_uint32_t angle_y; + /* 0018 */ parray items; + /* 0360 */ +} __packed_ws__(G_PartialPlayerData_DCProtos_6x1E, 0x360); // 6x1F: Set player floor and request positions @@ -4418,7 +4444,18 @@ struct G_RevivePlayer_6x33 { // 6x34: Unknown // This subcommand is ignored by all versions of PSO. -// 6x35: Invalid subcommand +// 6x35: Unknown (pre-v1 only) +// This command seems to have a unique history. In DC NTE, it is 6x30, and has +// a handler that does something related to what 6x36 does (6x31 in DC NTE). In +// 11/2000, however, it seems to have been entirely deleted, and has no command +// number at all. But then in DC v1 and later, there is a gap in the subcommand +// number table, implying that this command was assigned a number, but there is +// no function to send or handle it. + +struct G_Unknown_DCNTE_6x35 { + G_ClientIDHeader header; + parray unused; +} __packed_ws__(G_Unknown_DCNTE_6x35, 8); // 6x36: Unknown (supported; game only) // This subcommand is completely ignored on V3. @@ -4446,6 +4483,16 @@ struct G_DonateToPhotonBlast_6x38 { le_uint16_t unused = 0; } __packed_ws__(G_DonateToPhotonBlast_6x38, 8); +// 6x38.5 (nominally) / 6x33 (DC NTE) / 6x35 (11/2000): Unknown (related to +// level up sequence) +// This command was deleted after 11/2000 and has no assigned number in any +// final version of PSO, hence the odd numbering. It is sent during the level +// up sequence in certain situations (TODO). + +struct G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35 { + G_ClientIDHeader header; +} __packed_ws__(G_UnknownLevelUpSequence_DCNTE_6x33_112000_6x35, 4); + // 6x39: Photon blast ready (protected on V3/V4) // This is sent when a player's PB meter reaches 100. @@ -4471,7 +4518,20 @@ struct G_ClearTemporaryPhotonBlastStateFlags_6x3B { // 6x3C: Unknown (DCv1 and earlier) // This command has a handler, but it does nothing, even on DC NTE. -// 6x3D: Invalid subcommand +// 6x3D: Target list base +// This appears to be a base class for 6x46, 6x47, and 6x49 (and possibly other +// subcommands), but it is never sent on the wire. Its likely purpose is to +// provide the TargetEntry structure and related functions to derived classes, +// but it does still have a structure of its own, as described here. + +struct TargetEntry { + le_uint16_t entity_id = 0; + le_uint16_t unknown_a2 = 0; +} __packed_ws__(TargetEntry, 4); + +struct G_TargetBase_6x3D { + G_UnusedHeader header; +} __packed_ws__(G_TargetBase_6x3D, 4); // 6x3E: Stop moving (protected on V3/V4) @@ -4533,11 +4593,6 @@ struct G_Attack_6x43_6x44_6x45 { // targets is too large, the client will byteswap the function's return address // on the stack, and it will crash. -struct TargetEntry { - le_uint16_t entity_id = 0; - le_uint16_t unknown_a2 = 0; -} __packed_ws__(TargetEntry, 4); - struct G_AttackFinished_6x46 { G_ClientIDHeader header; le_uint32_t target_count = 0; @@ -4635,8 +4690,8 @@ struct G_SwitchInteraction_6x50 { // 6x51: Set player angle // If UDP mode is enabled, this command is sent via UDP. // This command appears to be vestigial - no version of the game has a handler -// for it (it is always ignored), but PSO GC has a function that sends it. It's -// not known if this function is ever called, or how to trigger it. +// for it (it is always ignored), but most versions have a function that sends +// it. It's not known if this function is ever called, or how to trigger it. struct G_SetPlayerAngle_6x51 { G_ClientIDHeader header; diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index 3dd9f3bd..763c41a9 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -72,55 +72,46 @@ struct SubcommandDefinition { }; using SDF = SubcommandDefinition::Flag; -extern const SubcommandDefinition subcommand_definitions[0x100]; - -static const SubcommandDefinition* def_for_nte_subcommand(uint8_t subcommand) { - static std::array nte_to_final_map; - static bool nte_to_final_map_populated = false; - if (!nte_to_final_map_populated) { - nte_to_final_map.fill(0); - for (size_t z = 0; z < 0x100; z++) { - const auto& def = subcommand_definitions[z]; - if (def.nte_subcommand != 0x00) { - if (nte_to_final_map[def.nte_subcommand]) { - throw logic_error("multiple NTE subcommands map to the same final subcommand"); - } - nte_to_final_map[def.nte_subcommand] = z; - } - } - nte_to_final_map_populated = true; - } - uint8_t final_subcommand = nte_to_final_map[subcommand]; - return final_subcommand ? &subcommand_definitions[final_subcommand] : nullptr; -} - -static const SubcommandDefinition* def_for_proto_subcommand(uint8_t subcommand) { - static std::array proto_to_final_map; - static bool proto_to_final_map_populated = false; - if (!proto_to_final_map_populated) { - proto_to_final_map.fill(0); - for (size_t z = 0; z < 0x100; z++) { - const auto& def = subcommand_definitions[z]; - if (def.proto_subcommand != 0x00) { - if (proto_to_final_map[def.proto_subcommand]) { - throw logic_error("multiple prototype subcommands map to the same final subcommand"); - } - proto_to_final_map[def.proto_subcommand] = z; - } - } - proto_to_final_map_populated = true; - } - uint8_t final_subcommand = proto_to_final_map[subcommand]; - return final_subcommand ? &subcommand_definitions[final_subcommand] : nullptr; -} +extern const vector subcommand_definitions; const SubcommandDefinition* def_for_subcommand(Version version, uint8_t subcommand) { + static bool populated = false; + static std::array nte_defs; + static std::array proto_defs; + static std::array final_defs; + if (!populated) { + nte_defs.fill(nullptr); + proto_defs.fill(nullptr); + final_defs.fill(nullptr); + for (const auto& def : subcommand_definitions) { + if (def.nte_subcommand != 0x00) { + if (nte_defs[def.nte_subcommand]) { + throw logic_error("multiple subcommand definitions map to the same NTE subcommand"); + } + nte_defs[def.nte_subcommand] = &def; + } + if (def.proto_subcommand != 0x00) { + if (proto_defs[def.proto_subcommand]) { + throw logic_error("multiple subcommand definitions map to the same 11/2000 subcommand"); + } + proto_defs[def.proto_subcommand] = &def; + } + if (def.final_subcommand != 0x00) { + if (final_defs[def.final_subcommand]) { + throw logic_error("multiple subcommand definitions map to the same final subcommand"); + } + final_defs[def.final_subcommand] = &def; + } + } + populated = true; + } + if (version == Version::DC_NTE) { - return def_for_nte_subcommand(subcommand); + return nte_defs[subcommand]; } else if (version == Version::DC_11_2000) { - return def_for_proto_subcommand(subcommand); + return proto_defs[subcommand]; } else { - return &subcommand_definitions[subcommand]; + return final_defs[subcommand]; } } @@ -5205,7 +5196,7 @@ static asio::awaitable on_write_quest_counter_bb(shared_ptr c, Sub // syntax highlighting constexpr uint8_t NONE = 0x00; -const SubcommandDefinition subcommand_definitions[0x100] = { +const vector subcommand_definitions{ // {DC NTE, 11/2000, all other versions, handler} /* 6x00 */ {0x00, 0x00, 0x00, on_invalid}, /* 6x01 */ {0x01, 0x01, 0x01, on_invalid}, @@ -5260,15 +5251,16 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x32 */ {NONE, NONE, 0x32, on_forward_check_game}, /* 6x33 */ {0x2E, 0x30, 0x33, on_forward_check_game}, /* 6x34 */ {0x2F, 0x31, 0x34, on_forward_check_game}, - /* 6x35 */ {0x30, 0x32, 0x35, on_invalid}, - /* 6x36 */ {NONE, NONE, 0x36, on_forward_check_game}, + /* 6x35 */ {0x30, NONE, 0x35, on_invalid}, + /* 6x36 */ {0x31, 0x32, 0x36, on_forward_check_game}, /* 6x37 */ {0x32, 0x33, 0x37, on_forward_check_game}, - /* 6x38 */ {0x33, 0x34, 0x38, on_forward_check_game}, + /* 6x38 */ {NONE, 0x34, 0x38, on_forward_check_game}, + /* NONE */ {0x33, 0x35, NONE, on_forward_check_game}, /* 6x39 */ {NONE, 0x36, 0x39, on_forward_check_game}, /* 6x3A */ {NONE, 0x37, 0x3A, on_forward_check_game}, /* 6x3B */ {NONE, 0x38, 0x3B, forward_subcommand_m}, /* 6x3C */ {0x34, 0x39, 0x3C, forward_subcommand_m}, - /* 6x3D */ {NONE, NONE, 0x3D, on_invalid}, + /* 6x3D */ {0x35, 0x3A, 0x3D, on_invalid}, /* 6x3E */ {NONE, NONE, 0x3E, on_movement_with_floor}, /* 6x3F */ {0x36, 0x3B, 0x3F, on_movement_with_floor}, /* 6x40 */ {0x37, 0x3C, 0x40, on_movement}, @@ -5288,7 +5280,7 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x4E */ {NONE, NONE, 0x4E, on_player_revivable}, /* 6x4F */ {0x43, 0x49, 0x4F, on_player_revived}, /* 6x50 */ {0x44, 0x4A, 0x50, on_forward_check_game_client}, - /* 6x51 */ {NONE, NONE, 0x51, on_invalid}, + /* 6x51 */ {0x45, 0x4B, 0x51, on_invalid}, /* 6x52 */ {0x46, 0x4C, 0x52, on_set_animation_state}, /* 6x53 */ {0x47, 0x4D, 0x53, on_forward_check_game}, /* 6x54 */ {0x48, 0x4E, 0x54, forward_subcommand_m},