#include "ActivityComponent.h" #include "GameMessages.h" #include "CDClientManager.h" #include "MissionComponent.h" #include "Character.h" #include "dZoneManager.h" #include "ZoneInstanceManager.h" #include "Game.h" #include "Logger.h" #include "WorldPackets.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 "ObjectIDManager.h" #include "CDCurrencyTableTable.h" #include "CDActivityRewardsTable.h" #include "CDActivitiesTable.h" #include "LeaderboardManager.h" #include "CharacterComponent.h" #include "Amf3.h" #include 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. */ m_ActivityID = componentID; LoadActivityData(componentID); if (m_Parent->HasVar(u"activityID")) { m_ActivityID = parent->GetVar(u"activityID"); LoadActivityData(m_ActivityID); } } void ActivityComponent::LoadActivityData(const int32_t activityId) { CDActivitiesTable* activitiesTable = CDClientManager::GetTable(); std::vector 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(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(transferOverride).value_or(m_ActivityInfo.instanceMapID); } } } } void ActivityComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) { outBitStream.Write(m_DirtyActivityInfo); if (m_DirtyActivityInfo) { outBitStream.Write(m_ActivityPlayers.size()); if (!m_ActivityPlayers.empty()) { for (const auto& [playerID, values] : m_ActivityPlayers) { outBitStream.Write(playerID); for (const auto& activityValue : values) { outBitStream.Write(activityValue); } } } if (!bIsInitialUpdate) m_DirtyActivityInfo = false; } } void ActivityComponent::ReloadConfig() { CDActivitiesTable* activitiesTable = CDClientManager::GetTable(); std::vector activities = activitiesTable->Query([this](CDActivities entry) {return (entry.ActivityID == m_ActivityID); }); for (auto activity : activities) { auto mapID = m_ActivityInfo.instanceMapID; if (static_cast(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) { if (PlayerIsInQueue(player)) return; // 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); } else if (!IsPlayedBy(player)) { NewInstance().AddParticipant(player); } } void ActivityComponent::PlayerJoinLobby(Entity* player) { if (!m_Parent->HasComponent(eReplicaComponentType::QUICK_BUILD)) GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby LobbyPlayer newLobbyPlayer{}; newLobbyPlayer.entityID = player->GetObjectID(); LWOOBJID playerLobbyID = LWOOBJID_EMPTY; auto* character = player->GetCharacter(); if (character != nullptr) character->SetLastNonInstanceZoneID(Game::zoneManager->GetZone()->GetWorldID()); for (auto& [lobbyID, lobby] : m_Queue) { if (lobby.players.size() < m_ActivityInfo.maxTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() < m_ActivityInfo.maxTeams) { // If an empty slot in an existing lobby is found lobby.players.push_back(newLobbyPlayer); playerLobbyID = lobbyID; // Update the joining player on players already in the lobby, and update players already in the lobby on the joining player LDFData playerLDF("player", player->GetObjectID()); LDFData playerName("playerName", player->GetCharacter()->GetName()); std::string matchUpdateJoined = playerLDF.GetString() + "\n" + playerName.GetString(); for (const auto& joinedPlayer : lobby.players) { auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) { continue; } LDFData entityLDF("player", entity->GetObjectID()); LDFData entityName("playerName", entity->GetCharacter()->GetName()); std::string matchUpdate = entityLDF.GetString() + "\n" + entityName.GetString(); GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED); PlayerReady(entity, joinedPlayer.ready); GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED); } break; } } if (playerLobbyID == LWOOBJID_EMPTY) { // If all lobbies are full playerLobbyID = ObjectIDManager::GenerateObjectID(); auto& newLobby = m_Queue[playerLobbyID]; newLobby.players.push_back(newLobbyPlayer); newLobby.timer = m_ActivityInfo.waitTime / 1000; } const auto& lobby = m_Queue[playerLobbyID]; if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { // Update the joining player on the match timer LDFData matchTimer("time", lobby.timer); GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimer.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } void ActivityComponent::PlayerLeave(LWOOBJID playerID) { // Removes the player from a lobby and notifies the others, not applicable for non-lobby instances for (auto& lobby : m_Queue | std::views::values) { for (int i = 0; i < lobby.players.size(); i++) { const auto& player = lobby.players[i]; if (player.entityID == playerID) { LDFData matchUpdateLeft("player", playerID); for (const auto& lobbyPlayer : lobby.players) { auto* const entity = lobbyPlayer.GetEntity(); if (entity == nullptr) continue; GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft.GetString(), eMatchUpdate::PLAYER_REMOVED); } lobby.players.erase(lobby.players.begin() + i); return; } } } } void ActivityComponent::Update(float deltaTime) { std::vector lobbiesToRemove{}; // Ticks all the lobbies, not applicable for non-instance activities for (auto& [lobbyID, lobby] : m_Queue) { for (const auto& player : lobby.players) { const auto* const entity = player.GetEntity(); if (entity == nullptr) { PlayerLeave(player.entityID); return; } } if (lobby.players.empty()) { lobbiesToRemove.push_back(lobbyID); continue; } // Update the match time for all players if (m_ActivityInfo.maxTeamSize != 1 && lobby.players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && lobby.players.size() >= m_ActivityInfo.minTeams) { if (lobby.timer == m_ActivityInfo.waitTime / 1000) { for (const auto& joinedPlayer : lobby.players) { auto* const entity = joinedPlayer.GetEntity(); if (entity == nullptr) continue; LDFData matchTimerUpdate("time", lobby.timer); GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_READY); } } lobby.timer -= deltaTime; } bool lobbyReady = true; for (const auto& player : lobby.players) { if (player.ready) continue; lobbyReady = false; } // If everyone's ready, jump the timer if (lobbyReady && lobby.timer > m_ActivityInfo.startDelay / 1000) { lobby.timer = m_ActivityInfo.startDelay / 1000; // Update players in lobby on switch to start delay LDFData matchTimerUpdate("time", lobby.timer); for (const auto& player : lobby.players) { auto* const entity = player.GetEntity(); if (entity == nullptr) continue; GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate.GetString(), eMatchUpdate::PHASE_WAIT_START); } } // The timer has elapsed, start the instance if (lobby.timer <= 0.0f) { LOG("Setting up instance."); auto& instance = NewInstance(); LoadPlayersIntoInstance(instance, lobby.players); instance.StartZone(); lobbiesToRemove.push_back(lobbyID); } } for (const auto id : lobbiesToRemove) { RemoveLobby(id); } } void ActivityComponent::RemoveLobby(const LWOOBJID lobbyID) { if (m_Queue.contains(lobbyID)) m_Queue.erase(lobbyID); } 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::PlayerIsInQueue(Entity* player) { for (const auto& lobby : m_Queue | std::views::values) { for (const auto& lobbyPlayer : lobby.players) { if (player->GetObjectID() == lobbyPlayer.entityID) return true; } } return false; } 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(); 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(); return CheckCost(player) && inventoryComponent && inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount, eInventoryType::ALL); } void ActivityComponent::PlayerReady(Entity* player, bool bReady) { for (auto& lobby : m_Queue | std::views::values) { for (auto& lobbyPlayer : lobby.players) { if (lobbyPlayer.entityID == player->GetObjectID()) { lobbyPlayer.ready = bReady; // Update players in lobby on player being ready LDFData matchReadyUpdate("player", player->GetObjectID()); eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY; if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY; for (const auto& otherPlayer : lobby.players) { auto* const entity = otherPlayer.GetEntity(); if (entity == nullptr) continue; GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate.GetString(), readyStatus); } } } } } ActivityInstance& ActivityComponent::NewInstance() { m_Instances.push_back(ActivityInstance(m_Parent, m_ActivityInfo)); return m_Instances.back(); } void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance& instance, const std::vector& lobby) const { for (const auto& player : lobby) { auto* const entity = player.GetEntity(); if (entity == nullptr || !CheckCost(entity)) { continue; } instance.AddParticipant(entity); } } 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(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(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::StartZone() { if (m_Participants.empty()) return; const auto& participants = GetParticipants(); if (participants.empty()) return; auto* leader = participants[0]; LWOZONEID zoneId = LWOZONEID(m_ActivityInfo.instanceMapID, 0, leader->GetCharacter()->GetPropertyCloneID()); // only make a team if we have more than one participant if (participants.size() > 1) { CBITSTREAM; BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::CREATE_TEAM); bitStream.Write(leader->GetObjectID()); bitStream.Write(m_Participants.size()); for (const auto& participant : m_Participants) { bitStream.Write(participant); } bitStream.Write(zoneId); Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); } const auto cloneId = GeneralUtils::GenerateRandomNumber(1, UINT32_MAX); for (Entity* player : participants) { const auto objid = player->GetObjectID(); ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, m_ActivityInfo.instanceMapID, cloneId, false, [objid](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { auto* player = Game::entityManager->GetEntity(objid); if (player == nullptr) return; 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, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (player->GetCharacter()) { auto* characterComponent = player->GetComponent(); if (characterComponent) { characterComponent->AddVisitedLevel(LWOZONEID(zoneID, LWOINSTANCEID_INVALID, zoneClone)); } player->GetCharacter()->SetZoneID(zoneID); player->GetCharacter()->SetZoneInstance(zoneInstance); player->GetCharacter()->SetZoneClone(zoneClone); } WorldPackets::SendTransferToWorld(player->GetSystemAddress(), serverIP, serverPort, mythranShift); return; }); } m_NextZoneCloneID++; } void ActivityInstance::RewardParticipant(Entity* participant) { auto* missionComponent = participant->GetComponent(); if (missionComponent) { missionComponent->Progress(eMissionTaskType::ACTIVITY, m_ActivityInfo.ActivityID); } // First, get the activity data auto* activityRewardsTable = CDClientManager::GetTable(); std::vector activityRewards = activityRewardsTable->Query([this](CDActivityRewards entry) { return (entry.objectTemplate == m_ActivityInfo.ActivityID); }); if (!activityRewards.empty()) { uint32_t minCoins = 0; uint32_t maxCoins = 0; auto* currencyTableTable = CDClientManager::GetTable(); std::vector currencyTable = currencyTableTable->Query([=](CDCurrencyTable entry) { return (entry.currencyIndex == activityRewards[0].CurrencyIndex && entry.npcminlevel == 1); }); if (!currencyTable.empty()) { minCoins = currencyTable[0].minvalue; maxCoins = currencyTable[0].maxvalue; } Loot::DropLoot(participant, m_Parent->GetObjectID(), activityRewards[0].LootMatrixIndex, minCoins, maxCoins); } } std::vector ActivityInstance::GetParticipants() const { std::vector 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; } Entity* LobbyPlayer::GetEntity() const { return Game::entityManager->GetEntity(entityID); } 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("Score") = activityInstance.GetScore(); instance.PushDebug("Next Zone Clone ID") = activityInstance.GetNextZoneCloneID(); { auto& activityInfo = instance.PushDebug("Activity Info"); const auto& instanceActInfo = activityInstance.GetActivityInfo(); activityInfo.PushDebug("ActivityID") = instanceActInfo.ActivityID; activityInfo.PushDebug("locStatus") = instanceActInfo.locStatus; activityInfo.PushDebug("instanceMapID") = instanceActInfo.instanceMapID; activityInfo.PushDebug("minTeams") = instanceActInfo.minTeams; activityInfo.PushDebug("maxTeams") = instanceActInfo.maxTeams; activityInfo.PushDebug("minTeamSize") = instanceActInfo.minTeamSize; activityInfo.PushDebug("maxTeamSize") = instanceActInfo.maxTeamSize; activityInfo.PushDebug("waitTime") = instanceActInfo.waitTime; activityInfo.PushDebug("startDelay") = instanceActInfo.startDelay; activityInfo.PushDebug("requiresUniqueData") = instanceActInfo.requiresUniqueData; activityInfo.PushDebug("leaderboardType") = instanceActInfo.leaderboardType; activityInfo.PushDebug("localize") = instanceActInfo.localize; activityInfo.PushDebug("optionalCostLOT") = instanceActInfo.optionalCostLOT; activityInfo.PushDebug("optionalCostCount") = instanceActInfo.optionalCostCount; activityInfo.PushDebug("showUIRewards") = instanceActInfo.showUIRewards; activityInfo.PushDebug("CommunityActivityFlagID") = instanceActInfo.CommunityActivityFlagID; activityInfo.PushDebug("gate_version") = instanceActInfo.gate_version; activityInfo.PushDebug("noTeamLootOnDeath") = instanceActInfo.noTeamLootOnDeath; activityInfo.PushDebug("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(std::to_string(participant->GetObjectID()) + ": " + character->GetName()) = ""; } } auto& queue = activityInfo.PushDebug("Queue"); i = 0; for (const auto& lobbyQueue : m_Queue | std::views::values) { auto& lobby = queue.PushDebug("Lobby " + std::to_string(i++)); lobby.PushDebug("Timer") = lobbyQueue.timer; auto& players = lobby.PushDebug("Players"); for (const auto& player : lobbyQueue.players) { const auto* const playerEntity = player.GetEntity(); if (!playerEntity) continue; auto* character = playerEntity->GetCharacter(); if (!character) continue; players.PushDebug(std::to_string(playerEntity->GetObjectID()) + ": " + character->GetName()) = player.ready ? "Ready" : "Not Ready"; } } 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(std::to_string(i)) = playerScores[i]; } } activityInfo.PushDebug("ActivityID") = m_ActivityID; return true; }