diff --git a/dGame/dGameMessages/ObjectMessages.cpp b/dGame/dGameMessages/ObjectMessages.cpp index 62ac10ccb..824a5d52d 100644 --- a/dGame/dGameMessages/ObjectMessages.cpp +++ b/dGame/dGameMessages/ObjectMessages.cpp @@ -13,6 +13,7 @@ #include "MissionOfferComponent.h" #include "PlayerManager.h" #include "RocketLaunchpadControlComponent.h" +#include "ZoneMessages.h" #include "ZoneInstanceManager.h" #include "dServer.h" #include "dZoneManager.h" @@ -115,6 +116,8 @@ namespace GameMessages { character->SetZoneClone(zoneClone); } + GameMessages::SendZoneTransferNotice(player->GetObjectID(), static_cast(zoneID), zoneClone, character ? GeneralUtils::ASCIIToUTF16(character->GetTargetScene()) : u"", sysAddr); + ClientPackets::TransferToWorld transfer; transfer.serverIP = LUString(serverIP); transfer.serverPort = serverPort; diff --git a/dGame/dGameMessages/ZoneMessages.cpp b/dGame/dGameMessages/ZoneMessages.cpp index 68727cf76..08f007d43 100644 --- a/dGame/dGameMessages/ZoneMessages.cpp +++ b/dGame/dGameMessages/ZoneMessages.cpp @@ -1,5 +1,6 @@ #include "ZoneMessages.h" #include "MovementMessages.h" +#include "MissionMessages.h" #include "BitStreamUtils.h" #include "CDClientManager.h" @@ -175,6 +176,84 @@ namespace GameMessages { return bitStream.Read(objectID); } + namespace { + template + void WriteTransfer(RakNet::BitStream& bitStream, const Msg& msg, const bool first) { + bitStream.Write(first); + BitStreamUtils::WriteOptional(bitStream, msg.cloneID, 0); + BitStreamUtils::WriteOptional(bitStream, msg.posX, std::numeric_limits::max()); + BitStreamUtils::WriteOptional(bitStream, msg.posY, std::numeric_limits::max()); + BitStreamUtils::WriteOptional(bitStream, msg.posZ, std::numeric_limits::max()); + BitStreamUtils::WriteOptional(bitStream, msg.rotW, 1.0f); + BitStreamUtils::WriteOptional(bitStream, msg.rotX, 0.0f); + BitStreamUtils::WriteOptional(bitStream, msg.rotY, 0.0f); + BitStreamUtils::WriteOptional(bitStream, msg.rotZ, 0.0f); + BitStreamUtils::WriteLengthPrefixed(bitStream, msg.spawnPoint); + bitStream.Write(msg.ucInstanceType); + BitStreamUtils::WriteOptional(bitStream, msg.zoneID, 0); + } + + template + bool ReadTransfer(RakNet::BitStream& bitStream, Msg& msg, bool& first) { + VALIDATE_READ(bitStream.Read(first)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.cloneID, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.posX, std::numeric_limits::max())); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.posY, std::numeric_limits::max())); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.posZ, std::numeric_limits::max())); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.rotW, 1.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.rotX, 0.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.rotY, 0.0f)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.rotZ, 0.0f)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, msg.spawnPoint)); + VALIDATE_READ(bitStream.Read(msg.ucInstanceType)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, msg.zoneID, 0)); + return true; + } + } + + void TransferToZone::Serialize(RakNet::BitStream& bitStream) const { + WriteTransfer(bitStream, *this, bCheckTransferAllowed); + } + + bool TransferToZone::Deserialize(RakNet::BitStream& bitStream) { + return ReadTransfer(bitStream, *this, bCheckTransferAllowed); + } + + void TransferToZoneCheckedIM::Serialize(RakNet::BitStream& bitStream) const { + WriteTransfer(bitStream, *this, bIsThereaQueue); + } + + bool TransferToZoneCheckedIM::Deserialize(RakNet::BitStream& bitStream) { + return ReadTransfer(bitStream, *this, bIsThereaQueue); + } + + void SendZoneTransferNotice(const LWOOBJID player, const LWOMAPID zoneID, const LWOCLONEID cloneID, const std::u16string& spawnPoint, const SystemAddress& sysAddr) { + NotifyClientFlagChange flag; + flag.target = player; + flag.iFlagID = static_cast(ePlayerFlag::BEGINNING_ZONE_SUMMARY_DISPLAYED); + flag.bFlag = true; + flag.SendToClient(sysAddr); + + TransferToZone transfer; + transfer.target = player; + transfer.bCheckTransferAllowed = true; + transfer.cloneID = cloneID; + transfer.spawnPoint = spawnPoint; + transfer.zoneID = zoneID; + transfer.SendToClient(sysAddr); + + TransferToZoneCheckedIM checked; + checked.target = player; + checked.bIsThereaQueue = false; + checked.cloneID = cloneID; + checked.spawnPoint = spawnPoint; + checked.zoneID = zoneID; + checked.SendToClient(sysAddr); + + flag.bFlag = false; + flag.SendToClient(sysAddr); + } + void InvalidZoneTransferList::Serialize(RakNet::BitStream& bitStream) const { BitStreamUtils::WriteLengthPrefixed(bitStream, customerFeedbackURL); BitStreamUtils::WriteLengthPrefixed(bitStream, invalidMapTransferList); diff --git a/dGame/dGameMessages/ZoneMessages.h b/dGame/dGameMessages/ZoneMessages.h index 7743a8d71..993844cc1 100644 --- a/dGame/dGameMessages/ZoneMessages.h +++ b/dGame/dGameMessages/ZoneMessages.h @@ -4,11 +4,63 @@ #include "GameMessages.h" #include "eObjectWorldState.h" +#include #include // Game messages for a player's zone lifecycle: loading in, zone summaries, level ups, announcements. // Fields are listed in wire order; names follow the client (legouniverse.exe 1.10.64) where known. namespace GameMessages { + // Server -> client, to one client. The player is about to change zones. The client checks the map against its + // INVALIDMAPTRANSFERLIST (civilians only, when bCheckTransferAllowed). The optional fields are written only when + // they differ from the defaults below (the client's own invalid clone and map are 0). + struct TransferToZone : public NetGameMsg { + TransferToZone() : NetGameMsg(MessageType::Game::TRANSFER_TO_ZONE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bCheckTransferAllowed{ false }; + LWOCLONEID cloneID{ 0 }; // optional + float posX{ std::numeric_limits::max() }; // optional + float posY{ std::numeric_limits::max() }; // optional + float posZ{ std::numeric_limits::max() }; // optional + float rotW{ 1.0f }; // optional + float rotX{ 0.0f }; // optional + float rotY{ 0.0f }; // optional + float rotZ{ 0.0f }; // optional + std::u16string spawnPoint{}; // u32 length prefixed + uint8_t ucInstanceType{}; + LWOMAPID zoneID{ 0 }; // optional + }; + + // Server -> client, to one client. The transfer was checked: with no queue the client pauses the player's + // controls, shows the target zone's loading screen and leaves gameplay; with a queue it cancels the launch + // (LWOCharacterComponent, case TransferToZoneCheckedIM). Same layout as TransferToZone. + struct TransferToZoneCheckedIM : public NetGameMsg { + TransferToZoneCheckedIM() : NetGameMsg(MessageType::Game::TRANSFER_TO_ZONE_CHECKED_IM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bIsThereaQueue{ false }; + LWOCLONEID cloneID{ 0 }; // optional + float posX{ std::numeric_limits::max() }; // optional + float posY{ std::numeric_limits::max() }; // optional + float posZ{ std::numeric_limits::max() }; // optional + float rotW{ 1.0f }; // optional + float rotX{ 0.0f }; // optional + float rotY{ 0.0f }; // optional + float rotZ{ 0.0f }; // optional + std::u16string spawnPoint{}; // u32 length prefixed + uint8_t ucInstanceType{}; + LWOMAPID zoneID{ 0 }; // optional + }; + + /** + * What live sent right before TRANSFER_TO_WORLD when a player launched to another zone (131 captured launches): + * player flag 32 set, TransferToZone (transfer check on), TransferToZoneCheckedIM (no queue), flag 32 cleared. + * No position or rotation; the clone only for properties. + */ + void SendZoneTransferNotice(LWOOBJID player, LWOMAPID zoneID, LWOCLONEID cloneID, const std::u16string& spawnPoint, const SystemAddress& sysAddr); + // Client -> server. The client finished loading the zone and its player. struct PlayerLoaded : public NetGameMsg { PlayerLoaded() : NetGameMsg(MessageType::Game::PLAYER_LOADED) {} diff --git a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp index ac96042c1..a4e6d0cd1 100644 --- a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp @@ -78,3 +78,40 @@ TEST_F(LoadSequenceTests, UnreadMailIsAnnouncedOnLoad) { "53 05 00 31 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 " "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00"), FromCapture(sent[0])); } + +// Live, right before TRANSFER_TO_WORLD on a rocket launch to Nimbus Station (spawn point MedPropLand): flag 32 on, +// TransferToZone, TransferToZoneCheckedIM, flag 32 off, all to the launching client. Bytes: live with the player ID +// replaced (the live UpdatePlayerStatistic between the first two is left out). +TEST_F(LoadSequenceTests, ZoneTransferNoticeMatchesLive) { + const auto sent = Capture([&] { GameMessages::SendZoneTransferNotice(PLAYER, 1200, 0, u"MedPropLand", ClientAddress()); }); + ASSERT_EQ(sent.size(), 4u); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 d8 01 90 00 00 00 00", 177), FromCapture(sent[0])); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 04 02 80 05 80 00 00 26 80 32 80 32 00 28 00 39 00 37 80 38 00 " + "26 00 30 80 37 00 32 00 00 6c 01 00", 386), FromCapture(sent[1])); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 05 02 00 05 80 00 00 26 80 32 80 32 00 28 00 39 00 37 80 38 00 " + "26 00 30 80 37 00 32 00 00 6c 01 00", 386), FromCapture(sent[2])); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 d8 01 10 00 00 00 00", 177), FromCapture(sent[3])); + for (const auto& packet : sent) { + EXPECT_EQ(packet.sysAddr, ClientAddress()); + EXPECT_FALSE(packet.broadcast); + } +} + +// A property launch carries the clone; the messages read back to what was sent. +TEST_F(LoadSequenceTests, ZoneTransferCarriesThePropertyClone) { + const auto sent = Capture([&] { GameMessages::SendZoneTransferNotice(PLAYER, 1250, 545173, u"", ClientAddress()); }); + const auto transfers = SentGameMessages(sent); + const auto checked = SentGameMessages(sent); + ASSERT_EQ(transfers.size(), 1u); + ASSERT_EQ(checked.size(), 1u); + EXPECT_TRUE(transfers[0].bCheckTransferAllowed); + EXPECT_EQ(transfers[0].cloneID, 545173u); + EXPECT_EQ(transfers[0].zoneID, 1250); + EXPECT_FALSE(checked[0].bIsThereaQueue); + EXPECT_EQ(checked[0].cloneID, 545173u); + RoundTrip(transfers[0]); + RoundTrip(checked[0]); + ExpectTruncatedFails(transfers[0]); +}