From eb4d1eabe5e484ad2ae890d2dd1ac3b7b8d53f64 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 13 Oct 2026 09:49:35 -0500 Subject: [PATCH] fix(world): a player's client reloading the zone gets its character and the world again A client that loads the zone again while its player is in the world (the Happy Flower editor reloads the zone in place) sent LEVEL_LOAD_COMPLETE a second time; LoadPlayer returned at once because the player existed, so the client waited forever. Now the server resends the character (CreateCharacter, ServerStates), forgets what it had sent that client, constructs every object again and sends the done-loading sequence at the player's current place. A first load is unchanged. The Happy Flower editor messages (GetEditorLevel, the HF_* messages) are logged when they arrive, until the server handles them. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/Character/GhostComponent.h | 3 ++ dGame/dGameMessages/GameMessageHandler.cpp | 6 +++- dWorldServer/WorldServer.cpp | 36 +++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/Character/GhostComponent.h b/dGame/dComponents/Character/GhostComponent.h index c66916b59..d61893e19 100644 --- a/dGame/dComponents/Character/GhostComponent.h +++ b/dGame/dComponents/Character/GhostComponent.h @@ -44,6 +44,9 @@ public: void GhostEntity(const LWOOBJID id); + // The client dropped every object it had (it reloaded the zone in place): nothing counts as sent to it any more + void ForgetObservedEntities() { m_ObservedEntities.clear(); m_LimboConstructions.clear(); } + bool OnToggleGMInvis(GameMessages::ToggleGMInvisEvent& event); bool OnGetGMInvis(GameMessages::GetGMInvis& msg); diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 6f3d16a03..5caa13ef9 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -268,7 +268,11 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System return; } - LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); + // The Happy Flower editor's messages, logged always until the server handles them (what a client sends while editing) + const bool isHappyFlower = messageID == MessageType::Game::GET_EDITOR_LEVEL + || (messageID >= MessageType::Game::HF_ATTRIBUTES_PUSH && messageID <= MessageType::Game::HF_GETS_WANT_PATH); + if (isHappyFlower) LOG("Received unhandled Happy Flower GM: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); + else LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); } std::unique_ptr GameMessageHandler::CreateReceived(MessageType::Game messageID) { diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index f23f57056..427552fdb 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -60,6 +60,7 @@ #include "ZoneInstanceManager.h" #include "dChatFilter.h" #include "ClientPackets.h" +#include "GhostComponent.h" #include "CharacterComponent.h" #include "EntityManager.h" @@ -219,6 +220,7 @@ void HandleMasterPacket(Packet* packet); void HandlePacket(Packet* packet); void CleanupDisconnectedUser(const SystemAddress& sysAddr); void LoadPlayer(const SystemAddress& sysAddr); +void ReloadPlayerInPlace(const SystemAddress& sysAddr, Entity& player, Character& character); int main(int argc, char** argv) { const auto curTimeStr = std::to_string(time(nullptr)); @@ -1018,13 +1020,45 @@ void HandleMasterPacket(Packet* packet) { // Creates the player's entity and sends the client everything in the world: after the client loaded the zone // (LEVEL_LOAD_COMPLETE), or at once when it kept its scene (an experimental seamless migration) +// A player already in the world whose client reloaded the zone: its character again (from its current save), every +// object again and the done-loading sequence, as on a first load. Nothing is created or restarted on the server. +void ReloadPlayerInPlace(const SystemAddress& sysAddr, Entity& player, Character& character) { + LOG("Reloading %s (%llu) in place: its client loaded the zone again", character.GetName().c_str(), player.GetObjectID()); + auto* const characterComponent = player.GetComponent(); + if (!characterComponent) return; + + character.SaveXMLToDatabase(); + ClientPackets::CreateCharacter createCharacter; + createCharacter.objectID = player.GetObjectID(); + createCharacter.xmlData = character.GetXMLData(); + createCharacter.name = GeneralUtils::ASCIIToUTF16(character.GetName()); + createCharacter.gmLevel = character.GetGMLevel(); + createCharacter.chatMode = static_cast(character.GetGMLevel()); + createCharacter.reputation = characterComponent->GetReputation(); + createCharacter.propertyCloneID = character.GetPropertyCloneID(); + createCharacter.Send(sysAddr); + ClientPackets::ServerStates serverStates; + serverStates.Send(sysAddr); + + if (auto* const ghostComponent = player.GetComponent()) ghostComponent->ForgetObservedEntities(); + Game::entityManager->ConstructAllEntities(sysAddr); + + const auto respawnPoint = character.GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID()); + GameMessages::SendDoneLoading(player.GetObjectID(), respawnPoint, player.GetPosition(), player.GetRotation(), sysAddr); +} + void LoadPlayer(const SystemAddress& sysAddr) { Profiler::Scope scope("LoadPlayer"); User* user = UserManager::Instance()->GetUser(sysAddr); if (user) { Character* c = user->GetLastUsedChar(); if (c != nullptr) { - if (Game::entityManager->GetEntity(c->GetObjectID())) return; + // The client loaded the zone again while its player is here (the Happy Flower editor reloads the zone in + // place): it dropped every object, so send its character and the world again + if (auto* const existing = Game::entityManager->GetEntity(c->GetObjectID())) { + ReloadPlayerInPlace(sysAddr, *existing, *c); + return; + } std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); Game::server->GetReplicaManager()->AddParticipant(sysAddr);