mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 15:57:13 +00:00
LWOActivityComponent::Deserialize (0x00d002b0) reads a flag, the player count and each player with its 10 values; a construction is the only way a client coming into range learns who is in the activity. The block was written only while it was waiting for the next update. Live finish line constructions (LOT 4967) had the flag and the count in 40 of 40, one of them with a player in it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
445 lines
17 KiB
C++
445 lines
17 KiB
C++
#include "ActivityComponent.h"
|
|
#include "ChatServerLink.h"
|
|
#include "ChatPackets.h"
|
|
#include "GameMessages.h"
|
|
#include "ActivityMessages.h"
|
|
#include "CDClientManager.h"
|
|
#include "MissionComponent.h"
|
|
#include "Character.h"
|
|
#include "dZoneManager.h"
|
|
#include "Game.h"
|
|
#include "Logger.h"
|
|
#include "ClientPackets.h"
|
|
#include "EntityManager.h"
|
|
#include "ChatPackets.h"
|
|
#include "BitStreamUtils.h"
|
|
#include "dServer.h"
|
|
#include "GeneralUtils.h"
|
|
#include "dZoneManager.h"
|
|
#include "dConfig.h"
|
|
#include "InventoryComponent.h"
|
|
#include "DestroyableComponent.h"
|
|
#include "Loot.h"
|
|
#include "eMissionTaskType.h"
|
|
#include "eMatchUpdate.h"
|
|
#include "ServiceType.h"
|
|
#include "MessageType/Chat.h"
|
|
|
|
#include "CDActivityRewardsTable.h"
|
|
#include "CDActivitiesTable.h"
|
|
#include "LeaderboardManager.h"
|
|
#include "CharacterComponent.h"
|
|
#include "Amf3.h"
|
|
#include <ranges>
|
|
|
|
namespace {
|
|
const ActivityInstance g_EmptyInstance{ nullptr, CDActivities{} };
|
|
}
|
|
|
|
ActivityComponent::ActivityComponent(Entity* parent, int32_t componentID) : Component(parent, componentID) {
|
|
RegisterMsg(&ActivityComponent::OnGetObjectReportInfo);
|
|
}
|
|
|
|
/*
|
|
* This is precisely what the client does functionally
|
|
* Use the component id as the default activity id and load its data from the database
|
|
* if activityID is specified and if that column exists in the activities table, update the activity info with that data.
|
|
*/
|
|
void ActivityComponent::LoadDataFromTemplate() {
|
|
m_ActivityID = m_ComponentID;
|
|
LoadActivityData(m_ComponentID);
|
|
}
|
|
|
|
// The level config's activityID, when there, replaces it
|
|
void ActivityComponent::LoadConfigData() {
|
|
if (m_Parent->HasVar(u"activityID")) {
|
|
m_ActivityID = m_Parent->GetVar<int32_t>(u"activityID");
|
|
LoadActivityData(m_ActivityID);
|
|
}
|
|
}
|
|
void ActivityComponent::LoadActivityData(const int32_t activityId) {
|
|
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
|
|
std::vector<CDActivities> activities = activitiesTable->Query([activityId](CDActivities entry) {return (entry.ActivityID == activityId); });
|
|
|
|
bool soloRacing = Game::config->GetValue("solo_racing") == "1";
|
|
for (CDActivities activity : activities) {
|
|
m_ActivityInfo = activity;
|
|
if (static_cast<Leaderboard::Type>(activity.leaderboardType) == Leaderboard::Type::Racing && soloRacing) {
|
|
m_ActivityInfo.minTeamSize = 1;
|
|
m_ActivityInfo.minTeams = 1;
|
|
}
|
|
if (m_ActivityInfo.instanceMapID == -1) {
|
|
const auto& transferOverride = m_Parent->GetVarAsString(u"transferZoneID");
|
|
if (!transferOverride.empty()) {
|
|
m_ActivityInfo.instanceMapID =
|
|
GeneralUtils::TryParse<uint32_t>(transferOverride).value_or(m_ActivityInfo.instanceMapID);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
// A construction always carries the players: it is the only way a client coming into range learns who is in the
|
|
// activity (LWOActivityComponent::Deserialize 0x00d002b0), and live wrote the block, empty or not, on every one
|
|
const bool writeActivityInfo = m_DirtyActivityInfo || bIsInitialUpdate;
|
|
outBitStream.Write(writeActivityInfo);
|
|
if (writeActivityInfo) {
|
|
outBitStream.Write<uint32_t>(m_ActivityPlayers.size());
|
|
if (!m_ActivityPlayers.empty()) {
|
|
for (const auto& [playerID, values] : m_ActivityPlayers) {
|
|
outBitStream.Write<LWOOBJID>(playerID);
|
|
for (const auto& activityValue : values) {
|
|
outBitStream.Write<float_t>(activityValue);
|
|
}
|
|
}
|
|
}
|
|
if (!bIsInitialUpdate) m_DirtyActivityInfo = false;
|
|
}
|
|
}
|
|
|
|
void ActivityComponent::ReloadConfig() {
|
|
CDActivitiesTable* activitiesTable = CDClientManager::GetTable<CDActivitiesTable>();
|
|
std::vector<CDActivities> activities = activitiesTable->Query([this](CDActivities entry) {return (entry.ActivityID == m_ActivityID); });
|
|
for (auto activity : activities) {
|
|
auto mapID = m_ActivityInfo.instanceMapID;
|
|
if (static_cast<Leaderboard::Type>(activity.leaderboardType) == Leaderboard::Type::Racing && Game::config->GetValue("solo_racing") == "1") {
|
|
m_ActivityInfo.minTeamSize = 1;
|
|
m_ActivityInfo.minTeams = 1;
|
|
} else {
|
|
m_ActivityInfo.minTeamSize = activity.minTeamSize;
|
|
m_ActivityInfo.minTeams = activity.minTeams;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ActivityComponent::HandleMessageBoxResponse(Entity* player, const std::string& id) {
|
|
if (id == "LobbyExit") {
|
|
PlayerLeave(player->GetObjectID());
|
|
} else if (id == "PlayButton") {
|
|
PlayerJoin(player);
|
|
}
|
|
}
|
|
|
|
void ActivityComponent::PlayerJoin(Entity* player, const std::string& playerChoices) {
|
|
// If we have a lobby, queue the player and allow others to join, otherwise spin up an instance on the spot
|
|
if (HasLobby()) {
|
|
PlayerJoinLobby(player, playerChoices);
|
|
} else if (!IsPlayedBy(player)) {
|
|
NewInstance().AddParticipant(player);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
void SendMatchRequest(const LWOOBJID playerID, const ChatPackets::eMatchRequestType type, const int32_t value) {
|
|
ChatPackets::MatchRequest request;
|
|
request.playerID = playerID;
|
|
request.type = type;
|
|
request.value = value;
|
|
ChatServerLink::Send(request);
|
|
}
|
|
}
|
|
|
|
void ActivityComponent::PlayerJoinLobby(Entity* player, const std::string& playerChoices) {
|
|
if (!m_Parent->HasComponent(eReplicaComponentType::QUICK_BUILD)) {
|
|
GameMessages::MatchResponse matchResponse;
|
|
matchResponse.target = player->GetObjectID();
|
|
matchResponse.response = 0;
|
|
matchResponse.SendToClient(player->GetSystemAddress()); // tell the client they joined a lobby
|
|
}
|
|
|
|
auto* character = player->GetCharacter();
|
|
if (character != nullptr)
|
|
character->SetLastNonInstanceZoneID(Game::zoneManager->GetZone()->GetWorldID());
|
|
|
|
// The lobbies are the chat server's, so players of every instance of this zone wait together (docs/Matchmaking.md).
|
|
// It gets what this world read from the activity, overrides (solo racing, transfer zone) included.
|
|
ChatPackets::MatchRequest request;
|
|
request.playerID = player->GetObjectID();
|
|
request.type = ChatPackets::eMatchRequestType::JOIN;
|
|
request.activityID = m_ActivityInfo.ActivityID;
|
|
request.playerName = character != nullptr ? character->GetName() : "";
|
|
request.playerChoices = playerChoices;
|
|
request.instanceMapID = m_ActivityInfo.instanceMapID;
|
|
request.minTeams = m_ActivityInfo.minTeams;
|
|
request.maxTeams = m_ActivityInfo.maxTeams;
|
|
request.minTeamSize = m_ActivityInfo.minTeamSize;
|
|
request.maxTeamSize = m_ActivityInfo.maxTeamSize;
|
|
request.waitTime = m_ActivityInfo.waitTime;
|
|
request.startDelay = m_ActivityInfo.startDelay;
|
|
ChatServerLink::Send(request);
|
|
}
|
|
|
|
void ActivityComponent::PlayerLeave(LWOOBJID playerID) {
|
|
// Not applicable for non-lobby instances
|
|
if (!HasLobby()) return;
|
|
SendMatchRequest(playerID, ChatPackets::eMatchRequestType::LEAVE, 0);
|
|
}
|
|
|
|
void ActivityComponent::StartMatch(const ChatPackets::MatchTransfer& transfer) {
|
|
auto& instance = NewInstance();
|
|
for (const auto playerID : transfer.players) {
|
|
auto* const entity = Game::entityManager->GetEntity(playerID);
|
|
if (entity == nullptr || !CheckCost(entity)) {
|
|
continue;
|
|
}
|
|
|
|
instance.AddParticipant(entity);
|
|
}
|
|
instance.TransferParticipants(transfer);
|
|
}
|
|
|
|
bool ActivityComponent::HasLobby() const {
|
|
// If the player is not in the world he has to be, create a lobby for the transfer
|
|
return m_ActivityInfo.instanceMapID != UINT_MAX && m_ActivityInfo.instanceMapID != Game::server->GetZoneID();
|
|
}
|
|
|
|
bool ActivityComponent::IsPlayedBy(Entity* player) const {
|
|
for (const auto& instance : m_Instances) {
|
|
for (const auto* instancePlayer : instance.GetParticipants()) {
|
|
if (instancePlayer != nullptr && instancePlayer->GetObjectID() == player->GetObjectID())
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ActivityComponent::IsPlayedBy(LWOOBJID playerID) const {
|
|
for (const auto& instance : m_Instances) {
|
|
for (const auto* instancePlayer : instance.GetParticipants()) {
|
|
if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID)
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ActivityComponent::CheckCost(Entity* player) const {
|
|
if (m_ActivityInfo.optionalCostLOT <= 0 || m_ActivityInfo.optionalCostCount <= 0)
|
|
return true;
|
|
|
|
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
|
if (inventoryComponent == nullptr)
|
|
return false;
|
|
|
|
if (inventoryComponent->GetLotCount(m_ActivityInfo.optionalCostLOT) < m_ActivityInfo.optionalCostCount)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ActivityComponent::TakeCost(Entity* player) const {
|
|
auto* inventoryComponent = player->GetComponent<InventoryComponent>();
|
|
return CheckCost(player) && inventoryComponent && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL);
|
|
}
|
|
|
|
void ActivityComponent::PlayerReady(Entity* player, bool bReady) {
|
|
SendMatchRequest(player->GetObjectID(), ChatPackets::eMatchRequestType::READY, bReady ? 1 : 0);
|
|
}
|
|
|
|
ActivityInstance& ActivityComponent::NewInstance() {
|
|
m_Instances.push_back(ActivityInstance(m_Parent, m_ActivityInfo));
|
|
return m_Instances.back();
|
|
}
|
|
|
|
const ActivityInstance& ActivityComponent::GetInstance(const LWOOBJID playerID) const {
|
|
for (const auto& instance : m_Instances) {
|
|
for (const auto* participant : instance.GetParticipants()) {
|
|
if (participant->GetObjectID() == playerID)
|
|
return instance;
|
|
}
|
|
}
|
|
|
|
return g_EmptyInstance;
|
|
}
|
|
|
|
bool ActivityComponent::PlayerHasActivityData(LWOOBJID playerID) const {
|
|
return m_ActivityPlayers.contains(playerID);
|
|
}
|
|
|
|
void ActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) {
|
|
m_ActivityPlayers.erase(playerID);
|
|
m_DirtyActivityInfo = true;
|
|
}
|
|
|
|
float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) const {
|
|
float value = -1.0f;
|
|
|
|
const auto& data = m_ActivityPlayers.find(playerID);
|
|
if (data != m_ActivityPlayers.cend()) {
|
|
value = data->second[std::min(index, static_cast<uint32_t>(9))];
|
|
}
|
|
LOG_DEBUG("Player %llu has score %f at index %i", playerID, value, index);
|
|
return value;
|
|
}
|
|
|
|
void ActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) {
|
|
auto& data = m_ActivityPlayers[playerID];
|
|
data[std::min(index, static_cast<uint32_t>(9))] = value;
|
|
LOG_DEBUG("%llu index %i has score of %f", playerID, index, value);
|
|
m_DirtyActivityInfo = true;
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
}
|
|
|
|
void ActivityComponent::PlayerRemove(LWOOBJID playerID) {
|
|
for (int i = 0; i < m_Instances.size(); i++) {
|
|
auto& instance = m_Instances[i];
|
|
auto participants = instance.GetParticipants();
|
|
for (const auto* participant : participants) {
|
|
if (participant != nullptr && participant->GetObjectID() == playerID) {
|
|
instance.RemoveParticipant(participant);
|
|
RemoveActivityPlayerData(playerID);
|
|
|
|
// If the instance is empty after the delete of the participant, delete the instance too
|
|
if (instance.GetParticipants().empty()) {
|
|
m_Instances.erase(m_Instances.begin() + i);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ActivityInstance::TransferParticipants(const ChatPackets::MatchTransfer& transfer) {
|
|
const auto zoneID = transfer.zoneID.GetMapID();
|
|
const auto zoneInstance = transfer.zoneID.GetInstanceID();
|
|
const auto zoneClone = transfer.zoneID.GetCloneID();
|
|
for (Entity* player : GetParticipants()) {
|
|
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", player->GetCharacter()->GetName().c_str(), zoneID, zoneInstance, zoneClone, transfer.mythranShift == true ? "true" : "false", transfer.serverIP.c_str(), transfer.serverPort);
|
|
if (player->GetCharacter()) {
|
|
auto* characterComponent = player->GetComponent<CharacterComponent>();
|
|
if (characterComponent) {
|
|
characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone));
|
|
}
|
|
|
|
player->GetCharacter()->SetZoneID(zoneID);
|
|
player->GetCharacter()->SetZoneInstance(zoneInstance);
|
|
player->GetCharacter()->SetZoneClone(zoneClone);
|
|
}
|
|
|
|
ClientPackets::TransferToWorld transferToWorld;
|
|
transferToWorld.serverIP = LUString(transfer.serverIP);
|
|
transferToWorld.serverPort = transfer.serverPort;
|
|
transferToWorld.mythranShift = transfer.mythranShift;
|
|
transferToWorld.Send(player->GetSystemAddress());
|
|
}
|
|
|
|
m_NextZoneCloneID++;
|
|
}
|
|
|
|
void ActivityInstance::RewardParticipant(Entity* participant) {
|
|
auto* missionComponent = participant->GetComponent<MissionComponent>();
|
|
if (missionComponent) {
|
|
missionComponent->Progress(eMissionTaskType::ACTIVITY, m_ActivityInfo.ActivityID);
|
|
}
|
|
|
|
// First, get the activity data
|
|
auto* activityRewardsTable = CDClientManager::GetTable<CDActivityRewardsTable>();
|
|
std::vector<CDActivityRewards> activityRewards = activityRewardsTable->Query([this](CDActivityRewards entry) { return (entry.objectTemplate == m_ActivityInfo.ActivityID); });
|
|
|
|
if (!activityRewards.empty()) {
|
|
const auto [minCoins, maxCoins] = Loot::GetActivityCoinRange(activityRewards[0]);
|
|
|
|
Loot::DropLoot(participant, m_Parent->GetObjectID(), activityRewards[0].LootMatrixIndex, minCoins, maxCoins);
|
|
}
|
|
}
|
|
|
|
std::vector<Entity*> ActivityInstance::GetParticipants() const {
|
|
std::vector<Entity*> entities;
|
|
entities.reserve(m_Participants.size());
|
|
|
|
for (const auto& id : m_Participants) {
|
|
auto* entity = Game::entityManager->GetEntity(id);
|
|
if (entity != nullptr)
|
|
entities.push_back(entity);
|
|
}
|
|
|
|
return entities;
|
|
}
|
|
|
|
void ActivityInstance::AddParticipant(Entity* participant) {
|
|
const auto id = participant->GetObjectID();
|
|
if (std::count(m_Participants.begin(), m_Participants.end(), id))
|
|
return;
|
|
|
|
m_Participants.push_back(id);
|
|
}
|
|
|
|
void ActivityInstance::RemoveParticipant(const Entity* participant) {
|
|
const auto loadedParticipant = std::find(m_Participants.begin(), m_Participants.end(), participant->GetObjectID());
|
|
if (loadedParticipant != m_Participants.end()) {
|
|
m_Participants.erase(loadedParticipant);
|
|
}
|
|
}
|
|
|
|
uint32_t ActivityInstance::GetScore() const {
|
|
return score;
|
|
}
|
|
|
|
void ActivityInstance::SetScore(uint32_t score) {
|
|
this->score = score;
|
|
}
|
|
|
|
bool ActivityComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo) {
|
|
auto& activityInfo = reportInfo.info->PushDebug("Activity");
|
|
|
|
auto& instances = activityInfo.PushDebug("Instances: " + std::to_string(m_Instances.size()));
|
|
size_t i = 0;
|
|
for (const auto& activityInstance : m_Instances) {
|
|
auto& instance = instances.PushDebug("Instance " + std::to_string(i++));
|
|
instance.PushDebug<AMFIntValue>("Score") = activityInstance.GetScore();
|
|
instance.PushDebug<AMFIntValue>("Next Zone Clone ID") = activityInstance.GetNextZoneCloneID();
|
|
|
|
{
|
|
auto& activityInfo = instance.PushDebug("Activity Info");
|
|
const auto& instanceActInfo = activityInstance.GetActivityInfo();
|
|
activityInfo.PushDebug<AMFIntValue>("ActivityID") = instanceActInfo.ActivityID;
|
|
activityInfo.PushDebug<AMFIntValue>("locStatus") = instanceActInfo.locStatus;
|
|
activityInfo.PushDebug<AMFIntValue>("instanceMapID") = instanceActInfo.instanceMapID;
|
|
activityInfo.PushDebug<AMFIntValue>("minTeams") = instanceActInfo.minTeams;
|
|
activityInfo.PushDebug<AMFIntValue>("maxTeams") = instanceActInfo.maxTeams;
|
|
activityInfo.PushDebug<AMFIntValue>("minTeamSize") = instanceActInfo.minTeamSize;
|
|
activityInfo.PushDebug<AMFIntValue>("maxTeamSize") = instanceActInfo.maxTeamSize;
|
|
activityInfo.PushDebug<AMFIntValue>("waitTime") = instanceActInfo.waitTime;
|
|
activityInfo.PushDebug<AMFIntValue>("startDelay") = instanceActInfo.startDelay;
|
|
activityInfo.PushDebug<AMFBoolValue>("requiresUniqueData") = instanceActInfo.requiresUniqueData;
|
|
activityInfo.PushDebug<AMFIntValue>("leaderboardType") = instanceActInfo.leaderboardType;
|
|
activityInfo.PushDebug<AMFBoolValue>("localize") = instanceActInfo.localize;
|
|
activityInfo.PushDebug<AMFIntValue>("optionalCostLOT") = instanceActInfo.optionalCostLOT;
|
|
activityInfo.PushDebug<AMFIntValue>("optionalCostCount") = instanceActInfo.optionalCostCount;
|
|
activityInfo.PushDebug<AMFBoolValue>("showUIRewards") = instanceActInfo.showUIRewards;
|
|
activityInfo.PushDebug<AMFIntValue>("CommunityActivityFlagID") = instanceActInfo.CommunityActivityFlagID;
|
|
activityInfo.PushDebug<AMFStringValue>("gate_version") = instanceActInfo.gate_version;
|
|
activityInfo.PushDebug<AMFBoolValue>("noTeamLootOnDeath") = instanceActInfo.noTeamLootOnDeath;
|
|
activityInfo.PushDebug<AMFDoubleValue>("optionalPercentage") = instanceActInfo.optionalPercentage;
|
|
}
|
|
|
|
auto& participants = instance.PushDebug("Participants");
|
|
for (const auto* participant : activityInstance.GetParticipants()) {
|
|
if (!participant) continue;
|
|
auto* character = participant->GetCharacter();
|
|
if (!character) continue;
|
|
participants.PushDebug<AMFStringValue>(std::to_string(participant->GetObjectID()) + ": " + character->GetName()) = "";
|
|
}
|
|
}
|
|
|
|
auto& activityPlayers = activityInfo.PushDebug("Activity Players");
|
|
for (const auto& [playerID, playerScores] : m_ActivityPlayers) {
|
|
auto* const activityPlayerEntity = Game::entityManager->GetEntity(playerID);
|
|
if (!activityPlayerEntity) continue;
|
|
auto* character = activityPlayerEntity->GetCharacter();
|
|
if (!character) continue;
|
|
|
|
auto& playerData = activityPlayers.PushDebug(std::to_string(playerID) + " " + character->GetName());
|
|
|
|
auto& scores = playerData.PushDebug("Scores");
|
|
for (size_t i = 0; i < 10; ++i) {
|
|
scores.PushDebug<AMFDoubleValue>(std::to_string(i)) = playerScores[i];
|
|
}
|
|
}
|
|
|
|
activityInfo.PushDebug<AMFIntValue>("ActivityID") = m_ActivityID;
|
|
return true;
|
|
}
|