#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 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(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(info->maxGmLevel)); } return static_cast(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->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); const std::string text = 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."; // A private announcement popup, as live told the player, and the same text in chat AMFArrayValue args; args.Insert("title", std::string(approved ? "Name approved" : "Name not approved")); args.Insert("message", text); GameMessages::UIMessageServerToSingleClient uiMessage; uiMessage.target = entity->GetObjectID(); uiMessage.strMessageName = "ToggleAnnounce"; uiMessage.args = std::move(args); uiMessage.SendToClient(entity->GetSystemAddress()); Tell(entity, text); 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() : 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() : 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 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(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(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(request.targetId), request.approved, request.text); case ePlayerAction::RELOAD_LIVE_OPS: return LiveEvents::Reload(); case ePlayerAction::RELOAD_CONTRABAND: return Contraband::Reload(); case ePlayerAction::GUILD_CHANGED: // The chat server's (master doesn't send it to worlds) return 0; } return 0; }