diff --git a/dGame/dComponents/MultiZoneEntranceComponent.cpp b/dGame/dComponents/MultiZoneEntranceComponent.cpp index 0e2681c76..db716fcb9 100644 --- a/dGame/dComponents/MultiZoneEntranceComponent.cpp +++ b/dGame/dComponents/MultiZoneEntranceComponent.cpp @@ -34,5 +34,5 @@ void MultiZoneEntranceComponent::OnSelectWorld(Entity* originator, uint32_t inde auto* rocketLaunchpadControlComponent = m_Parent->GetComponent(); if (!rocketLaunchpadControlComponent || index >= m_LUPWorlds.size()) return; - rocketLaunchpadControlComponent->Launch(originator, m_LUPWorlds[index], 0); + rocketLaunchpadControlComponent->Launch(originator, m_LUPWorlds[index], 0, static_cast(index)); } diff --git a/dGame/dComponents/RocketLaunchpadControlComponent.cpp b/dGame/dComponents/RocketLaunchpadControlComponent.cpp index 686e51dde..b920cb0db 100644 --- a/dGame/dComponents/RocketLaunchpadControlComponent.cpp +++ b/dGame/dComponents/RocketLaunchpadControlComponent.cpp @@ -45,7 +45,14 @@ RocketLaunchpadControlComponent::~RocketLaunchpadControlComponent() { delete m_AltPrecondition; } -void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId, LWOCLONEID cloneId) { +GameMessages::FireEventClientSide RocketLaunchpadControlComponent::MakeRocketEquipped(const LWOOBJID launchpad, const LWOOBJID rocket, const LWOOBJID player, const LWOCLONEID cloneId, const int32_t worldIndex) { + GameMessages::FireEventClientSide rocketEquipped(launchpad, u"RocketEquipped", rocket, player); + if (cloneId != LWOCLONEID_INVALID) rocketEquipped.param1 = cloneId; + rocketEquipped.param2 = worldIndex; + return rocketEquipped; +} + +void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId, LWOCLONEID cloneId, int32_t worldIndex) { auto zone = mapId == LWOMAPID_INVALID ? m_TargetZone : mapId; if (zone == 0) { @@ -79,7 +86,7 @@ void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId, SetSelectedMapId(originator->GetObjectID(), zone); // Equipping the rocket (RocketEquip) sent ChangeObjectWorldState(ATTACHED); live sent it before this event - GameMessages::FireEventClientSide(m_Parent->GetObjectID(), u"RocketEquipped", rocket->GetId(), originator->GetObjectID()).SendToClient(originator->GetSystemAddress()); + MakeRocketEquipped(m_Parent->GetObjectID(), rocket->GetId(), originator->GetObjectID(), cloneId, worldIndex).Send(UNASSIGNED_SYSTEM_ADDRESS); Game::entityManager->SerializeEntity(originator); } diff --git a/dGame/dComponents/RocketLaunchpadControlComponent.h b/dGame/dComponents/RocketLaunchpadControlComponent.h index def3bacd3..ee593a894 100644 --- a/dGame/dComponents/RocketLaunchpadControlComponent.h +++ b/dGame/dComponents/RocketLaunchpadControlComponent.h @@ -11,6 +11,10 @@ #include "Component.h" #include "eReplicaComponentType.h" +namespace GameMessages { + struct FireEventClientSide; +} + class PreconditionExpression; /** @@ -28,8 +32,17 @@ public: * @param originator the entity to launch * @param mapId the world to go to * @param cloneId the clone ID (for properties) + * @param worldIndex which of a LUP entrance's worlds was picked, -1 otherwise */ - void Launch(Entity* originator, LWOMAPID mapId = LWOMAPID_INVALID, LWOCLONEID cloneId = LWOCLONEID_INVALID); + void Launch(Entity* originator, LWOMAPID mapId = LWOMAPID_INVALID, LWOCLONEID cloneId = LWOCLONEID_INVALID, int32_t worldIndex = -1); + + /** + * The "RocketEquipped" event live sent from the launchpad to every client in the zone when a player launched + * (1,196 of the 1,265 captured were other players' launches): it plays the launch for that player on every + * client (LWORocketLaunchComponentCommon::msgFireEventClientSide). param1 is the property clone (0 is left out), + * param2 the picked LUP world index (-1 is left out). + */ + static GameMessages::FireEventClientSide MakeRocketEquipped(LWOOBJID launchpad, LWOOBJID rocket, LWOOBJID player, LWOCLONEID cloneId, int32_t worldIndex); /** * Handles an OnUse event from some entity, preparing it for launch to some other world diff --git a/docs/PacketArchitecture.md b/docs/PacketArchitecture.md index d40b0d426..55ad08b7f 100644 --- a/docs/PacketArchitecture.md +++ b/docs/PacketArchitecture.md @@ -315,7 +315,7 @@ the 1.10.64 client. | `SetStatusImmunity` | Fixed (wire fix): used to write the flags in DLU's order (BasicAttack, DOT, Knockback, Interrupt, Speed, ImaginationGain, ImaginationLoss, QuickbuildInterrupt, PullToPoint). | The client uses alphabetical order: BasicAttack, DOT, ImaginationGain, ImaginationLoss, Interrupt, Knockback, PullToPoint, QuickbuildInterrupt, Speed (`0x00d8f140`; field offsets named by `0x00d8f410`). | | `RequestDie` | Read with the `Die` layout. | Starts with one `bDieAccepted` bit and has a mandatory `lootOwnerID` (`0x00e02d90`). | | `SetCurrency` | `sourceTradeID` is an optional `int32_t`. | lu_packets has an object ID (8 bytes). DLU only ever sends 0 (flag bit 0), so no bytes differ today. | -| `FireEventClientSide` | Never writes `param1`/`param2` (both flag bits 0), whatever the caller passed: `RocketEquipped` loses the clone ID. | Optional `i64 param1` (default 0) and `i32 param2` (default -1). | +| `FireEventClientSide` | Fixed: writes `param1`/`param2`. `RocketEquipped` carries the property clone in `param1` and a LUP entrance's world index in `param2`, and goes to every client (live: 1,196 of 1,265 captured were other players' launches). | Optional `i64 param1` (default 0) and `i32 param2` (default -1). | | `PickupCurrency` | Reads only the amount. | lu_packets has a position after it (ignored, harmless). | | `MatchUpdate`, `MatchRequest` | Name-value text is widened/narrowed one byte per UTF-16 unit, so non-ASCII names are garbled. | UTF-16 text. | | `ScriptNetworkVarUpdate` | The text goes through `ASCIIToUTF16`. | UTF-16 text (non-ASCII values are garbled). | diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 67990031f..4e2ada537 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -14,6 +14,7 @@ set(DCOMPONENTS_TESTS "CollectibleComponentTests.cpp" "ReplicaComponentOrderTests.cpp" "QuickBuildCompleteTests.cpp" + "RocketLaunchTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp b/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp new file mode 100644 index 000000000..45f0ff75b --- /dev/null +++ b/tests/dGameTests/dComponentsTests/RocketLaunchTests.cpp @@ -0,0 +1,51 @@ +#include "GameDependencies.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" + +#include "ObjectMessages.h" +#include "RocketLaunchpadControlComponent.h" + +#include + +using namespace GameMessageTestUtils; + +// The launchpad's RocketEquipped event, compared with live. Each expected packet is a live one with the launchpad, +// rocket and player IDs replaced. +class RocketLaunchTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } + + static constexpr LWOOBJID PAD = 0x0000400000112233LL; + static constexpr LWOOBJID ROCKET = 0x1000000000000002LL; + static constexpr LWOOBJID PLAYER = 0x1000000000000001LL; +}; + +TEST_F(RocketLaunchTests, ZoneLaunchHasNoParameters) { + const auto msg = RocketLaunchpadControlComponent::MakeRocketEquipped(PAD, ROCKET, PLAYER, LWOCLONEID_INVALID, -1); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 33 22 11 00 00 40 00 00 bd 04 0e 00 00 00 52 00 6f 00 63 00 6b 00 65 00 74 00 45 00 71 00 " + "75 00 69 00 70 00 70 00 65 00 64 00 02 00 00 00 00 00 00 10 00 40 00 00 00 00 00 04 00", 530), StructPacket(msg)); +} + +TEST_F(RocketLaunchTests, PropertyLaunchCarriesTheClone) { + const auto msg = RocketLaunchpadControlComponent::MakeRocketEquipped(PAD, ROCKET, PLAYER, 338862, -1); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 33 22 11 00 00 40 00 00 bd 04 0e 00 00 00 52 00 6f 00 63 00 6b 00 65 00 74 00 45 00 71 00 " + "75 00 69 00 70 00 70 00 65 00 64 00 02 00 00 00 00 00 00 10 d7 15 82 80 00 00 00 00 00 40 00 00 00 00 00 04 00", 594), StructPacket(msg)); +} + +TEST_F(RocketLaunchTests, LupLaunchCarriesTheWorldIndex) { + const auto msg = RocketLaunchpadControlComponent::MakeRocketEquipped(PAD, ROCKET, PLAYER, 0, 0); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 33 22 11 00 00 40 00 00 bd 04 0e 00 00 00 52 00 6f 00 63 00 6b 00 65 00 74 00 45 00 71 00 " + "75 00 69 00 70 00 70 00 65 00 64 00 02 00 00 00 00 00 00 10 40 00 00 00 00 40 00 00 00 00 00 04 00", 562), StructPacket(msg)); +} + +// Sent to every client (Send with UNASSIGNED_SYSTEM_ADDRESS broadcasts), as Launch does. +TEST_F(RocketLaunchTests, RocketEquippedIsBroadcast) { + const auto sent = Capture([&] { + RocketLaunchpadControlComponent::MakeRocketEquipped(PAD, ROCKET, PLAYER, LWOCLONEID_INVALID, -1).Send(UNASSIGNED_SYSTEM_ADDRESS); + }); + ASSERT_EQ(sent.size(), 1u); + EXPECT_TRUE(sent[0].broadcast); +}