Files
DarkflameServer/dChatServer/PlayerContainer.cpp
Aaron Kimbrell b466c08ee2 Merge origin/main into experimental: behaviors, raw terrain scene lookups, property model fixes, player count
Main's model behaviors (every rotation, DoDamage, OnTimer, OnStartup, MoveBackToStart, moves along the model's
local axes), raw terrain reading with scene lookups and scene tools, property model and brick by brick fixes and the
chat server's player count fix, merged onto this branch's structures:

- game messages stay structs; the angular velocity messages join them and the handler sets a message's target
- strips own their speed again and the block names, moves and rotations stay in the shared action table
  (PropertyBehaviorActions, which the dashboard's behavior player reads; it now lists rotations too)
- the zone keeps reading its file through ZoneFile and takes the boundaries from it, then loads the raw terrain as main
  does; the old dNavigation terrain reader is gone
- the show all response stays a struct and logs a player count mismatch as main does
- the property management component tells models who sent a chat message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:57:45 -06:00

216 lines
6.7 KiB
C++

#include "PlayerContainer.h"
#include "dNetCommon.h"
#include <iostream>
#include <algorithm>
#include "Game.h"
#include "Logger.h"
#include "ChatPacketHandler.h"
#include "GeneralUtils.h"
#include "BitStreamUtils.h"
#include "Database.h"
#include "ServiceType.h"
#include "ChatPackets.h"
#include "dConfig.h"
#include "MessageType/Chat.h"
#include "TeamContainer.h"
#include "ChatMatchmaking.h"
#include "ChatGuilds.h"
#include "GuildManager.h"
void PlayerContainer::Initialize() {
m_MaxNumberOfBestFriends =
GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("max_number_of_best_friends")).value_or(m_MaxNumberOfBestFriends);
m_MaxNumberOfFriends =
GeneralUtils::TryParse<uint32_t>(Game::config->GetValue("max_number_of_friends")).value_or(m_MaxNumberOfFriends);
}
TeamData::TeamData() {
lootFlag = Game::config->GetValue("default_team_loot") == "0" ? 0 : 1;
}
void PlayerContainer::InsertPlayer(const ChatPackets::LoginSessionNotify& notify, const SystemAddress& sysAddr) {
// Names longer than ChatPackets::LoginSessionNotify::MAX_NAME_LENGTH were dropped when read
const LWOOBJID playerId = notify.playerID;
// A world sending its players to a new chat server (live update): nobody logged in or changed zones
if (notify.resync) {
if (m_Players.contains(playerId)) {
m_Players[playerId].worldServerSysAddr = sysAddr;
m_PlayersToRemove.erase(playerId);
return;
}
auto& data = m_Players[playerId];
data = PlayerData();
data.playerID = playerId;
data.playerName = notify.playerName;
data.zoneID = notify.zoneID;
data.muteExpire = static_cast<time_t>(notify.muteExpire);
data.gmLevel = notify.gmLevel;
data.worldServerSysAddr = sysAddr;
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
m_PlayerCount++;
// Their friends list is read again when their client next asks (every zone load); until then the friends
// they have are what friend updates go to
ChatPacketHandler::LoadFriends(data);
m_PlayersToRemove.erase(playerId);
LOG("Took over user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
// Guildmates' clients only get the zone again (no "logged in" line)
ChatGuilds::Get().PlayerOnline(playerId, false);
return;
}
// Loaded into a world (a new one when changing zones): a lobby they waited in on their last world is left
ChatMatchmaking::PlayerLeftWorld(playerId);
auto isLogin = !m_Players.contains(playerId);
auto& data = m_Players[playerId];
data = PlayerData();
data.isLogin = isLogin;
data.playerID = playerId;
data.playerName = notify.playerName;
data.zoneID = notify.zoneID;
data.muteExpire = static_cast<time_t>(notify.muteExpire);
data.gmLevel = notify.gmLevel;
data.worldServerSysAddr = sysAddr;
m_Names[data.playerID] = GeneralUtils::UTF8ToUTF16(data.playerName);
if (isLogin) m_PlayerCount++;
LOG("Added user: %s (%llu), zone: %i", data.playerName.c_str(), data.playerID, data.zoneID.GetMapID());
Database::Get()->UpdateActivityLog(data.playerID, isLogin ? eActivityType::PlayerLoggedIn : eActivityType::PlayerChangedZone, data.zoneID.GetMapID());
m_PlayersToRemove.erase(playerId);
ChatGuilds::Get().PlayerOnline(playerId, isLogin);
}
void PlayerContainer::ScheduleRemovePlayer(const ChatPackets::UnexpectedDisconnect& notify, const SystemAddress& sysAddr) {
const LWOOBJID playerID = notify.playerID;
constexpr float updatePlayerOnLogoutTime = 20.0f;
if (playerID != LWOOBJID_EMPTY) m_PlayersToRemove.insert_or_assign(playerID, updatePlayerOnLogoutTime);
}
void PlayerContainer::Update(const float deltaTime) {
for (auto it = m_PlayersToRemove.begin(); it != m_PlayersToRemove.end();) {
auto& [id, time] = *it;
time -= deltaTime;
if (time <= 0.0f) {
RemovePlayer(id);
it = m_PlayersToRemove.erase(it);
} else {
++it;
}
}
}
void PlayerContainer::RemovePlayer(const LWOOBJID playerID) {
//Before they get kicked, we need to also send a message to their friends saying that they disconnected.
const auto& player = GetPlayerData(playerID);
if (!player) {
LOG("Failed to find user: %llu", playerID);
return;
}
for (const auto& fr : player.friends) {
const auto& fd = this->GetPlayerData(fr.friendID);
if (fd) ChatPacketHandler::SendFriendUpdate(fd, player, 0, fr.isBestFriend);
}
auto* team = TeamContainer::GetTeam(playerID);
if (team != nullptr) {
TeamContainer::RemoveMember(team, playerID, false, false, true);
}
ChatGuilds::Get().PlayerOffline(playerID);
m_PlayerCount--;
LOG("Removed user: %llu", playerID);
m_Players.erase(playerID);
Database::Get()->UpdateActivityLog(playerID, eActivityType::PlayerLoggedOut, player.zoneID.GetMapID());
}
void PlayerContainer::MuteUpdate(const ChatPackets::GMMute& mute, const SystemAddress& sysAddr) {
const LWOOBJID playerID = mute.playerID;
const time_t expire = static_cast<time_t>(mute.expire);
auto& player = this->GetPlayerDataMutable(playerID);
if (!player) {
LOG("Failed to find user: %llu", playerID);
return;
}
player.muteExpire = expire;
BroadcastMuteUpdate(playerID, expire);
}
void PlayerContainer::BroadcastMuteUpdate(LWOOBJID player, time_t time) {
ChatPackets::GMMute mute;
mute.playerID = player;
mute.expire = time;
mute.Broadcast();
}
std::u16string PlayerContainer::GetName(LWOOBJID playerID) {
const auto iter = m_Names.find(playerID);
if (iter == m_Names.end()) return u"";
return iter->second;
}
LWOOBJID PlayerContainer::GetId(const std::u16string& playerName) {
LWOOBJID toReturn = LWOOBJID_EMPTY;
for (const auto& [id, name] : m_Names) {
if (name == playerName) {
toReturn = id;
break;
}
}
return toReturn;
}
// TODO Make this a pointer again or do something to make it so you cant edit the LWOOBJID_EMPTY entry? it should be ignored in all cases anyways though...
PlayerData& PlayerContainer::GetPlayerDataMutable(const LWOOBJID& playerID) {
return m_Players.contains(playerID) ? m_Players[playerID] : m_Players[LWOOBJID_EMPTY];
}
PlayerData& PlayerContainer::GetPlayerDataMutable(const std::string& playerName) {
for (auto& [id, player] : m_Players) {
if (!player) continue;
if (player.playerName == playerName) return player;
}
return m_Players[LWOOBJID_EMPTY];
}
const PlayerData& PlayerContainer::GetPlayerData(const LWOOBJID& playerID) {
return GetPlayerDataMutable(playerID);
}
const PlayerData& PlayerContainer::GetPlayerData(const std::string& playerName) {
return GetPlayerDataMutable(playerName);
}
void PlayerContainer::Shutdown() {
m_Players.erase(LWOOBJID_EMPTY);
// Handed over to the next chat server (live update): everyone is still online
if (m_Retiring) {
m_Players.clear();
return;
}
while (!m_Players.empty()) {
const auto& [id, playerData] = *m_Players.begin();
Database::Get()->UpdateActivityLog(id, eActivityType::PlayerLoggedOut, playerData.zoneID.GetMapID());
m_Players.erase(m_Players.begin());
}
}