From 87ce8e3e45c8980138e84963ac48df9da7392a72 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 03:29:39 -0500 Subject: [PATCH] fix(load): the respawn checkpoint follows ServerDoneLoadingAllObjects on every load Live sent PlayerReachedRespawnCheckpoint right after ServerDoneLoadingAllObjects on 143 of 157 captured loads, with a rotation: the spawn point the player arrived at, or a checkpoint they had reached before. DLU sent it before constructing the zone's objects, only when a checkpoint was saved for the zone, and always unrotated. It now goes right after ServerDoneLoadingAllObjects on every load: the saved checkpoint when there is one (DLU saves no rotation for it, so that one stays unrotated), otherwise the position and facing the player loaded in at. Inferred: that the loaded-in spot matches live's spawn point choice (live's is the spawn point object, a few tenths of a unit from the player's start). Check: log in fresh to a zone, die before touching any checkpoint and respawn: you come back where you arrived, facing the same way; reach a checkpoint, change zones and come back, die: you respawn at that checkpoint. Co-Authored-By: Claude Opus 5.5 --- dGame/dGameMessages/ZoneMessages.cpp | 14 ++++++++++ dGame/dGameMessages/ZoneMessages.h | 8 ++++++ dWorldServer/WorldServer.cpp | 14 ++-------- .../dGameMessagesTests/LoadSequenceTests.cpp | 28 +++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/dGame/dGameMessages/ZoneMessages.cpp b/dGame/dGameMessages/ZoneMessages.cpp index 7230db842..68727cf76 100644 --- a/dGame/dGameMessages/ZoneMessages.cpp +++ b/dGame/dGameMessages/ZoneMessages.cpp @@ -1,4 +1,5 @@ #include "ZoneMessages.h" +#include "MovementMessages.h" #include "BitStreamUtils.h" #include "CDClientManager.h" @@ -35,6 +36,19 @@ namespace GameMessages { return bitStream.Read(playerID); } + void SendDoneLoading(const LWOOBJID player, const NiPoint3& savedCheckpoint, const NiPoint3& spawnPosition, const NiQuaternion& spawnRotation, const SystemAddress& sysAddr) { + ServerDoneLoadingAllObjects doneLoading; + doneLoading.target = player; + doneLoading.SendToClient(sysAddr); + + PlayerReachedRespawnCheckpoint checkpoint; + checkpoint.target = player; + const bool hasSavedCheckpoint = savedCheckpoint != NiPoint3Constant::ZERO; + checkpoint.pos = hasSavedCheckpoint ? savedCheckpoint : spawnPosition; + checkpoint.rot = hasSavedCheckpoint ? QuatUtils::IDENTITY : spawnRotation; + checkpoint.SendToClient(sysAddr); + } + void SendPlayerReady(const LWOOBJID player, const LWOOBJID zoneControl, const SystemAddress& sysAddr) { PlayerReady playerReady; playerReady.target = player; diff --git a/dGame/dGameMessages/ZoneMessages.h b/dGame/dGameMessages/ZoneMessages.h index d5f82d855..7743a8d71 100644 --- a/dGame/dGameMessages/ZoneMessages.h +++ b/dGame/dGameMessages/ZoneMessages.h @@ -47,6 +47,14 @@ namespace GameMessages { ServerDoneLoadingAllObjects() : NetGameMsg(MessageType::Game::SERVER_DONE_LOADING_ALL_OBJECTS) {} }; + /** + * Ends a player's load the way live did: ServerDoneLoadingAllObjects, then PlayerReachedRespawnCheckpoint with where + * the player respawns in this zone (143 of 157 captured loads send the pair back to back, with a rotation). + * That is their saved checkpoint for the zone when they have one (DLU saves no rotation for it, so it goes + * unrotated), otherwise the spot and facing they loaded in at. + */ + void SendDoneLoading(LWOOBJID player, const NiPoint3& savedCheckpoint, const NiPoint3& spawnPosition, const NiQuaternion& spawnRotation, const SystemAddress& sysAddr); + // Server -> client, to one client. struct InvalidZoneTransferList : public NetGameMsg { InvalidZoneTransferList() : NetGameMsg(MessageType::Game::INVALID_ZONE_TRANSFER_LIST) {} diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index af7f2a84a..9dd673c96 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1051,17 +1051,11 @@ void LoadPlayer(const SystemAddress& sysAddr) { serverStates.Send(sysAddr); const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID()); + const auto spawnPosition = player->GetPosition(); + const auto spawnRotation = player->GetRotation(); Game::entityManager->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS); - if (respawnPoint != NiPoint3Constant::ZERO) { - GameMessages::PlayerReachedRespawnCheckpoint respawnCheckpoint; - respawnCheckpoint.target = player->GetObjectID(); - respawnCheckpoint.pos = respawnPoint; - respawnCheckpoint.rot = QuatUtils::IDENTITY; - respawnCheckpoint.SendToClient(player->GetSystemAddress()); - } - // Before the models are constructed: the served meshes' checksums, so a client whose cached checksum is its own // build's downloads the served mesh again (UgcManifest, docs/UgcServer.md) if (g_CloneID != 0 && UgcManifest::ServesModels()) { @@ -1141,9 +1135,7 @@ void LoadPlayer(const SystemAddress& sysAddr) { invalidTransferList.bCustomerFeedbackOnExit = false; invalidTransferList.bCustomerFeedbackOnInvalidMapTransfer = false; invalidTransferList.SendToClient(sysAddr); - GameMessages::ServerDoneLoadingAllObjects doneLoading; - doneLoading.target = player->GetObjectID(); - doneLoading.SendToClient(sysAddr); + GameMessages::SendDoneLoading(player->GetObjectID(), respawnPoint, spawnPosition, spawnRotation, sysAddr); //Send the player it's mail count: //update: this might not be needed so im going to try disabling this here. diff --git a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp index 5fc5ff456..fb88cf906 100644 --- a/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/LoadSequenceTests.cpp @@ -1,6 +1,7 @@ #include "GameDependencies.h" #include "GameMessageTestUtils.h" +#include "MovementMessages.h" #include "ZoneMessages.h" #include @@ -35,3 +36,30 @@ TEST_F(LoadSequenceTests, PlayerReadyWithoutZoneControlGoesToThePlayerOnly) { ASSERT_EQ(sent.size(), 1u); EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 fd 01"), FromCapture(sent[0])); } + +// Live: ServerDoneLoadingAllObjects, then right after it the respawn checkpoint with a rotation. Bytes: a live pair +// from Avant Gardens Survival (a first load, so the spawn point) with the player ID replaced. +TEST_F(LoadSequenceTests, RespawnCheckpointFollowsDoneLoading) { + const NiPoint3 spawn(35.218f, 365.7804f, -201.3283f); + const NiQuaternion facing(0.7015f, 0.0f, -0.7126f, 0.0f); + const auto sent = Capture([&] { GameMessages::SendDoneLoading(PLAYER, NiPoint3Constant::ZERO, spawn, facing, ClientAddress()); }); + ASSERT_EQ(sent.size(), 2u); + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 6a 06"), FromCapture(sent[0])); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 10 05 3b df 0c 42 e4 e3 b6 43 0b 54 49 c3 c0 ca 99 9f 80 00 00 00 " + "7a 36 1b 5f 80 00 00 00 00", 369), FromCapture(sent[1])); + for (const auto& packet : sent) { + EXPECT_EQ(packet.sysAddr, ClientAddress()); + EXPECT_FALSE(packet.broadcast); + } +} + +// A saved checkpoint wins over where the player loaded in; DLU keeps no rotation for it. +TEST_F(LoadSequenceTests, SavedCheckpointIsSentUnrotated) { + const NiPoint3 saved(100.0f, 200.0f, 300.0f); + const auto sent = Capture([&] { GameMessages::SendDoneLoading(PLAYER, saved, NiPoint3(1.0f, 2.0f, 3.0f), NiQuaternion(0.7015f, 0.0f, -0.7126f, 0.0f), ClientAddress()); }); + const auto checkpoints = SentGameMessages(sent); + ASSERT_EQ(checkpoints.size(), 1u); + EXPECT_EQ(checkpoints[0].pos, saved); + EXPECT_EQ(checkpoints[0].rot, QuatUtils::IDENTITY); +}