mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-09 22:33:45 +00:00
Every packet of the chat service is now an LUBitStream struct in dNet/ChatPackets.h (docs/PacketArchitecture.md, PR 13), and nothing in the chat server or the chat side of the world server reads or writes a packet by hand any more. - World <-> chat: LoginSessionNotify, UnexpectedDisconnect, GMLevelUpdate, GMMute, Announcement (GM announce), CreateTeam, TeamUpdate (TEAM_GET_STATUS to worlds), AchievementNotify, ShowAllRequest, FindPlayerRequest. - Client -> world -> chat (friends, ignore list, teams, general and private chat): GetFriendsList, AddFriendRequest, AddFriendResponse, RemoveFriend, GetIgnoreList, AddIgnore, RemoveIgnore, GeneralChatMessage, PrivateChatMessage, TeamInvite, TeamInviteResponse, TeamLeave, TeamKick, TeamSetLeader, TeamSetLoot, TeamGetStatus. The 77 bytes the handlers skipped are named fields now (the sender block the client fills in); the 4 unused bytes after the player ID are kept as `unknown`. - What the client receives. Chat service (ChatPackets::Client): GeneralChatMessage (replaces SendChatMessage; SendSystemMessage stays as a helper built on it), PrivateChatMessage. Client service (ClientPackets, as structs go in the file of the ServiceType in their header): SendCannedText (replaces SendMessageFail), GetFriendsListResponse, AddFriendRequest, AddFriendResponse, RemoveFriendResponse, UpdateFriendNotify, WhoResponse, ShowAllResponse, Get/Add/RemoveIgnoreResponse, TeamInvite, TeamInviteInitialResponse, and the team game messages chat writes (TeamInviteConfirm, TeamGetStatusResponse, TeamSetLeader, TeamAddPlayer, TeamRemovePlayer, TeamSetOffWorldFlag) as TeamGameMsg structs, since chat doesn't link dGame's NetGameMsg. - WORLD_ROUTE_PACKET is ChatPackets::WorldRoutePacket (its own file, dNet/WorldRoutePacket.h, since it carries packets of other services): the target and the inner packet. ChatPacketHandler::SendRouted replaces the per-function route headers and SendRoutedMsg. Dispatch: dNet/PacketDispatcher.h is a dispatch map from packet ID to (struct, handler function); packets that fail to Deserialize are logged and dropped. The chat server's switch and the world's HandlePacketChat switch are now maps. The handlers take the structs; their logic is unchanged. World code sends to chat with ChatServerLink::Send (dGame) instead of writing to Game::chatServer by hand. eChatChannel, eChatMessageResponseCode and eAddIgnoreResponse moved to dCommon/dEnums so the structs can use them. Verified: tests/dGameTests/dNetTests/Legacy/ChatPacketsLegacy.h is a verbatim copy of the old senders and readers; ChatPacketsTests (25 tests) sends a grid of inputs through both and requires identical bits, bytes and destination, checks the old readers read what the structs write, round trips every struct, checks truncated packets are refused, and has hand written golden packets for general chat, canned text, GM mute and a routed team game message. Breaking one field width (UpdateFriendNotify) and the TeamAddPlayer zone flag made the tests fail. No wire bytes changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
316 lines
11 KiB
C++
316 lines
11 KiB
C++
#include "GMGreaterThanZeroCommands.h"
|
|
#include "ChatServerLink.h"
|
|
#include "DashboardNotify.h"
|
|
#include "SlashCommandHandler.h"
|
|
|
|
// Classes
|
|
#include "Character.h"
|
|
#include "ChatPackets.h"
|
|
#include "dServer.h"
|
|
#include "PlayerManager.h"
|
|
#include "User.h"
|
|
|
|
// Database
|
|
#include "Database.h"
|
|
|
|
// Components
|
|
#include "DestroyableComponent.h"
|
|
#include "PropertyManagementComponent.h"
|
|
|
|
// Enums
|
|
#include "MessageType/Chat.h"
|
|
#include "eServerDisconnectIdentifiers.h"
|
|
#include "eObjectBits.h"
|
|
|
|
namespace GMGreaterThanZeroCommands {
|
|
|
|
void Kick(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
if (splitArgs.size() == 1) {
|
|
auto* player = PlayerManager::GetPlayer(splitArgs[0]);
|
|
|
|
std::u16string username = GeneralUtils::UTF8ToUTF16(splitArgs[0]);
|
|
if (player == nullptr) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Count not find player of name: " + username);
|
|
return;
|
|
}
|
|
const auto target = SlashCommandHandler::TargetOf(player);
|
|
if (!target || !SlashCommandHandler::MayActOn(entity, sysAddr, *target, SlashCommandLevels::eTargetRule::TOOLS, "kick")) return;
|
|
|
|
Game::server->Disconnect(player->GetSystemAddress(), eServerDisconnectIdentifiers::KICK);
|
|
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Kicked: " + username);
|
|
} else {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /kick <username>");
|
|
}
|
|
}
|
|
|
|
void Ban(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
|
|
if (splitArgs.size() == 1) {
|
|
const auto target = SlashCommandHandler::FindTarget(splitArgs[0]);
|
|
if (!target) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Count not find player of name: " + GeneralUtils::UTF8ToUTF16(splitArgs[0]));
|
|
return;
|
|
}
|
|
if (!SlashCommandHandler::MayActOn(entity, sysAddr, *target, SlashCommandLevels::eTargetRule::MODERATION, "ban")) return;
|
|
// The server always keeps a GM 9 account that can sign in
|
|
if (target->gmLevel >= AccountRules::OPERATOR_LEVEL &&
|
|
AccountRules::RemovesLastOperator(target->gmLevel, Database::Get()->CountActiveAccountsAtGmLevel(AccountRules::OPERATOR_LEVEL, target->accountId))) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"/ban: " + GeneralUtils::UTF8ToUTF16(AccountRules::LastOperatorMessage("banned")));
|
|
return;
|
|
}
|
|
|
|
const auto accountId = target->accountId;
|
|
if (accountId != 0) Database::Get()->UpdateAccountBan(accountId, true);
|
|
if (accountId != 0) DashboardNotify::Changed("account_banned", accountId);
|
|
|
|
if (target->entity != nullptr) {
|
|
Game::server->Disconnect(target->entity->GetSystemAddress(), eServerDisconnectIdentifiers::FREE_TRIAL_EXPIRED);
|
|
}
|
|
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Banned: " + GeneralUtils::ASCIIToUTF16(splitArgs[0]));
|
|
} else {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /ban <username>");
|
|
}
|
|
}
|
|
|
|
void MailItem(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
if (splitArgs.size() < 2) return;
|
|
|
|
const auto& playerName = splitArgs[0];
|
|
|
|
auto playerInfo = Database::Get()->GetCharacterInfo(playerName);
|
|
|
|
uint32_t receiverID = 0;
|
|
if (!playerInfo) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Failed to find that player");
|
|
|
|
return;
|
|
}
|
|
|
|
receiverID = playerInfo->id;
|
|
|
|
const auto target = SlashCommandHandler::FindTarget(playerName);
|
|
if (!target || !SlashCommandHandler::MayActOn(entity, sysAddr, *target, SlashCommandLevels::eTargetRule::ITEMS, "mailitem")) return;
|
|
|
|
const auto lot = GeneralUtils::TryParse<LOT>(splitArgs.at(1));
|
|
|
|
if (!lot) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item lot.");
|
|
return;
|
|
}
|
|
|
|
MailInfo mailInsert;
|
|
mailInsert.senderId = entity->GetObjectID();
|
|
mailInsert.senderUsername = "Darkflame Universe";
|
|
mailInsert.receiverId = receiverID;
|
|
mailInsert.recipient = playerName;
|
|
mailInsert.subject = "Lost item";
|
|
mailInsert.body = "This is a replacement item for one you lost.";
|
|
mailInsert.itemID = LWOOBJID_EMPTY;
|
|
mailInsert.itemLOT = lot.value();
|
|
mailInsert.itemSubkey = LWOOBJID_EMPTY;
|
|
mailInsert.itemCount = 1;
|
|
Database::Get()->InsertNewMail(mailInsert);
|
|
DashboardNotify::Changed("mail", mailInsert.receiverId);
|
|
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Mail sent");
|
|
}
|
|
|
|
void ApproveProperty(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
if (PropertyManagementComponent::Instance() != nullptr) {
|
|
PropertyManagementComponent::Instance()->UpdateApprovedStatus(true);
|
|
}
|
|
}
|
|
|
|
void Mute(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
|
|
if (splitArgs.size() >= 1) {
|
|
const auto target = SlashCommandHandler::FindTarget(splitArgs[0]);
|
|
if (!target) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Count not find player of name: " + GeneralUtils::UTF8ToUTF16(splitArgs[0]));
|
|
return;
|
|
}
|
|
if (!SlashCommandHandler::MayActOn(entity, sysAddr, *target, SlashCommandLevels::eTargetRule::MODERATION, "mute")) return;
|
|
|
|
const uint32_t accountId = target->accountId;
|
|
const LWOOBJID characterId = target->characterId;
|
|
|
|
time_t expire = 1; // Default to indefinate mute
|
|
|
|
if (splitArgs.size() >= 2) {
|
|
const auto days = GeneralUtils::TryParse<uint32_t>(splitArgs[1]);
|
|
if (!days) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Invalid days.");
|
|
|
|
return;
|
|
}
|
|
|
|
std::optional<uint32_t> hours;
|
|
if (splitArgs.size() >= 3) {
|
|
hours = GeneralUtils::TryParse<uint32_t>(splitArgs[2]);
|
|
if (!hours) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Invalid hours.");
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
expire = time(NULL);
|
|
expire += 24 * 60 * 60 * days.value();
|
|
expire += 60 * 60 * hours.value_or(0);
|
|
}
|
|
|
|
if (accountId != 0) Database::Get()->UpdateAccountUnmuteTime(accountId, expire);
|
|
if (accountId != 0) DashboardNotify::Changed("account_muted", accountId);
|
|
|
|
char buffer[32] = "brought up for review.\0";
|
|
|
|
if (expire != 1) {
|
|
std::tm* ptm = std::localtime(&expire);
|
|
// Format: Mo, 15.06.2009 20:20:00
|
|
std::strftime(buffer, 32, "%a, %d.%m.%Y %H:%M:%S", ptm);
|
|
}
|
|
|
|
const auto timeStr = GeneralUtils::ASCIIToUTF16(std::string(buffer));
|
|
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Muted: " + GeneralUtils::UTF8ToUTF16(splitArgs[0]) + u" until " + timeStr);
|
|
|
|
//Notify chat about it
|
|
ChatPackets::GMMute mute;
|
|
mute.playerID = characterId;
|
|
mute.expire = expire;
|
|
ChatServerLink::Send(mute);
|
|
} else {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Correct usage: /mute <username> <days (optional)> <hours (optional)>");
|
|
}
|
|
}
|
|
|
|
void Fly(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
auto* character = entity->GetCharacter();
|
|
|
|
if (character) {
|
|
bool isFlying = character->GetIsFlying();
|
|
|
|
if (isFlying) {
|
|
GameMessages::SendSetJetPackMode(entity, false);
|
|
|
|
character->SetIsFlying(false);
|
|
} else {
|
|
float speedScale = 1.0f;
|
|
|
|
if (splitArgs.size() >= 1) {
|
|
const auto tempScaleStore = GeneralUtils::TryParse<float>(splitArgs.at(0));
|
|
|
|
if (tempScaleStore) {
|
|
speedScale = tempScaleStore.value();
|
|
} else {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Failed to parse speed scale argument.");
|
|
}
|
|
}
|
|
|
|
float airSpeed = 20 * speedScale;
|
|
float maxAirSpeed = 30 * speedScale;
|
|
float verticalVelocity = 1.5 * speedScale;
|
|
|
|
GameMessages::SendSetJetPackMode(entity, true, true, false, 167, airSpeed, maxAirSpeed, verticalVelocity);
|
|
|
|
character->SetIsFlying(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AttackImmune(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
if (splitArgs.empty()) return;
|
|
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
|
|
const auto state = GeneralUtils::TryParse<int32_t>(splitArgs[0]);
|
|
|
|
if (!state) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Invalid state.");
|
|
return;
|
|
}
|
|
|
|
if (destroyableComponent) destroyableComponent->SetIsImmune(state.value());
|
|
}
|
|
|
|
void GmImmune(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
if (splitArgs.empty()) return;
|
|
|
|
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
|
|
|
|
const auto state = GeneralUtils::TryParse<int32_t>(splitArgs[0]);
|
|
|
|
if (!state) {
|
|
ChatPackets::SendSystemMessage(sysAddr, u"Invalid state.");
|
|
return;
|
|
}
|
|
|
|
if (destroyableComponent) destroyableComponent->SetIsGMImmune(state.value());
|
|
}
|
|
|
|
void GmInvis(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
GameMessages::ToggleGMInvisEvent msg;
|
|
msg.Send(entity->GetObjectID());
|
|
}
|
|
|
|
void SetName(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
GameMessages::SendSetName(entity->GetObjectID(), GeneralUtils::UTF8ToUTF16(args), UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
void Title(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
std::string name = entity->GetCharacter()->GetName() + " - " + args;
|
|
GameMessages::SendSetName(entity->GetObjectID(), GeneralUtils::UTF8ToUTF16(name), UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
void ShowAll(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
bool displayZoneData = true;
|
|
bool displayIndividualPlayers = true;
|
|
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
|
|
|
|
if (!splitArgs.empty() && !splitArgs.at(0).empty()) displayZoneData = splitArgs.at(0) == "1";
|
|
if (splitArgs.size() > 1) displayIndividualPlayers = splitArgs.at(1) == "1";
|
|
|
|
ChatPackets::ShowAllRequest request;
|
|
request.requestor = entity->GetObjectID();
|
|
request.displayZoneData = displayZoneData;
|
|
request.displayIndividualPlayers = displayIndividualPlayers;
|
|
ChatServerLink::Send(request);
|
|
}
|
|
|
|
void FindPlayer(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
if (args.empty()) {
|
|
GameMessages::SendSlashCommandFeedbackText(entity, u"No player Given");
|
|
return;
|
|
}
|
|
|
|
ChatPackets::FindPlayerRequest request;
|
|
request.requestor = entity->GetObjectID();
|
|
request.playerName = LUWString(args);
|
|
ChatServerLink::Send(request);
|
|
}
|
|
|
|
void Spectate(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
|
|
if (args.empty()) {
|
|
GameMessages::SendForceCameraTargetCycle(entity, false, eCameraTargetCyclingMode::DISALLOW_CYCLING, entity->GetObjectID());
|
|
return;
|
|
}
|
|
|
|
auto player = PlayerManager::GetPlayer(args);
|
|
if (!player) {
|
|
GameMessages::SendSlashCommandFeedbackText(entity, u"Player not found");
|
|
return;
|
|
}
|
|
GameMessages::SendSlashCommandFeedbackText(entity, u"Spectating Player");
|
|
GameMessages::SendForceCameraTargetCycle(entity, false, eCameraTargetCyclingMode::DISALLOW_CYCLING, player->GetObjectID());
|
|
}
|
|
}
|