feat(transfer): rocket launches send TransferToZone before TRANSFER_TO_WORLD

Live sent, right before TRANSFER_TO_WORLD on every rocket launch (131
captured): NotifyClientFlagChange(32, true), TransferToZone
(check_transfer_allowed, the target map, the landing spawn point, the
clone only for properties, no position or rotation),
TransferToZoneCheckedIM (no queue) and NotifyClientFlagChange(32,
false). DLU went straight to TRANSFER_TO_WORLD.

In the client TransferToZone runs the civilian INVALIDMAPTRANSFERLIST
check, and TransferToZoneCheckedIM pauses the player's controls, starts
the target zone's loading screen and leaves the gameplay state
(LWOCharacterComponent, cases TransferToZone and
TransferToZoneCheckedIM). They are sent from the transfer callback, so
a failed zone request never leaves the client paused. Instance exits
through a message box used TransferToLastNonInstance in live instead
and are not changed here.

Check: launch rockets to Nimbus Station, Gnarled Forest and your
property: controls lock and the destination's loading screen shows as
the rocket leaves, and you land at the usual spawn point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 03:39:06 -05:00
parent 41ecf9d5d8
commit 04c5c4f310
4 changed files with 171 additions and 0 deletions

View File

@@ -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<GameMessages::TransferToZone>(sent);
const auto checked = SentGameMessages<GameMessages::TransferToZoneCheckedIM>(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]);
}