mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Every game message still written or read by hand is now a NetGameMsg with Serialize and Deserialize, in per-domain files: - MovementMessages: teleport, platforms (resync and its request), orient to angle, node rotation lock, gravity scale, jetpack mode, control scheme, respawn checkpoint, rails (set/start, ready, cancel, arrived), mount inventory ID, dismount complete, possession ack, ghost reference override and position, camera cycling (eCameraTargetCyclingMode moves here). - ZoneMessages: player loaded (the old PLAYER_LOADED case), ready for updates, player ready, restore to post load stats, server done loading, invalid zone transfer list, zone summary display/dismissed, level processing complete, object world state, and the localized announcement WorldMigration wrote by hand. - PlayerMessages: chat mode, GM level, LEGO score, currency, reputation, GM invis, pickup currency, zone and player statistics, chat commands, bug reports, verify ack. - ObjectMessages: fire event client/server side, notify client (zone) object, notify object, script network vars, failed preconditions, terminate interaction, set name, request use, request server object info. - QuickBuildMessages: notify state, enable, cancel. - ActivityMessages gains match response/update/request, leaderboard request and data, shooting gallery score/rotation/fire, activity state change. - MissionMessages gains MissionDialogueCancelled (a no-op, as before). The wire structs left in GameMessages.h move to their domains (tooltip and emote to Effects, loot and item use to Inventory, model build to Building, behavior sound to Property, skill sets to Skill) and gain the missing direction. The dismount logic moves to PossessorComponent::OnDismountComplete. Every inbound message is registered in the GameMessageHandler map; the switch is gone, and GameMessages.cpp only holds the GameMsg/NetGameMsg base code. Call sites build the structs (NotifyClientObject, TerminateInteraction, Teleport, PlatformResync, FireEventClientSide, NotifyObject and NotifyClientZoneObject get convenience constructors like PlayFXEffect). Dead senders are dropped: SendSetShootingGalleryParams (no callers, field order was a guess), SendTeamPickupItem (the struct already existed), SendRequestActivitySummaryLeaderboardData (the struct covers it). Verified with RemainingMessagesTests: every old Send* function is frozen verbatim in Legacy/RemainingMessagesLegacy.h and compared byte for byte (same bits, destination and broadcast flag) over grids of inputs; every old Handle* read sequence is frozen as a Read* oracle and compared with the struct's Deserialize; round trips, truncation and a golden packet. PlayerLoaded (0x00dc36f0), SetGMLevel (0x00dd6230), MissionDialogueCancelled (0x00d9cc10) and LocalizedAnnouncementServerToSingleClient (0x00f23c50) were checked against the client. Behaviour notes: an inbound message that fails to deserialize is dropped, so ParseChatMessage over MAX_MESSAGE_LENGTH is dropped instead of truncated, and PLAYER_LOADED / READY_FOR_UPDATES / MISSION_DIALOGUE_CANCELLED now read their (unused) client fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
307 lines
13 KiB
C++
307 lines
13 KiB
C++
#include "DashboardActions.h"
|
|
|
|
#include "master/PlayerAction.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "Database.h"
|
|
#include "dServer.h"
|
|
#include "dZoneManager.h"
|
|
#include "Entity.h"
|
|
#include "Character.h"
|
|
#include "CharacterComponent.h"
|
|
#include "ChatPackets.h"
|
|
#include "GameMessages.h"
|
|
#include "EffectsMessages.h"
|
|
#include "MovementMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "PlayerManager.h"
|
|
#include "User.h"
|
|
#include "UserManager.h"
|
|
#include "ClientPackets.h"
|
|
#include "ZoneInstanceManager.h"
|
|
#include "eGameMasterLevel.h"
|
|
#include "eServerDisconnectIdentifiers.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Amf3.h"
|
|
#include "PetComponent.h"
|
|
#include "PropertyManagementComponent.h"
|
|
#include "GeneralUtils.h"
|
|
#include "VanityUtilities.h"
|
|
#include "dConfig.h"
|
|
#include "dChatFilter.h"
|
|
#include "MissionComponent.h"
|
|
#include "Mission.h"
|
|
#include "Contraband.h"
|
|
#include "LiveEvents.h"
|
|
|
|
namespace {
|
|
std::function<void(const SystemAddress&)> g_LogoutHandler;
|
|
|
|
uint32_t KickAccount(uint32_t accountId, eServerDisconnectIdentifiers reason) {
|
|
const auto users = UserManager::Instance()->GetUsersForAccount(accountId);
|
|
for (auto* user : users) {
|
|
const SystemAddress sysAddr = user->GetSystemAddress();
|
|
LOG("Dashboard disconnecting account %u (%s)", accountId, user->GetUsername().c_str());
|
|
if (g_LogoutHandler) g_LogoutHandler(sysAddr);
|
|
Game::server->Disconnect(sysAddr, reason);
|
|
}
|
|
return static_cast<uint32_t>(users.size());
|
|
}
|
|
|
|
// Pick up a GM level change: lower the in-game level if it now exceeds the account's maximum
|
|
uint32_t RefreshAccount(uint32_t accountId) {
|
|
const auto users = UserManager::Instance()->GetUsersForAccount(accountId);
|
|
if (users.empty()) return 0;
|
|
|
|
const auto info = Database::Get()->GetAccountInfo(users.front()->GetUsername());
|
|
if (!info) return 0;
|
|
|
|
for (auto* user : users) {
|
|
user->SetMaxGMLevel(info->maxGmLevel);
|
|
|
|
auto* entity = PlayerManager::GetPlayer(user->GetSystemAddress());
|
|
if (!entity || entity->GetGMLevel() <= info->maxGmLevel) continue;
|
|
|
|
ClientPackets::MakeGMResponse response;
|
|
response.success = true;
|
|
response.highestLevel = info->maxGmLevel;
|
|
response.previousLevel = entity->GetGMLevel();
|
|
response.newLevel = info->maxGmLevel;
|
|
response.Send(entity->GetSystemAddress());
|
|
GameMessages::UpdateChatMode chatMode;
|
|
chatMode.target = entity->GetObjectID();
|
|
chatMode.level = info->maxGmLevel;
|
|
chatMode.Send(UNASSIGNED_SYSTEM_ADDRESS);
|
|
entity->SetGMLevel(info->maxGmLevel);
|
|
if (info->maxGmLevel == eGameMasterLevel::CIVILIAN) {
|
|
GameMessages::ToggleGMInvisEvent msg;
|
|
msg.Send(entity->GetObjectID());
|
|
}
|
|
GameMessages::SlashCommandTextFeedback(entity->GetObjectID(), u"Your game master level has been changed.").SendToClient(entity->GetSystemAddress());
|
|
LOG("Dashboard lowered GM level of %s to %i", user->GetUsername().c_str(), static_cast<int>(info->maxGmLevel));
|
|
}
|
|
return static_cast<uint32_t>(users.size());
|
|
}
|
|
|
|
uint32_t RefreshCharacter(LWOOBJID characterId) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
auto* character = entity ? entity->GetCharacter() : nullptr;
|
|
if (!character) return 0;
|
|
|
|
const auto info = Database::Get()->GetCharacterInfo(characterId);
|
|
if (!info) return 0;
|
|
character->SetPermissionMap(info->permissionMap);
|
|
return 1;
|
|
}
|
|
|
|
// A moderator's warning, as a popup and in chat, to whichever character of the account is online here
|
|
uint32_t WarnAccount(uint32_t accountId, const std::string& text) {
|
|
uint32_t affected = 0;
|
|
for (auto* player : PlayerManager::GetAllPlayers()) {
|
|
auto* user = player ? UserManager::Instance()->GetUser(player->GetSystemAddress()) : nullptr;
|
|
if (!user || user->GetAccountID() != accountId) continue;
|
|
AMFArrayValue args;
|
|
args.Insert("title", std::string("Warning from a moderator"));
|
|
args.Insert("message", text);
|
|
GameMessages::UIMessageServerToSingleClient uiMessage;
|
|
uiMessage.target = player->GetObjectID();
|
|
uiMessage.strMessageName = "ToggleAnnounce";
|
|
uiMessage.args = std::move(args);
|
|
uiMessage.SendToClient(player->GetSystemAddress());
|
|
ChatPackets::SendSystemMessage(player->GetSystemAddress(), u"Warning from a moderator: " + GeneralUtils::UTF8ToUTF16(text));
|
|
affected++;
|
|
}
|
|
return affected;
|
|
}
|
|
|
|
// Every world gets the request; only the one with both players acts on it
|
|
uint32_t TeleportToPlayer(LWOOBJID characterId, LWOOBJID targetId) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
auto* target = PlayerManager::GetPlayer(targetId);
|
|
if (!entity || !target || entity == target) return 0;
|
|
const auto position = target->GetPosition();
|
|
GameMessages::Teleport(entity->GetObjectID(), position, entity->GetRotation()).SendToClient(entity->GetSystemAddress());
|
|
ChatPackets::SendSystemMessage(entity->GetSystemAddress(), u"A moderator moved you to " + GeneralUtils::ASCIIToUTF16(target->GetCharacter() ? target->GetCharacter()->GetName() : "another player") + u".");
|
|
LOG("Dashboard teleport of %llu to %llu", characterId, targetId);
|
|
return 1;
|
|
}
|
|
|
|
uint32_t RescueCharacter(LWOOBJID characterId, LWOMAPID zoneId, const std::string& spawnPoint) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
if (!entity || !entity->GetCharacter()) return 0;
|
|
if (!Game::zoneManager->CheckIfAccessibleZone(zoneId)) {
|
|
LOG("Dashboard rescue of %llu to inaccessible zone %u refused", characterId, zoneId);
|
|
return 0;
|
|
}
|
|
|
|
ChatPackets::SendSystemMessage(entity->GetSystemAddress(), u"A moderator is moving you to safety...");
|
|
const auto objectId = entity->GetObjectID();
|
|
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneId, 0, false,
|
|
[objectId, spawnPoint](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
|
|
auto* entity = Game::entityManager->GetEntity(objectId);
|
|
if (!entity || !entity->GetCharacter()) return;
|
|
|
|
auto* character = entity->GetCharacter();
|
|
if (auto* characterComponent = entity->GetComponent<CharacterComponent>()) {
|
|
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
|
characterComponent->SetLastRocketConfig(u"");
|
|
}
|
|
character->SetZoneID(zoneID);
|
|
character->SetZoneInstance(zoneInstance);
|
|
character->SetZoneClone(zoneClone);
|
|
// A named spawn point (a respawnname in the zone), as rocket launchers land players; empty is the zone's default
|
|
character->SetTargetScene(spawnPoint);
|
|
character->SaveXMLToDatabase();
|
|
|
|
LOG("Dashboard rescue: transferring %llu to zone %u", objectId, zoneID);
|
|
ClientPackets::TransferToWorld transfer;
|
|
transfer.serverIP = LUString(serverIP);
|
|
transfer.serverPort = serverPort;
|
|
transfer.mythranShift = mythranShift;
|
|
transfer.Send(entity->GetSystemAddress());
|
|
});
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
void Tell(Entity* player, const std::string& message) {
|
|
if (player) ChatPackets::SendSystemMessage(player->GetSystemAddress(), GeneralUtils::UTF8ToUTF16(message));
|
|
}
|
|
|
|
uint32_t NameModerated(LWOOBJID characterId, bool approved, const std::string& name) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
auto* character = entity ? entity->GetCharacter() : nullptr;
|
|
if (!character) return 0;
|
|
character->ApplyNameModeration(approved);
|
|
Tell(entity, approved ? "Your new name \"" + name + "\" was approved. Other players will see it after you next change worlds."
|
|
: "Your requested name \"" + name + "\" was not approved. You can pick another one at character select.");
|
|
return 1;
|
|
}
|
|
|
|
uint32_t PetNameModerated(LWOOBJID petId, bool approved, const std::string& name) {
|
|
uint32_t affected = 0;
|
|
// A pet that is out updates for everyone at once
|
|
for (auto* player : PlayerManager::GetAllPlayers()) {
|
|
auto* pet = player ? PetComponent::GetActivePet(player->GetObjectID()) : nullptr;
|
|
if (pet && pet->GetDatabaseId() == petId) pet->ApplyNameModeration(approved);
|
|
}
|
|
// Tell whoever owns it
|
|
for (auto* player : PlayerManager::GetAllPlayers()) {
|
|
auto* inventory = player ? player->GetComponent<InventoryComponent>() : nullptr;
|
|
if (!inventory || !inventory->GetPetsMut().contains(petId)) continue;
|
|
Tell(player, approved ? "Your pet's name \"" + name + "\" was approved." : "Your pet's name \"" + name + "\" was not approved. You can give it a new one.");
|
|
affected++;
|
|
}
|
|
return affected;
|
|
}
|
|
|
|
uint32_t PropertyModerated(LWOOBJID propertyId, LWOOBJID ownerId, bool approved, const std::string& reason) {
|
|
uint32_t affected = 0;
|
|
if (auto* property = PropertyManagementComponent::Instance(); property && property->GetId() == propertyId) {
|
|
property->ApplyModeration(approved, reason);
|
|
affected++;
|
|
}
|
|
if (auto* owner = PlayerManager::GetPlayer(ownerId)) {
|
|
Tell(owner, approved ? "Your property was approved and can now be visited by others."
|
|
: "Your property was not approved" + (reason.empty() ? std::string(".") : ": " + reason) + " It has been made private.");
|
|
affected++;
|
|
}
|
|
return affected;
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
/**
|
|
* A moderator changing a mission of a character that is here, as the GM commands do: /completemission (rewards
|
|
* only when asked), /resetmission and /addmission. Answers 1 when it changed, 2 when there was nothing to change.
|
|
*/
|
|
uint32_t ChangeMission(ePlayerAction action, LWOOBJID characterId, uint32_t missionId, bool rewards, const std::string& name) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
auto* missions = entity ? entity->GetComponent<MissionComponent>() : nullptr;
|
|
if (!missions || !entity->GetCharacter()) return 0;
|
|
auto* mission = missions->GetMission(missionId);
|
|
std::string text;
|
|
switch (action) {
|
|
case ePlayerAction::MISSION_COMPLETE:
|
|
if (mission && mission->IsComplete() && !mission->IsRepeatable()) return 2;
|
|
missions->CompleteMission(missionId, true, rewards);
|
|
text = "A moderator completed \"" + name + "\" for you" + (rewards ? "." : " (without its rewards).");
|
|
break;
|
|
case ePlayerAction::MISSION_RESET:
|
|
if (!mission) return 2;
|
|
missions->ResetMission(missionId);
|
|
text = "A moderator reset \"" + name + "\" so you can do it again.";
|
|
break;
|
|
default:
|
|
if (mission && !mission->IsRepeatable()) return 2;
|
|
missions->AcceptMission(missionId, true);
|
|
text = "A moderator gave you \"" + name + "\".";
|
|
break;
|
|
}
|
|
entity->GetCharacter()->SaveXMLToDatabase();
|
|
Tell(entity, text);
|
|
LOG("Dashboard %s of mission %u for %llu", std::string(magic_enum::enum_name(action)).c_str(), missionId, characterId);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
void DashboardActions::SetLogoutHandler(std::function<void(const SystemAddress&)> handler) {
|
|
g_LogoutHandler = std::move(handler);
|
|
}
|
|
|
|
uint32_t DashboardActions::Apply(const PlayerActionRequest& request) {
|
|
switch (request.action) {
|
|
case ePlayerAction::KICK_ACCOUNT:
|
|
return KickAccount(request.accountId, static_cast<eServerDisconnectIdentifiers>(request.disconnectReason));
|
|
case ePlayerAction::REFRESH_ACCOUNT:
|
|
return RefreshAccount(request.accountId);
|
|
case ePlayerAction::REFRESH_CHARACTER:
|
|
return RefreshCharacter(request.characterId);
|
|
case ePlayerAction::RESCUE_CHARACTER:
|
|
return RescueCharacter(request.characterId, request.zoneId, request.text);
|
|
case ePlayerAction::NAME_MODERATED:
|
|
return NameModerated(request.characterId, request.approved, request.text);
|
|
case ePlayerAction::PET_NAME_MODERATED:
|
|
return PetNameModerated(request.targetId, request.approved, request.text);
|
|
case ePlayerAction::PROPERTY_MODERATED:
|
|
return PropertyModerated(request.targetId, request.characterId, request.approved, request.text);
|
|
case ePlayerAction::TELEPORT_TO_PLAYER:
|
|
return TeleportToPlayer(request.characterId, request.targetId);
|
|
case ePlayerAction::WARN_ACCOUNT:
|
|
return WarnAccount(request.accountId, request.text);
|
|
case ePlayerAction::CHAT_MESSAGE: {
|
|
// From the dashboard or a chat bridge: shown to everyone in this world (or only the chosen zone or instance), checked by the
|
|
// same chat filter as players' messages unless chat_bridge_filter=0
|
|
if (Game::server->GetZoneID() == 0 || (request.zoneId != 0 && request.zoneId != Game::server->GetZoneID())) return 0;
|
|
if (request.instanceId >= 0 && request.instanceId != Game::server->GetInstanceID()) return 0;
|
|
if (Game::config->GetValue("chat_bridge_filter") != "0" && !Game::chatFilter->IsSentenceOkay(request.text, eGameMasterLevel::CIVILIAN).empty()) return 0;
|
|
const auto players = PlayerManager::GetAllPlayers().size();
|
|
ChatPackets::Client::GeneralChatMessage chatMessage;
|
|
chatMessage.chatChannel = 4;
|
|
chatMessage.senderName = LUWString(request.name);
|
|
chatMessage.message = GeneralUtils::UTF8ToUTF16(request.text);
|
|
chatMessage.Broadcast();
|
|
return static_cast<uint32_t>(players);
|
|
}
|
|
case ePlayerAction::RELOAD_VANITY:
|
|
// Character select (zone 0) has no vanity
|
|
if (Game::server->GetZoneID() == 0) return 0;
|
|
VanityUtilities::SpawnVanity();
|
|
return 1;
|
|
case ePlayerAction::RELOAD_CHAT_FILTER:
|
|
if (!Game::chatFilter) return 0;
|
|
Game::chatFilter->ReloadCustomWords();
|
|
return 1;
|
|
case ePlayerAction::MISSION_COMPLETE:
|
|
case ePlayerAction::MISSION_RESET:
|
|
case ePlayerAction::MISSION_ACCEPT:
|
|
return ChangeMission(request.action, request.characterId, static_cast<uint32_t>(request.targetId), request.approved, request.text);
|
|
case ePlayerAction::RELOAD_LIVE_OPS:
|
|
return LiveEvents::Reload();
|
|
case ePlayerAction::RELOAD_CONTRABAND:
|
|
return Contraband::Reload();
|
|
}
|
|
return 0;
|
|
}
|