mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat: worlds move their players to another instance, GM commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
|
||||
#include "SlashCommandHandler.h"
|
||||
#include "WorldMigration.h"
|
||||
|
||||
#include <iomanip>
|
||||
#include <ranges>
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
419
dGame/dUtilities/WorldMigration.cpp
Normal file
419
dGame/dUtilities/WorldMigration.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
#include "WorldMigration.h"
|
||||
|
||||
#include <ctime>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#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<LWOOBJID, float> waiting; // players who can't go yet, and for how long
|
||||
};
|
||||
|
||||
std::optional<Active> g_Active;
|
||||
std::map<SystemAddress, Leaving> g_Leaving;
|
||||
std::map<LWOOBJID, std::pair<CarriedPlayerState, float>> g_Carried;
|
||||
std::function<void(const SystemAddress&)> g_Cleanup;
|
||||
std::map<uint32_t, eState> 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<uint32_t>(text.size());
|
||||
for (const auto character : text) bitStream.Write<uint16_t>(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<uint32_t>(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<uint32_t>(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>()) 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<float>(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<SystemAddress> 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<void(const SystemAddress&)> 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<InventoryComponent>();
|
||||
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<uint32_t>(std::time(nullptr));
|
||||
InstanceMigrationRequest request;
|
||||
// Different worlds may ask at the same time; master keys migrations by this
|
||||
request.requestId = (++nextId << 8) ^ static_cast<uint32_t>(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<std::string>& words, size_t skip, uint16_t& warnSeconds, bool& seamless) {
|
||||
warnSeconds = 10;
|
||||
seamless = false;
|
||||
if (words.size() > skip) {
|
||||
const auto warn = GeneralUtils::TryParse<uint16_t>(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<uint32_t>(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<uint32_t>(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);
|
||||
}
|
||||
64
dGame/dUtilities/WorldMigration.h
Normal file
64
dGame/dUtilities/WorldMigration.h
Normal file
@@ -0,0 +1,64 @@
|
||||
#ifndef __WORLDMIGRATION__H__
|
||||
#define __WORLDMIGRATION__H__
|
||||
|
||||
#include <functional>
|
||||
|
||||
#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<void(const SystemAddress&)> 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__
|
||||
@@ -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<uint32_t>(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<LWOINSTANCEID>(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<CharacterComponent>();
|
||||
if (!characterComponent) return;
|
||||
|
||||
// Do charxml fixes here
|
||||
auto* levelComponent = player->GetComponent<LevelProgressionComponent>();
|
||||
auto* const inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
auto* const missionComponent = player->GetComponent<MissionComponent>();
|
||||
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<DestroyableComponent>();
|
||||
|
||||
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>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
|
||||
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
|
||||
bitStream.Write<uint32_t>(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<uint32_t>(lxfmlSize);
|
||||
bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(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<uint32_t>(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<SkillComponent>();
|
||||
|
||||
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<LWOMAPID>(Game::server->GetZoneID());
|
||||
bitStream.Write<LWOINSTANCEID>(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<SkillComponent>();
|
||||
|
||||
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<LWOMAPID>(Game::server->GetZoneID());
|
||||
bitStream.Write<LWOINSTANCEID>(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<CharacterComponent>();
|
||||
if (!characterComponent) return;
|
||||
|
||||
// Do charxml fixes here
|
||||
auto* levelComponent = player->GetComponent<LevelProgressionComponent>();
|
||||
auto* const inventoryComponent = player->GetComponent<InventoryComponent>();
|
||||
auto* const missionComponent = player->GetComponent<MissionComponent>();
|
||||
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<DestroyableComponent>();
|
||||
|
||||
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>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
|
||||
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
|
||||
bitStream.Write<uint32_t>(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<uint32_t>(lxfmlSize);
|
||||
bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(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<uint32_t>(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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user