mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Adding or removing a grant sends the existing PLAYER_ACTION refresh through master to every world: REFRESH_ACCOUNT for an account's grants, REFRESH_CHARACTER for a character's. A world with that account's sessions (or the loaded character) drops the grants it kept, so the next command reads them again: no relog. The dashboard toasts whether the player was online. The dashboard's own open pages already get the new rights on their next request, and their WebSockets are checked again. Check: with a character in game, grant it /spawn on the dashboard (toast: "Applied in game at once") and use /spawn without relogging; remove the grant and /spawn is refused again at once. With the player offline the toast says it applies when they next play. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
13 KiB
C++
311 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. Also a change of the
|
|
// account's permission grants: they are read again the next time a command is used
|
|
uint32_t RefreshAccount(uint32_t accountId) {
|
|
const auto users = UserManager::Instance()->GetUsersForAccount(accountId);
|
|
if (users.empty()) return 0;
|
|
for (auto* user : users) user->ForgetGrants();
|
|
|
|
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());
|
|
}
|
|
|
|
// Restrictions or permission grants of a character changed
|
|
uint32_t RefreshCharacter(LWOOBJID characterId) {
|
|
auto* entity = PlayerManager::GetPlayer(characterId);
|
|
auto* character = entity ? entity->GetCharacter() : nullptr;
|
|
if (!character) return 0;
|
|
if (auto* user = character->GetParentUser()) user->ForgetGrants();
|
|
|
|
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;
|
|
}
|