diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index b77e7f86d..8c1ab2369 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -4,6 +4,7 @@ */ #include "GameMessageHandler.h" +#include "WorldMigration.h" #include "MissionComponent.h" #include "BitStreamUtils.h" #include "dServer.h" @@ -230,6 +231,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System character->OnZoneLoad(); } + // Moved here from another instance: put back what their save doesn't keep (the pet that was out) + WorldMigration::OnPlayerLoaded(entity); + LOG("Player %s (%llu) loaded.", entity->GetCharacter()->GetName().c_str(), entity->GetObjectID()); // After we've done our thing, tell the client they're ready diff --git a/dGame/dUtilities/CMakeLists.txt b/dGame/dUtilities/CMakeLists.txt index 2202fddc1..f3ce5ec07 100644 --- a/dGame/dUtilities/CMakeLists.txt +++ b/dGame/dUtilities/CMakeLists.txt @@ -6,7 +6,8 @@ set(DGAME_DUTILITIES_SOURCES "BrickDatabase.cpp" "ObjectIDManager.cpp" "Preconditions.cpp" "SlashCommandHandler.cpp" - "VanityUtilities.cpp") + "VanityUtilities.cpp" + "WorldMigration.cpp") add_subdirectory(SlashCommands) diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index e4782db49..e47c082fb 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -6,6 +6,7 @@ #include "SlashCommandHandler.h" +#include "WorldMigration.h" #include #include @@ -1478,4 +1479,19 @@ void SlashCommandHandler::Startup() { .handle = DEVGMCommands::Despawn, .requiredLevel = eGameMasterLevel::DEVELOPER }); + RegisterCommand({ + .help = "Moves everyone here to a fresh instance of this zone", + .info = "Starts a fresh instance of this zone (from the world server binary on disk now, so an update takes effect), moves everyone here to it with the game's Mythran maintenance warning and a short loading screen, then shuts this instance down. Usage: /replaceinstance [warn seconds, 0-300, default 10] [seamless (experimental: no loading screen)]", + .aliases = {"replaceinstance"}, + .handle = WorldMigration::ReplaceInstanceCommand, + .requiredLevel = eGameMasterLevel::DEVELOPER + }); + + RegisterCommand({ + .help = "Moves everyone here into another instance of this zone", + .info = "Moves everyone here into another running instance of this zone (0 or nothing picks the best fit), with the game's Mythran maintenance warning and a short loading screen, then shuts this instance down. Usage: /mergeinstance [target instance] [warn seconds, 0-300, default 10] [seamless (experimental: no loading screen)]", + .aliases = {"mergeinstance"}, + .handle = WorldMigration::MergeInstanceCommand, + .requiredLevel = eGameMasterLevel::DEVELOPER + }); } diff --git a/dGame/dUtilities/WorldMigration.cpp b/dGame/dUtilities/WorldMigration.cpp new file mode 100644 index 000000000..5ee7f27ae --- /dev/null +++ b/dGame/dUtilities/WorldMigration.cpp @@ -0,0 +1,419 @@ +#include "WorldMigration.h" + +#include +#include +#include + +#include "BitStreamUtils.h" +#include "ChatPackets.h" +#include "GeneralUtils.h" +#include "Character.h" +#include "CharacterComponent.h" +#include "Entity.h" +#include "EntityManager.h" +#include "GameMessageHandler.h" +#include "Game.h" +#include "InventoryComponent.h" +#include "Logger.h" +#include "MessageType/Game.h" +#include "MessageType/Master.h" +#include "PetComponent.h" +#include "PlayerManager.h" +#include "ServiceType.h" +#include "TradingManager.h" +#include "WorldPackets.h" +#include "dServer.h" +#include "eServerDisconnectIdentifiers.h" + +using namespace InstanceMigration; + +namespace { + // Players who are dead or building wait this long to finish before they go anyway + constexpr float MAX_WAIT_SECONDS = 15.0f; + // A client that got its transfer and is still connected after this is dropped (it is saved already) + constexpr float LEAVE_TIMEOUT_SECONDS = 20.0f; + // Nobody left to move for this long: done (players still loading in finish first) + constexpr float EMPTY_SECONDS = 3.0f; + // Give up on a migration after this long moving; whoever is left stays (or is saved when the world shuts down) + constexpr float MAX_MOVE_SECONDS = 150.0f; + constexpr float STATUS_INTERVAL = 1.0f; + // Carried state for a player who never arrives + constexpr float CARRIED_EXPIRY_SECONDS = 300.0f; + + struct Leaving { + LWOOBJID characterId{}; + float since{}; + }; + + struct Active { + MigratePlayersOrder order; + float warnLeft{}; + float moving{}; + float budget{}; + float statusIn{}; + float emptyFor{}; + uint16_t moved{}; + uint16_t failed{}; + std::map waiting; // players who can't go yet, and for how long + }; + + std::optional g_Active; + std::map g_Leaving; + std::map> g_Carried; + std::function g_Cleanup; + std::map g_ToldRequester; // the last state the GM who asked was told, per migration + + void SendStatus(uint32_t migrationId, uint32_t targetInstance, eState state, uint16_t moved, uint16_t failed, uint16_t remaining, const std::string& message) { + MigrationStatus status; + status.migrationId = migrationId; + status.state = state; + status.zoneId = Game::server->GetZoneID(); + status.sourceInstance = Game::server->GetInstanceID(); + status.targetInstance = targetInstance; + status.moved = moved; + status.failed = failed; + status.remaining = remaining; + status.message = message; + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::MIGRATE_STATUS); + status.Serialize(bitStream); + Game::server->SendToMaster(bitStream); + } + + void SendStatus(eState state, uint16_t remaining, const std::string& message) { + if (g_Active) SendStatus(g_Active->order.migrationId, g_Active->order.targetInstance, state, g_Active->moved, g_Active->failed, remaining, message); + } + + void WriteWString(RakNet::BitStream& bitStream, const std::u16string& text) { + bitStream.Write(text.size()); + for (const auto character : text) bitStream.Write(character); + } + + // LocalizedAnnouncementServerToSingleClient: the client looks both strings up in its locale (falling back to + // the text itself) and shows the announcement popup, like its own instance-lock warning did in live + void SendLocalizedAnnouncement(Entity* player, const std::u16string& body, const std::u16string& title) { + const auto& sysAddr = player->GetSystemAddress(); + CBITSTREAM; + CMSGHEADER; + bitStream.Write(player->GetObjectID()); + bitStream.Write(MessageType::Game::LOCALIZED_ANNOUNCEMENT_SERVER_TO_SINGLE_CLIENT); + bitStream.Write(0); // body parameters (LDF), none + bitStream.Write(false); // force open chat box + bitStream.Write(true); // show the announcement + bitStream.Write(true); // and put the text in chat + WriteWString(bitStream, body); + WriteWString(bitStream, title); + bitStream.Write(0); // title parameters (LDF), none + SEND_PACKET; + } + + uint16_t Remaining() { + uint16_t remaining = 0; + for (auto* player : PlayerManager::GetAllPlayers()) { + if (player && !g_Leaving.contains(player->GetSystemAddress())) remaining++; + } + return remaining; + } + + // Save, lock and send one player over. Returns false when they couldn't be sent. + bool MovePlayer(Entity* player) { + auto* character = player->GetCharacter(); + if (!character) return false; + const auto& order = g_Active->order; + const auto sysAddr = player->GetSystemAddress(); + const auto playerId = player->GetObjectID(); + + // Nothing changes hands in a cancelled trade + if (const auto& trade = TradingManager::Instance()->GetPlayerTrade(playerId)) { + TradingManager::Instance()->CancelTrade(playerId, trade->GetTradeId()); + } + + CarriedPlayerState carried; + carried.targetZone = order.targetZone; + carried.targetInstance = order.targetInstance; + carried.characterId = playerId; + carried.seamless = order.seamless; + if (auto* pet = PetComponent::GetActivePet(playerId)) carried.petItemId = pet->GetItemId(); + + // Same zone, so they spawn where they stood (Entity.cpp only uses a spawn point when the zone changes) + character->SetZoneID(order.targetZone); + character->SetZoneInstance(order.targetInstance); + character->SetZoneClone(order.targetClone); + character->SetTargetScene(""); + if (auto* characterComponent = player->GetComponent()) characterComponent->SetLastRocketConfig(u""); + character->SaveXMLToDatabase(); + + // From here on nothing they do counts: the target loads what was just saved + g_Leaving[sysAddr] = { playerId, 0.0f }; + + { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::MIGRATE_PLAYER_STATE); + carried.Serialize(bitStream); + Game::server->SendToMaster(bitStream); + } + + // Seamless: take every object we sent away first. The client deletes them but keeps its own player object when + // that ghost goes (LWOGhostComponent::OnGhostReceiveDestruction), and the target's construction of the + // player is adopted by it. Same ordered channel as the transfer, so these arrive before it. + if (order.seamless) Game::entityManager->DestructAllEntities(sysAddr); + + WorldPackets::SendTransferToWorld(sysAddr, order.targetIp, order.targetPort, order.mythranShift); + LOG("Migration %u: sent %s (%llu) to instance %u", order.migrationId, character->GetName().c_str(), playerId, order.targetInstance); + return true; + } + + void MovePlayers(float deltaTime) { + auto& active = *g_Active; + active.budget = std::min(active.budget + active.order.playersPerSecond * deltaTime, static_cast(active.order.playersPerSecond)); + // Copy: moving a player can change the list (a cancelled trade, a pet) + const auto players = PlayerManager::GetAllPlayers(); + for (auto* player : players) { + if (!player || g_Leaving.contains(player->GetSystemAddress())) continue; + if (active.budget < 1.0f) break; + auto* character = player->GetCharacter(); + const bool inTrade = TradingManager::Instance()->GetPlayerTrade(player->GetObjectID()) != nullptr; + auto& waited = active.waiting[player->GetObjectID()]; + const auto decision = DecidePlayer(inTrade, character && character->GetBuildMode(), player->GetIsDead(), waited, MAX_WAIT_SECONDS); + if (decision == ePlayerDecision::WAIT) { + waited += deltaTime; + continue; + } + active.budget -= 1.0f; + active.waiting.erase(player->GetObjectID()); + if (MovePlayer(player)) active.moved++; + else active.failed++; + } + } + + void DropStuckClients(float deltaTime) { + std::vector stuck; + for (auto& [sysAddr, leaving] : g_Leaving) { + leaving.since += deltaTime; + if (leaving.since > LEAVE_TIMEOUT_SECONDS) stuck.push_back(sysAddr); + } + for (const auto& sysAddr : stuck) { + LOG("Migration: %llu got its transfer but never left; disconnecting it (it is saved)", g_Leaving[sysAddr].characterId); + if (g_Active) g_Active->failed++; + if (g_Cleanup) g_Cleanup(sysAddr); + Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::SERVER_SHUTDOWN); + g_Leaving.erase(sysAddr); + } + } +} + +void WorldMigration::SetCleanupHandler(std::function handler) { + g_Cleanup = std::move(handler); +} + +void WorldMigration::HandleOrder(const MigratePlayersOrder& order) { + if (order.targetPort == 0) { + if (!g_Active || g_Active->order.migrationId != order.migrationId) return; + LOG("Migration %u cancelled; %u player(s) were moved", order.migrationId, g_Active->moved); + SendStatus(eState::FAILED, Remaining(), "Cancelled"); + g_Active.reset(); + return; + } + if (g_Active) { + LOG("Migration %u refused: migration %u is still running here", order.migrationId, g_Active->order.migrationId); + SendStatus(order.migrationId, order.targetInstance, eState::FAILED, 0, 0, Remaining(), "Another migration is running on this instance"); + return; + } + + g_Active = Active{}; + g_Active->order = order; + g_Active->warnLeft = order.warnSeconds; + LOG("Migration %u: moving everyone to %s:%u (zone %u instance %u) in %u s", order.migrationId, order.targetIp.c_str(), order.targetPort, + order.targetZone, order.targetInstance, order.warnSeconds); + if (order.warnSeconds > 0) { + for (auto* player : PlayerManager::GetAllPlayers()) { + if (player) SendLocalizedAnnouncement(player, u"UI_INSTANCE_LOCKED_ANNOUNCE_BODY", u"UI_INSTANCE_LOCKED_ANNOUNCE_TITLE"); + } + } +} + +void WorldMigration::StoreCarriedState(const CarriedPlayerState& state) { + if (state.targetZone != Game::server->GetZoneID() || state.targetInstance != Game::server->GetInstanceID()) return; + g_Carried[state.characterId] = { state, 0.0f }; +} + +void WorldMigration::Update(float deltaTime) { + for (auto it = g_Carried.begin(); it != g_Carried.end();) { + it->second.second += deltaTime; + if (it->second.second > CARRIED_EXPIRY_SECONDS) it = g_Carried.erase(it); + else ++it; + } + DropStuckClients(deltaTime); + if (!g_Active) return; + + auto& active = *g_Active; + if (active.warnLeft > 0.0f) { + active.warnLeft -= deltaTime; + if (active.warnLeft > 0.0f) return; + SendStatus(eState::MOVING, Remaining(), "Moving players"); + } + + active.moving += deltaTime; + MovePlayers(deltaTime); + + const auto remaining = Remaining(); + active.emptyFor = remaining == 0 ? active.emptyFor + deltaTime : 0.0f; + // Done once nobody is left to move and everyone sent away has disconnected (or was dropped) + if (active.emptyFor >= EMPTY_SECONDS && g_Leaving.empty()) { + SendStatus(eState::DONE, 0, std::to_string(active.moved) + " player(s) moved" + + (active.failed ? ", " + std::to_string(active.failed) + " could not be moved" : "")); + LOG("Migration %u done: %u moved, %u failed", active.order.migrationId, active.moved, active.failed); + g_Active.reset(); + return; + } + if (active.moving > MAX_MOVE_SECONDS) { + active.failed += remaining; + SendStatus(eState::DONE, remaining, std::to_string(active.moved) + " player(s) moved; gave up on " + std::to_string(remaining)); + g_Active.reset(); + return; + } + + active.statusIn -= deltaTime; + if (active.statusIn <= 0.0f) { + active.statusIn = STATUS_INTERVAL; + SendStatus(eState::MOVING, remaining, ""); + } +} + +bool WorldMigration::IsLeaving(const SystemAddress& sysAddr) { + return g_Leaving.contains(sysAddr); +} + +void WorldMigration::OnDisconnected(const SystemAddress& sysAddr) { + g_Leaving.erase(sysAddr); +} + +bool WorldMigration::ArrivesSeamlessly(LWOOBJID characterId) { + const auto it = g_Carried.find(characterId); + return it != g_Carried.end() && it->second.first.seamless; +} + +void WorldMigration::OnSeamlessArrival(Entity* player) { + if (!player) return; + const auto playerId = player->GetObjectID(); + const auto sysAddr = player->GetSystemAddress(); + LOG("Player %llu arrived without reloading the zone", playerId); + // The client already finished loading on the old instance and won't send PlayerLoaded again: do what it would + // trigger, once the constructions have had a moment to arrive + player->AddCallbackTimer(1.0f, [playerId, sysAddr]() { + if (!Game::entityManager->GetEntity(playerId)) return; + RakNet::BitStream empty; + GameMessageHandler::HandleMessage(empty, sysAddr, playerId, MessageType::Game::PLAYER_LOADED); + }); +} + +void WorldMigration::OnPlayerLoaded(Entity* player) { + if (!player) return; + const auto it = g_Carried.find(player->GetObjectID()); + if (it == g_Carried.end()) return; + const auto state = it->second.first; + g_Carried.erase(it); + + LOG("Player %llu arrived from a migrated instance", state.characterId); + if (state.petItemId != LWOOBJID_EMPTY) { + auto* inventory = player->GetComponent(); + auto* item = inventory ? inventory->FindItemById(state.petItemId) : nullptr; + if (item) inventory->SpawnPet(item); + } +} + +void WorldMigration::HandleStatus(const MigrationStatus& status) { + if (status.requesterId == LWOOBJID_EMPTY) return; + auto* requester = PlayerManager::GetPlayer(status.requesterId); + if (!requester) return; + // Each change of state once, not every progress report + const auto told = g_ToldRequester.find(status.migrationId); + if (told != g_ToldRequester.end() && told->second == status.state) return; + g_ToldRequester[status.migrationId] = status.state; + if (status.Finished()) g_ToldRequester.erase(status.migrationId); + + std::string text = "Instance " + std::string(KindName(status.kind)) + " (zone " + std::to_string(status.zoneId) + " instance " + + std::to_string(status.sourceInstance) + (status.targetInstance ? " -> " + std::to_string(status.targetInstance) : std::string()) + "): " + StateName(status.state); + if (status.moved || status.failed) text += ", " + std::to_string(status.moved) + " moved, " + std::to_string(status.failed) + " failed"; + if (!status.message.empty()) text += ". " + status.message; + ChatPackets::SendSystemMessage(requester->GetSystemAddress(), GeneralUtils::ASCIIToUTF16(text)); +} + +void WorldMigration::RequestMigration(Entity* requester, eKind kind, uint32_t targetInstance, uint16_t warnSeconds, bool seamless) { + static uint32_t nextId = static_cast(std::time(nullptr)); + InstanceMigrationRequest request; + // Different worlds may ask at the same time; master keys migrations by this + request.requestId = (++nextId << 8) ^ static_cast(Game::server->GetInstanceID()); + request.kind = kind; + request.zoneId = Game::server->GetZoneID(); + request.sourceInstance = Game::server->GetInstanceID(); + request.targetInstance = kind == eKind::MERGE ? targetInstance : 0; + request.warnSeconds = std::min(warnSeconds, InstanceMigrationRequest::MAX_WARN_SECONDS); + request.seamless = seamless; + if (requester) { + request.requesterId = requester->GetObjectID(); + if (auto* character = requester->GetCharacter()) request.requestedBy = character->GetName(); + } + LOG("Asking master to %s this instance (migration %u, requested by %s)", KindName(kind), request.requestId, request.requestedBy.c_str()); + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::INSTANCE_MIGRATE); + request.Serialize(bitStream); + Game::server->SendToMaster(bitStream); +} + +namespace { + // [warn seconds] [seamless] from the words after the first `skip` + bool ParseMoveOptions(const std::vector& words, size_t skip, uint16_t& warnSeconds, bool& seamless) { + warnSeconds = 10; + seamless = false; + if (words.size() > skip) { + const auto warn = GeneralUtils::TryParse(words[skip]); + if (!warn || *warn > InstanceMigrationRequest::MAX_WARN_SECONDS) return false; + warnSeconds = *warn; + } + if (words.size() > skip + 1) { + if (words[skip + 1] != "seamless") return false; + seamless = true; + } + return true; + } +} + +void WorldMigration::ReplaceInstanceCommand(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + const auto words = GeneralUtils::SplitString(args, ' '); + uint16_t warnSeconds{}; + bool seamless{}; + if (!ParseMoveOptions(words, words.size() == 1 && words[0].empty() ? 1 : 0, warnSeconds, seamless)) { + ChatPackets::SendSystemMessage(sysAddr, u"Usage: /replaceinstance [warn seconds, 0-300] [seamless]"); + return; + } + ChatPackets::SendSystemMessage(sysAddr, u"Asking master to replace this instance with a fresh one."); + RequestMigration(entity, eKind::REPLACE, 0, warnSeconds, seamless); +} + +void WorldMigration::MergeInstanceCommand(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + const auto words = GeneralUtils::SplitString(args, ' '); + uint32_t target = 0; + if (!words.empty() && !words[0].empty()) { + const auto parsed = GeneralUtils::TryParse(words[0]); + if (!parsed) { + ChatPackets::SendSystemMessage(sysAddr, u"Usage: /mergeinstance [target instance, 0 for the best fit] [warn seconds, 0-300] [seamless]"); + return; + } + target = *parsed; + } + uint16_t warnSeconds{}; + bool seamless{}; + if (!ParseMoveOptions(words, 1, warnSeconds, seamless)) { + ChatPackets::SendSystemMessage(sysAddr, u"Usage: /mergeinstance [target instance, 0 for the best fit] [warn seconds, 0-300] [seamless]"); + return; + } + if (target == static_cast(Game::server->GetInstanceID())) { + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(Describe(eRefusal::TARGET_IS_SOURCE))); + return; + } + ChatPackets::SendSystemMessage(sysAddr, u"Asking master to merge this instance into " + + (target ? u"instance " + GeneralUtils::ASCIIToUTF16(std::to_string(target)) : std::u16string(u"the best fit")) + u"."); + RequestMigration(entity, eKind::MERGE, target, warnSeconds, seamless); +} diff --git a/dGame/dUtilities/WorldMigration.h b/dGame/dUtilities/WorldMigration.h new file mode 100644 index 000000000..857a5e61a --- /dev/null +++ b/dGame/dUtilities/WorldMigration.h @@ -0,0 +1,64 @@ +#ifndef __WORLDMIGRATION__H__ +#define __WORLDMIGRATION__H__ + +#include + +#include "InstanceMigration.h" +#include "RakNetTypes.h" +#include "dCommonVars.h" + +class Entity; + +/** + * A world server's side of instance migrations (InstanceMigration.h, docs/SeamlessTransfer.md). + * + * As the source: warns everyone with the client's own "Mythran Maintenance Alert", then moves players a few at a + * time. Each is saved (position included), locked (their packets are ignored and they aren't saved again when they + * disconnect, so nothing they do after the save can be lost or duplicated), and sent TRANSFER_TO_WORLD with the + * Mythran shift flag. As the target: puts back what was carried over (the pet that was out) once they have loaded. + * GMs start migrations with /replaceinstance and /mergeinstance. + */ +namespace WorldMigration { + // What a disconnect runs (save and remove the user); used to drop players whose client never left + void SetCleanupHandler(std::function handler); + + // MIGRATE_PLAYERS from master (a target port of 0 cancels the running migration) + void HandleOrder(const MigratePlayersOrder& order); + + // MIGRATE_PLAYER_STATE from master: a player on their way here + void StoreCarriedState(const CarriedPlayerState& state); + + // Every world frame + void Update(float deltaTime); + + // The player was sent away: ignore their packets, and don't save them again when they disconnect + bool IsLeaving(const SystemAddress& sysAddr); + + // Their connection closed; forget them + void OnDisconnected(const SystemAddress& sysAddr); + + // A player finished loading here (PLAYER_LOADED) + void OnPlayerLoaded(Entity* player); + + // Experimental seamless migration: this character is on its way and its client kept the zone loaded, so it must + // not be sent LOAD_STATIC_ZONE (that tears the scene down); load it at once instead + bool ArrivesSeamlessly(LWOOBJID characterId); + + // Its player entity was created and constructed: run what the client's PlayerLoaded would have + void OnSeamlessArrival(Entity* player); + + // MIGRATE_STATUS from master: tell the GM who asked, if they are here + void HandleStatus(const MigrationStatus& status); + + // Ask master to move everyone in this instance; the seam a dashboard or other tool can use too, by sending + // INSTANCE_MIGRATE to master + void RequestMigration(Entity* requester, InstanceMigration::eKind kind, uint32_t targetInstance, uint16_t warnSeconds, bool seamless); + + // /replaceinstance [warn seconds] [seamless] + void ReplaceInstanceCommand(Entity* entity, const SystemAddress& sysAddr, const std::string args); + + // /mergeinstance [target instance, 0 for the best fit] [warn seconds] [seamless] + void MergeInstanceCommand(Entity* entity, const SystemAddress& sysAddr, const std::string args); +} + +#endif //!__WORLDMIGRATION__H__ diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index e4c421bc8..dc04a3fd6 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -83,6 +83,7 @@ #include "InventoryComponent.h" #include "Item.h" #include "eFunnessTypes.h" +#include "WorldMigration.h" namespace Game { Logger* logger = nullptr; @@ -127,6 +128,8 @@ void SendShutdownMessageToMaster(); void HandlePacketChat(Packet* packet); void HandleMasterPacket(Packet* packet); void HandlePacket(Packet* packet); +void CleanupDisconnectedUser(const SystemAddress& sysAddr); +void LoadPlayer(const SystemAddress& sysAddr); int main(int argc, char** argv) { const auto curTimeStr = std::to_string(time(nullptr)); @@ -255,6 +258,7 @@ int main(int argc, char** argv) { &Game::lastSignal, masterPassword, zoneID); + WorldMigration::SetCleanupHandler(CleanupDisconnectedUser); //Connect to the chat server: uint32_t chatPort = GeneralUtils::TryParse(Game::config->GetValue("chat_server_port")).value_or(1501); @@ -434,6 +438,8 @@ int main(int argc, char** argv) { Metrics::StartMeasurement(MetricVariable::UpdateSpawners); Game::zoneManager->Update(deltaTime); Metrics::EndMeasurement(MetricVariable::UpdateSpawners); + + WorldMigration::Update(deltaTime); } Metrics::StartMeasurement(MetricVariable::PacketHandling); @@ -719,7 +725,13 @@ void HandleMasterPacket(Packet* packet) { //Create our user and send them in: UserManager::Instance()->CreateUser(it->second.sysAddr, username.GetAsString(), userHash); - if (Game::zoneManager->HasZone()) { + // Moved here by an experimental seamless migration: the client still has the zone loaded + const auto sysAddr = it->second.sysAddr; + auto* newUser = UserManager::Instance()->GetUser(sysAddr); + const auto* lastCharacter = newUser ? newUser->GetLastUsedChar() : nullptr; + const auto seamlessCharacter = lastCharacter && WorldMigration::ArrivesSeamlessly(lastCharacter->GetObjectID()) ? lastCharacter->GetObjectID() : LWOOBJID_EMPTY; + + if (Game::zoneManager->HasZone() && seamlessCharacter == LWOOBJID_EMPTY) { float x = 0.0f; float y = 0.0f; float z = 0.0f; @@ -750,6 +762,11 @@ void HandleMasterPacket(Packet* packet) { bitStream.Write(g_InstanceID); Game::server->SendToMaster(bitStream); } + + if (seamlessCharacter != LWOOBJID_EMPTY) { + LoadPlayer(sysAddr); + WorldMigration::OnSeamlessArrival(Game::entityManager->GetEntity(seamlessCharacter)); + } } break; @@ -775,6 +792,27 @@ void HandleMasterPacket(Packet* packet) { break; } + case MessageType::Master::MIGRATE_PLAYERS: { + CINSTREAM_SKIP_HEADER; + MigratePlayersOrder order; + if (order.Deserialize(inStream)) WorldMigration::HandleOrder(order); + break; + } + + case MessageType::Master::MIGRATE_PLAYER_STATE: { + CINSTREAM_SKIP_HEADER; + CarriedPlayerState state; + if (state.Deserialize(inStream)) WorldMigration::StoreCarriedState(state); + break; + } + + case MessageType::Master::MIGRATE_STATUS: { + CINSTREAM_SKIP_HEADER; + MigrationStatus status; + if (status.Deserialize(inStream)) WorldMigration::HandleStatus(status); + break; + } + case MessageType::Master::NEW_SESSION_ALERT: { CINSTREAM_SKIP_HEADER; uint32_t sessionKey{}; @@ -803,58 +841,306 @@ 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) +void LoadPlayer(const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (user) { + Character* c = user->GetLastUsedChar(); + if (c != nullptr) { + if (Game::entityManager->GetEntity(c->GetObjectID())) return; + std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); + Game::server->GetReplicaManager()->AddParticipant(sysAddr); + + EntityInfo info{}; + info.lot = 1; + Entity* player = Game::entityManager->CreateEntity(info, UserManager::Instance()->GetUser(sysAddr)); + + auto* characterComponent = player->GetComponent(); + if (!characterComponent) return; + + // Do charxml fixes here + auto* levelComponent = player->GetComponent(); + auto* const inventoryComponent = player->GetComponent(); + auto* const missionComponent = player->GetComponent(); + if (!levelComponent || !missionComponent || !inventoryComponent) return; + + auto version = levelComponent->GetCharacterVersion(); + LOG("Updating character from version %s", StringifiedEnum::ToString(version).data()); + if (version < eCharacterVersion::UP_TO_DATE) { + switch (version) { + case eCharacterVersion::RELEASE: + // TODO: Implement, super low priority + [[fallthrough]]; + case eCharacterVersion::LIVE: + LOG("Updating Character Flags"); + c->SetRetroactiveFlags(); + levelComponent->SetCharacterVersion(eCharacterVersion::PLAYER_FACTION_FLAGS); + [[fallthrough]]; + case eCharacterVersion::PLAYER_FACTION_FLAGS: + LOG("Updating Vault Size"); + player->RetroactiveVaultSize(); + levelComponent->SetCharacterVersion(eCharacterVersion::VAULT_SIZE); + [[fallthrough]]; + case eCharacterVersion::VAULT_SIZE: + LOG("Updaing Speedbase"); + levelComponent->SetRetroactiveBaseSpeed(); + levelComponent->SetCharacterVersion(eCharacterVersion::SPEED_BASE); + [[fallthrough]]; + case eCharacterVersion::SPEED_BASE: { + LOG("Removing lots from NJ Jay missions bugged at foss"); + // https://explorer.lu/missions/1789 + const auto* mission = missionComponent->GetMission(1789); + if (mission && mission->IsComplete()) { + inventoryComponent->RemoveItem(14474, 1, eInventoryType::ITEMS); + inventoryComponent->RemoveItem(14474, 1, eInventoryType::VAULT_ITEMS); + } + // https://explorer.lu/missions/1927 + mission = missionComponent->GetMission(1927); + if (mission && mission->IsComplete()) { + inventoryComponent->RemoveItem(14493, 1, eInventoryType::ITEMS); + inventoryComponent->RemoveItem(14493, 1, eInventoryType::VAULT_ITEMS); + } + levelComponent->SetCharacterVersion(eCharacterVersion::NJ_JAYMISSIONS); + [[fallthrough]]; + } + case eCharacterVersion::NJ_JAYMISSIONS: { + LOG("Fixing Nexus Force Explorer missions"); + auto missions = { 502 /* Pet Cove */, 593/* Nimbus Station */, 938/* Avant Gardens */, 284/* Gnarled Forest */, 754/* Forbidden Valley */ }; + bool complete = true; + for (auto missionID : missions) { + auto* mission = missionComponent->GetMission(missionID); + if (!mission || !mission->IsComplete()) { + complete = false; + } + } + + if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); + levelComponent->SetCharacterVersion(eCharacterVersion::NEXUS_FORCE_EXPLORER); + [[fallthrough]]; + } + case eCharacterVersion::NEXUS_FORCE_EXPLORER: { + LOG("Fixing pet IDs"); + + // First copy the original ids + const auto pets = inventoryComponent->GetPetsMut(); + + // Then clear the pets so we can re-add them with the updated IDs + auto& invPets = inventoryComponent->GetPetsMut(); + invPets.clear(); + for (auto& [id, databasePet] : pets) { + const auto originalID = id; + const auto newId = GeneralUtils::ClearBit(id, 32); // Persistent bit that didn't exist + LOG("New ID %llu", newId); + auto* item = inventoryComponent->FindItemBySubKey(originalID); + if (item) { + LOG("item subkey %llu", item->GetSubKey()); + item->SetSubKey(newId); + invPets[newId] = databasePet; + } + } + levelComponent->SetCharacterVersion(eCharacterVersion::PET_IDS); + [[fallthrough]]; + } + case eCharacterVersion::PET_IDS: { + LOG("Regenerating item ids"); + inventoryComponent->RegenerateItemIDs(); + levelComponent->SetCharacterVersion(eCharacterVersion::INVENTORY_PERSISTENT_IDS); + [[fallthrough]]; + } + case eCharacterVersion::INVENTORY_PERSISTENT_IDS: { + LOG("Fixing racing meta missions"); + missionComponent->FixRacingMetaMissions(); + levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); + [[fallthrough]]; + } + case eCharacterVersion::UP_TO_DATE: + break; + } + } + + // Update the characters xml to ensure the update above is not only saved, but so the client picks up on the changes. + c->SaveXMLToDatabase(); + + // Fix the destroyable component + auto* destroyableComponent = player->GetComponent(); + + if (destroyableComponent != nullptr) { + destroyableComponent->FixStats(); + } + + WorldPackets::SendCreateCharacter(sysAddr, characterComponent->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel(), c->GetPropertyCloneID()); + WorldPackets::SendServerState(sysAddr); + + const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID()); + + Game::entityManager->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS); + + if (respawnPoint != NiPoint3Constant::ZERO) { + GameMessages::SendPlayerReachedRespawnCheckpoint(player, respawnPoint, QuatUtils::IDENTITY); + } + + Game::entityManager->ConstructAllEntities(sysAddr); + + characterComponent->RocketUnEquip(player); + + player->GetCharacter()->SetTargetScene(""); + + //Tell the player to generate BBB models, if any: + if (g_CloneID != 0) { + const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); + + const auto zoneId = worldId.GetMapID(); + const auto cloneId = g_CloneID; + + //Check for BBB models: + auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); + + LWOOBJID propertyId = LWOOBJID_EMPTY; + if (propertyInfo) propertyId = propertyInfo->id; + else { + LOG("Couldn't find property ID for zone %i, clone %i", zoneId, cloneId); + goto noBBB; + } + + // Workaround for not having a UGC server to get model LXFML onto the client so it + // can generate the physics and nif for the object. + + auto bbbModels = Database::Get()->GetUgcModels(propertyId); + if (bbbModels.empty()) { + LOG("No BBB models found for property %llu", propertyId); + goto noBBB; + } + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); + bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes + bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); + bitStream.Write(bbbModels.size()); + for (auto& bbbModel : bbbModels) { + LOG("Getting lxfml ugcID: %llu", bbbModel.id); + + bbbModel.lxfmlData.seekg(0, std::ios::end); + size_t lxfmlSize = bbbModel.lxfmlData.tellg(); + bbbModel.lxfmlData.seekg(0); + + // write data + LWOOBJID blueprintID = bbbModel.id; + bitStream.Write(blueprintID); + bitStream.Write(lxfmlSize); + bitStream.WriteAlignedBytes(reinterpret_cast(bbbModel.lxfmlData.str().c_str()), lxfmlSize); + } + SEND_PACKET; + } + + noBBB: + + // Tell the client it's done loading: + GameMessages::SendInvalidZoneTransferList(player, sysAddr, GeneralUtils::ASCIIToUTF16(Game::config->GetValue("source")), u"", false, false); + GameMessages::SendServerDoneLoadingAllObjects(player, sysAddr); + + //Send the player it's mail count: + //update: this might not be needed so im going to try disabling this here. + //Mail::HandleNotificationRequest(sysAddr, player->GetObjectID()); + + //Notify chat that a player has loaded: + auto* character = player->GetCharacter(); + auto* user = character != nullptr ? character->GetParentUser() : nullptr; + if (user) { + const auto& playerName = character->GetName(); + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::LOGIN_SESSION_NOTIFY); + bitStream.Write(player->GetObjectID()); + bitStream.Write(playerName.size()); + for (size_t i = 0; i < playerName.size(); i++) { + bitStream.Write(playerName[i]); + } + + auto zone = Game::zoneManager->GetZone()->GetZoneID(); + bitStream.Write(zone.GetMapID()); + bitStream.Write(zone.GetInstanceID()); + bitStream.Write(zone.GetCloneID()); + bitStream.Write(user->GetMuteExpire()); + bitStream.Write(player->GetGMLevel()); + + Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + } + } else { + LOG("Couldn't find character to log in with for user %s (%i)!", user->GetUsername().c_str(), user->GetAccountID()); + Game::server->Disconnect(sysAddr, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND); + } + } else { + LOG("Couldn't get user for level load complete!"); + } +} + +// Save and remove a user whose connection closed (or who is being dropped). Safe to call twice: the second call finds +// no user and does nothing. +void CleanupDisconnectedUser(const SystemAddress& sysAddr) { + // Players moved to another instance were saved when they were sent; saving again could overwrite what the + // other instance has saved since + const bool savedByMigration = WorldMigration::IsLeaving(sysAddr); + WorldMigration::OnDisconnected(sysAddr); + + auto user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + + auto c = user->GetLastUsedChar(); + if (!c) { + UserManager::Instance()->DeleteUser(sysAddr); + return; + } + + auto* entity = Game::entityManager->GetEntity(c->GetObjectID()); + + if (!entity) { + entity = PlayerManager::GetPlayer(sysAddr); + } + + if (entity) { + auto* skillComponent = entity->GetComponent(); + + if (skillComponent != nullptr) { + skillComponent->Reset(); + } + + if (!savedByMigration) entity->GetCharacter()->SaveXMLToDatabase(); + + LOG("Deleting player %llu", entity->GetObjectID()); + + Game::entityManager->DestroyEntity(entity); + } + + { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); + bitStream.Write(user->GetLoggedInChar()); + Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + } + + UserManager::Instance()->DeleteUser(sysAddr); + + if (PropertyManagementComponent::Instance() != nullptr) { + PropertyManagementComponent::Instance()->Save(); + } + + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::PLAYER_REMOVED); + bitStream.Write(Game::server->GetZoneID()); + bitStream.Write(g_InstanceID); + Game::server->SendToMaster(bitStream); +} + void HandlePacket(Packet* packet) { if (packet->length < 1) return; if (packet->data[0] == ID_DISCONNECTION_NOTIFICATION || packet->data[0] == ID_CONNECTION_LOST) { - auto user = UserManager::Instance()->GetUser(packet->systemAddress); - if (!user) return; - - auto c = user->GetLastUsedChar(); - if (!c) { - UserManager::Instance()->DeleteUser(packet->systemAddress); - return; - } - - auto* entity = Game::entityManager->GetEntity(c->GetObjectID()); - - if (!entity) { - entity = PlayerManager::GetPlayer(packet->systemAddress); - } - - if (entity) { - auto* skillComponent = entity->GetComponent(); - - if (skillComponent != nullptr) { - skillComponent->Reset(); - } - - entity->GetCharacter()->SaveXMLToDatabase(); - - LOG("Deleting player %llu", entity->GetObjectID()); - - Game::entityManager->DestroyEntity(entity); - } - - { - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::UNEXPECTED_DISCONNECT); - bitStream.Write(user->GetLoggedInChar()); - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); - } - - UserManager::Instance()->DeleteUser(packet->systemAddress); - - if (PropertyManagementComponent::Instance() != nullptr) { - PropertyManagementComponent::Instance()->Save(); - } - - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::MASTER, MessageType::Master::PLAYER_REMOVED); - bitStream.Write(Game::server->GetZoneID()); - bitStream.Write(g_InstanceID); - Game::server->SendToMaster(bitStream); + CleanupDisconnectedUser(packet->systemAddress); } + // Sent to another instance and locked: nothing more from them counts here + if (WorldMigration::IsLeaving(packet->systemAddress)) return; + if (packet->data[0] != ID_USER_PACKET_ENUM || packet->length < 4) return; CINSTREAM; @@ -1029,236 +1315,7 @@ void HandlePacket(Packet* packet) { case MessageType::World::LEVEL_LOAD_COMPLETE: { LOG("Received level load complete from user."); - User* user = UserManager::Instance()->GetUser(packet->systemAddress); - if (user) { - Character* c = user->GetLastUsedChar(); - if (c != nullptr) { - if (Game::entityManager->GetEntity(c->GetObjectID())) return; - std::u16string username = GeneralUtils::ASCIIToUTF16(c->GetName()); - Game::server->GetReplicaManager()->AddParticipant(packet->systemAddress); - - EntityInfo info{}; - info.lot = 1; - Entity* player = Game::entityManager->CreateEntity(info, UserManager::Instance()->GetUser(packet->systemAddress)); - - auto* characterComponent = player->GetComponent(); - if (!characterComponent) return; - - // Do charxml fixes here - auto* levelComponent = player->GetComponent(); - auto* const inventoryComponent = player->GetComponent(); - auto* const missionComponent = player->GetComponent(); - if (!levelComponent || !missionComponent || !inventoryComponent) return; - - auto version = levelComponent->GetCharacterVersion(); - LOG("Updating character from version %s", StringifiedEnum::ToString(version).data()); - if (version < eCharacterVersion::UP_TO_DATE) { - switch (version) { - case eCharacterVersion::RELEASE: - // TODO: Implement, super low priority - [[fallthrough]]; - case eCharacterVersion::LIVE: - LOG("Updating Character Flags"); - c->SetRetroactiveFlags(); - levelComponent->SetCharacterVersion(eCharacterVersion::PLAYER_FACTION_FLAGS); - [[fallthrough]]; - case eCharacterVersion::PLAYER_FACTION_FLAGS: - LOG("Updating Vault Size"); - player->RetroactiveVaultSize(); - levelComponent->SetCharacterVersion(eCharacterVersion::VAULT_SIZE); - [[fallthrough]]; - case eCharacterVersion::VAULT_SIZE: - LOG("Updaing Speedbase"); - levelComponent->SetRetroactiveBaseSpeed(); - levelComponent->SetCharacterVersion(eCharacterVersion::SPEED_BASE); - [[fallthrough]]; - case eCharacterVersion::SPEED_BASE: { - LOG("Removing lots from NJ Jay missions bugged at foss"); - // https://explorer.lu/missions/1789 - const auto* mission = missionComponent->GetMission(1789); - if (mission && mission->IsComplete()) { - inventoryComponent->RemoveItem(14474, 1, eInventoryType::ITEMS); - inventoryComponent->RemoveItem(14474, 1, eInventoryType::VAULT_ITEMS); - } - // https://explorer.lu/missions/1927 - mission = missionComponent->GetMission(1927); - if (mission && mission->IsComplete()) { - inventoryComponent->RemoveItem(14493, 1, eInventoryType::ITEMS); - inventoryComponent->RemoveItem(14493, 1, eInventoryType::VAULT_ITEMS); - } - levelComponent->SetCharacterVersion(eCharacterVersion::NJ_JAYMISSIONS); - [[fallthrough]]; - } - case eCharacterVersion::NJ_JAYMISSIONS: { - LOG("Fixing Nexus Force Explorer missions"); - auto missions = { 502 /* Pet Cove */, 593/* Nimbus Station */, 938/* Avant Gardens */, 284/* Gnarled Forest */, 754/* Forbidden Valley */ }; - bool complete = true; - for (auto missionID : missions) { - auto* mission = missionComponent->GetMission(missionID); - if (!mission || !mission->IsComplete()) { - complete = false; - } - } - - if (complete) missionComponent->CompleteMission(937 /* Nexus Force explorer */); - levelComponent->SetCharacterVersion(eCharacterVersion::NEXUS_FORCE_EXPLORER); - [[fallthrough]]; - } - case eCharacterVersion::NEXUS_FORCE_EXPLORER: { - LOG("Fixing pet IDs"); - - // First copy the original ids - const auto pets = inventoryComponent->GetPetsMut(); - - // Then clear the pets so we can re-add them with the updated IDs - auto& invPets = inventoryComponent->GetPetsMut(); - invPets.clear(); - for (auto& [id, databasePet] : pets) { - const auto originalID = id; - const auto newId = GeneralUtils::ClearBit(id, 32); // Persistent bit that didn't exist - LOG("New ID %llu", newId); - auto* item = inventoryComponent->FindItemBySubKey(originalID); - if (item) { - LOG("item subkey %llu", item->GetSubKey()); - item->SetSubKey(newId); - invPets[newId] = databasePet; - } - } - levelComponent->SetCharacterVersion(eCharacterVersion::PET_IDS); - [[fallthrough]]; - } - case eCharacterVersion::PET_IDS: { - LOG("Regenerating item ids"); - inventoryComponent->RegenerateItemIDs(); - levelComponent->SetCharacterVersion(eCharacterVersion::INVENTORY_PERSISTENT_IDS); - [[fallthrough]]; - } - case eCharacterVersion::INVENTORY_PERSISTENT_IDS: { - LOG("Fixing racing meta missions"); - missionComponent->FixRacingMetaMissions(); - levelComponent->SetCharacterVersion(eCharacterVersion::UP_TO_DATE); - [[fallthrough]]; - } - case eCharacterVersion::UP_TO_DATE: - break; - } - } - - // Update the characters xml to ensure the update above is not only saved, but so the client picks up on the changes. - c->SaveXMLToDatabase(); - - // Fix the destroyable component - auto* destroyableComponent = player->GetComponent(); - - if (destroyableComponent != nullptr) { - destroyableComponent->FixStats(); - } - - WorldPackets::SendCreateCharacter(packet->systemAddress, characterComponent->GetReputation(), player->GetObjectID(), c->GetXMLData(), username, c->GetGMLevel(), c->GetPropertyCloneID()); - WorldPackets::SendServerState(packet->systemAddress); - - const auto respawnPoint = player->GetCharacter()->GetRespawnPoint(Game::zoneManager->GetZone()->GetWorldID()); - - Game::entityManager->ConstructEntity(player, UNASSIGNED_SYSTEM_ADDRESS); - - if (respawnPoint != NiPoint3Constant::ZERO) { - GameMessages::SendPlayerReachedRespawnCheckpoint(player, respawnPoint, QuatUtils::IDENTITY); - } - - Game::entityManager->ConstructAllEntities(packet->systemAddress); - - characterComponent->RocketUnEquip(player); - - player->GetCharacter()->SetTargetScene(""); - - //Tell the player to generate BBB models, if any: - if (g_CloneID != 0) { - const auto& worldId = Game::zoneManager->GetZone()->GetZoneID(); - - const auto zoneId = worldId.GetMapID(); - const auto cloneId = g_CloneID; - - //Check for BBB models: - auto propertyInfo = Database::Get()->GetPropertyInfo(zoneId, cloneId); - - LWOOBJID propertyId = LWOOBJID_EMPTY; - if (propertyInfo) propertyId = propertyInfo->id; - else { - LOG("Couldn't find property ID for zone %i, clone %i", zoneId, cloneId); - goto noBBB; - } - - // Workaround for not having a UGC server to get model LXFML onto the client so it - // can generate the physics and nif for the object. - - auto bbbModels = Database::Get()->GetUgcModels(propertyId); - if (bbbModels.empty()) { - LOG("No BBB models found for property %llu", propertyId); - goto noBBB; - } - - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE); - bitStream.Write(LWOOBJID_EMPTY); //always zero so that a check on the client passes - bitStream.Write(eBlueprintSaveResponseType::EverythingWorked); - bitStream.Write(bbbModels.size()); - for (auto& bbbModel : bbbModels) { - LOG("Getting lxfml ugcID: %llu", bbbModel.id); - - bbbModel.lxfmlData.seekg(0, std::ios::end); - size_t lxfmlSize = bbbModel.lxfmlData.tellg(); - bbbModel.lxfmlData.seekg(0); - - // write data - LWOOBJID blueprintID = bbbModel.id; - bitStream.Write(blueprintID); - bitStream.Write(lxfmlSize); - bitStream.WriteAlignedBytes(reinterpret_cast(bbbModel.lxfmlData.str().c_str()), lxfmlSize); - } - SystemAddress sysAddr = packet->systemAddress; - SEND_PACKET; - } - - noBBB: - - // Tell the client it's done loading: - GameMessages::SendInvalidZoneTransferList(player, packet->systemAddress, GeneralUtils::ASCIIToUTF16(Game::config->GetValue("source")), u"", false, false); - GameMessages::SendServerDoneLoadingAllObjects(player, packet->systemAddress); - - //Send the player it's mail count: - //update: this might not be needed so im going to try disabling this here. - //Mail::HandleNotificationRequest(packet->systemAddress, player->GetObjectID()); - - //Notify chat that a player has loaded: - auto* character = player->GetCharacter(); - auto* user = character != nullptr ? character->GetParentUser() : nullptr; - if (user) { - const auto& playerName = character->GetName(); - - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::LOGIN_SESSION_NOTIFY); - bitStream.Write(player->GetObjectID()); - bitStream.Write(playerName.size()); - for (size_t i = 0; i < playerName.size(); i++) { - bitStream.Write(playerName[i]); - } - - auto zone = Game::zoneManager->GetZone()->GetZoneID(); - bitStream.Write(zone.GetMapID()); - bitStream.Write(zone.GetInstanceID()); - bitStream.Write(zone.GetCloneID()); - bitStream.Write(user->GetMuteExpire()); - bitStream.Write(player->GetGMLevel()); - - Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); - } - } else { - LOG("Couldn't find character to log in with for user %s (%i)!", user->GetUsername().c_str(), user->GetAccountID()); - Game::server->Disconnect(packet->systemAddress, eServerDisconnectIdentifiers::CHARACTER_NOT_FOUND); - } - } else { - LOG("Couldn't get user for level load complete!"); - } + LoadPlayer(packet->systemAddress); break; }