fix(master): a rocket launch preps the clone the player is going to

Launching to a property started an extra clone 0 instance of the zone besides the property's own, because
the prep only carried the zone. PrepZone now carries the clone when there is one (written only then, so a
plain prep is unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 23:11:20 -05:00
parent 1147b61fcb
commit 79d69bf4ac
6 changed files with 30 additions and 7 deletions

View File

@@ -71,8 +71,9 @@ void RocketLaunchpadControlComponent::Launch(Entity* originator, LWOMAPID mapId,
return;
}
// we have the ability to launch, so now we prep the zone
TellMasterToPrepZone(zone);
// we have the ability to launch, so now we prep the zone: the clone the player is going to (a property), not an extra
// clone 0 instance nobody asked for
TellMasterToPrepZone(zone, cloneId == LWOCLONEID_INVALID ? 0 : cloneId);
// Achievement unlocked: "All zones unlocked"
if (!m_AltLandingScene.empty() && m_AltPrecondition->Check(originator)) {
@@ -142,9 +143,10 @@ LWOCLONEID RocketLaunchpadControlComponent::GetSelectedCloneId(LWOOBJID player)
return index->second;
}
void RocketLaunchpadControlComponent::TellMasterToPrepZone(int zoneID) {
void RocketLaunchpadControlComponent::TellMasterToPrepZone(int zoneID, LWOCLONEID cloneID) {
MasterPackets::PrepZone request;
request.zoneID = zoneID;
request.cloneID = cloneID;
MasterPackets::SendToMaster(request);
}

View File

@@ -140,7 +140,7 @@ private:
* Notifies the master server to prepare some world for a player to be able to travel to it
* @param zoneID the ID of the zone to prepare
*/
void TellMasterToPrepZone(int zoneID);
void TellMasterToPrepZone(int zoneID, LWOCLONEID cloneID = 0);
};
#endif // ROCKETLAUNCHPADCONTROLCOMPONENT_H

View File

@@ -899,8 +899,8 @@ namespace {
if (shutdownSequenceStarted) {
LOG("Shutdown sequence has been started. Not prepping a new zone.");
} else {
LOG("Prepping zone %i", zoneID);
Game::im->GetInstance(zoneID, false, 0);
LOG("Prepping zone %i clone %u", zoneID, request.cloneID);
Game::im->GetInstance(zoneID, false, request.cloneID);
}
}

View File

@@ -201,10 +201,13 @@ namespace MasterPackets {
void PrepZone::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(zoneID);
if (cloneID != 0) bitStream.Write(cloneID);
}
bool PrepZone::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(zoneID));
cloneID = 0;
if (bitStream.GetNumberOfUnreadBits() >= 32) VALIDATE_READ(bitStream.Read(cloneID));
return true;
}

View File

@@ -190,9 +190,11 @@ namespace MasterPackets {
bool Deserialize(RakNet::BitStream& bitStream) override;
};
// World -> master: start an instance of zoneID now, players are about to ask for it
// World -> master: start an instance of zoneID now, players are about to ask for it. cloneID: the clone they will ask
// for (a property), written only when not 0 so a plain prep stays as it always was
struct PrepZone : public LUBitStream {
int32_t zoneID{};
uint32_t cloneID{};
PrepZone() : LUBitStream(ServiceType::MASTER, MessageType::Master::PREP_ZONE) {}
void Serialize(RakNet::BitStream& bitStream) const override;

View File

@@ -290,6 +290,22 @@ TEST(MasterPacketsTests, WorldStateMatchesLegacy) {
RoundTrip(prep);
ExpectTruncatedFails(prep);
}
{
// A property's clone rides after the zone; a prep without one reads as clone 0
MasterPackets::PrepZone prep;
prep.zoneID = 1251;
prep.cloneID = 2290;
RakNet::BitStream stream;
prep.Serialize(stream);
MasterPackets::PrepZone read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.zoneID, 1251);
EXPECT_EQ(read.cloneID, 2290u);
RakNet::BitStream plain;
plain.Write<int32_t>(1100);
ASSERT_TRUE(read.Deserialize(plain));
EXPECT_EQ(read.cloneID, 0u);
}
for (const auto requestID : g_RequestIds) {
MasterPackets::AffirmTransferRequest request;