diff --git a/dGame/dComponents/ActivityComponent.cpp b/dGame/dComponents/ActivityComponent.cpp index 8f7f286a..13c362f7 100644 --- a/dGame/dComponents/ActivityComponent.cpp +++ b/dGame/dComponents/ActivityComponent.cpp @@ -1,5 +1,621 @@ #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 "dLogger.h" +#include +#include "EntityManager.h" +#include "ChatPackets.h" +#include "Player.h" +#include "PacketUtils.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 "eConnectionType.h" +#include "eChatInternalMessageType.h" + +#include "CDCurrencyTableTable.h" +#include "CDActivityRewardsTable.h" +#include "CDActivitiesTable.h" +#include "LeaderboardManager.h" ActivityComponent::ActivityComponent(Entity* parent) : Component(parent) { + m_ActivityID = activityID; + CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); + std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.ActivityID == m_ActivityID); }); + for (CDActivities activity : activities) { + m_ActivityInfo = activity; + if (static_cast(activity.leaderboardType) == LeaderboardType::Racing && Game::config->GetValue("solo_racing") == "1") { + m_ActivityInfo.minTeamSize = 1; + m_ActivityInfo.minTeams = 1; + } + + const auto& transferOverride = parent->GetVar(u"transferZoneID"); + if (!transferOverride.empty()) { + m_ActivityInfo.instanceMapID = std::stoi(GeneralUtils::UTF16ToWTF8(transferOverride)); + + // TODO: LU devs made me do it (for some reason cannon cove instancer is marked to go to GF survival) + // NOTE: 1301 is GF survival + if (m_ActivityInfo.instanceMapID == 1301) { + m_ActivityInfo.instanceMapID = 1302; + } + } + } + + auto* destroyableComponent = m_ParentEntity->GetComponent(); + + if (destroyableComponent) { + // check for LMIs and set the loot LMIs + CDActivityRewardsTable* activityRewardsTable = CDClientManager::Instance().GetTable(); + std::vector activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) {return (entry.LootMatrixIndex == destroyableComponent->GetLootMatrixID()); }); + + uint32_t startingLMI = 0; + + if (activityRewards.size() > 0) { + startingLMI = activityRewards[0].LootMatrixIndex; + } + + if (startingLMI > 0) { + // now time for bodge :) + + std::vector objectTemplateActivities = activityRewardsTable->Query([=](CDActivityRewards entry) {return (activityRewards[0].objectTemplate == entry.objectTemplate); }); + for (const auto& item : objectTemplateActivities) { + if (item.activityRating > 0 && item.activityRating < 5) { + m_ActivityLootMatrices.insert({ item.activityRating, item.LootMatrixIndex }); + } + } + } + } +} + +ActivityComponent::~ActivityComponent() += default; + +void ActivityComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) const { + outBitStream->Write(true); + outBitStream->Write(m_ActivityPlayers.size()); + + if (!m_ActivityPlayers.empty()) { + for (const auto& activityPlayer : m_ActivityPlayers) { + + outBitStream->Write(activityPlayer->playerID); + for (const auto& activityValue : activityPlayer->values) { + outBitStream->Write(activityValue); + } + } + } +} + +void ActivityComponent::ReloadConfig() { + CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); + std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.ActivityID == m_ActivityID); }); + for (auto activity : activities) { + auto mapID = m_ActivityInfo.instanceMapID; + if ((mapID == 1203 || mapID == 1261 || mapID == 1303 || mapID == 1403) && 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 (m_ActivityInfo.ActivityID == 103) { + return; + } + + if (id == "LobbyExit") { + PlayerLeave(player->GetObjectID()); + } else if (id == "PlayButton") { + PlayerJoin(player); + } +} + +void ActivityComponent::PlayerJoin(Entity* player) { + if (m_ActivityInfo.ActivityID == 103 || PlayerIsInQueue(player) || !IsValidActivity(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)) { + auto* instance = NewInstance(); + instance->AddParticipant(player); + } + + EntityManager::Instance()->SerializeEntity(m_ParentEntity); +} + +void ActivityComponent::PlayerJoinLobby(Entity* player) { + if (!m_ParentEntity->HasComponent(eReplicaComponentType::QUICK_BUILD)) + GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby + LobbyPlayer* newLobbyPlayer = new LobbyPlayer(); + newLobbyPlayer->entityID = player->GetObjectID(); + Lobby* playerLobby = nullptr; + + auto* character = player->GetCharacter(); + if (character != nullptr) + character->SetLastNonInstanceZoneID(dZoneManager::Instance()->GetZone()->GetWorldID()); + + for (Lobby* 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); + playerLobby = lobby; + + // Update the joining player on players already in the lobby, and update players already in the lobby on the joining player + std::string matchUpdateJoined = "player=9:" + std::to_string(player->GetObjectID()) + "\nplayerName=0:" + player->GetCharacter()->GetName(); + for (LobbyPlayer* joinedPlayer : lobby->players) { + auto* entity = joinedPlayer->GetEntity(); + + if (entity == nullptr) { + continue; + } + + std::string matchUpdate = "player=9:" + std::to_string(entity->GetObjectID()) + "\nplayerName=0:" + entity->GetCharacter()->GetName(); + GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED); + PlayerReady(entity, joinedPlayer->ready); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED); + } + } + } + + if (!playerLobby) { + // If all lobbies are full + playerLobby = new Lobby(); + playerLobby->players.push_back(newLobbyPlayer); + playerLobby->timer = m_ActivityInfo.waitTime / 1000; + m_Queue.push_back(playerLobby); + } + + if (m_ActivityInfo.maxTeamSize != 1 && playerLobby->players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && playerLobby->players.size() >= m_ActivityInfo.minTeams) { + // Update the joining player on the match timer + std::string matchTimerUpdate = "time=3:" + std::to_string(playerLobby->timer); + GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimerUpdate, 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 (Lobby* lobby : m_Queue) { + for (int i = 0; i < lobby->players.size(); ++i) { + if (lobby->players[i]->entityID == playerID) { + std::string matchUpdateLeft = "player=9:" + std::to_string(playerID); + for (LobbyPlayer* lobbyPlayer : lobby->players) { + auto* entity = lobbyPlayer->GetEntity(); + if (entity == nullptr) + continue; + + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft, eMatchUpdate::PLAYER_REMOVED); + } + + delete lobby->players[i]; + lobby->players[i] = nullptr; + 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 (Lobby* lobby : m_Queue) { + for (LobbyPlayer* player : lobby->players) { + auto* entity = player->GetEntity(); + if (entity == nullptr) { + PlayerLeave(player->entityID); + return; + } + } + + if (lobby->players.empty()) { + lobbiesToRemove.push_back(lobby); + 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 (LobbyPlayer* joinedPlayer : lobby->players) { + auto* entity = joinedPlayer->GetEntity(); + + if (entity == nullptr) + continue; + + std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); + } + } + + lobby->timer -= deltaTime; + } + + bool lobbyReady = true; + for (LobbyPlayer* 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 + std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); + for (LobbyPlayer* player : lobby->players) { + auto* entity = player->GetEntity(); + + if (entity == nullptr) + continue; + + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_START); + } + } + + // The timer has elapsed, start the instance + if (lobby->timer <= 0.0f) { + Game::logger->Log("ActivityComponent", "Setting up instance."); + ActivityInstance* instance = NewInstance(); + LoadPlayersIntoInstance(instance, lobby->players); + instance->StartZone(); + lobbiesToRemove.push_back(lobby); + } + } + + while (!lobbiesToRemove.empty()) { + RemoveLobby(lobbiesToRemove.front()); + lobbiesToRemove.erase(lobbiesToRemove.begin()); + } +} + +void ActivityComponent::RemoveLobby(Lobby* lobby) { + for (int i = 0; i < m_Queue.size(); ++i) { + if (m_Queue[i] == lobby) { + m_Queue.erase(m_Queue.begin() + i); + return; + } + } +} + +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::IsValidActivity(Entity* player) { + // Makes it so that scripted activities with an unimplemented map cannot be joined + /*if (player->GetGMLevel() < eGameMasterLevel::DEVELOPER && (m_ActivityInfo.instanceMapID == 1302 || m_ActivityInfo.instanceMapID == 1301)) { + if (m_ParentEntity->GetLOT() == 4860) { + auto* missionComponent = player->GetComponent(); + missionComponent->CompleteMission(229); + } + + ChatPackets::SendSystemMessage(player->GetSystemAddress(), u"Sorry, this activity is not ready."); + static_cast(player)->SendToZone(dZoneManager::Instance()->GetZone()->GetWorldID()); // Gets them out of this stuck state + + return false; + }*/ + + return true; +} + +bool ActivityComponent::PlayerIsInQueue(Entity* player) { + for (Lobby* lobby : m_Queue) { + for (LobbyPlayer* lobbyPlayer : lobby->players) { + if (player->GetObjectID() == lobbyPlayer->entityID) return true; + } + } + + return false; +} + +bool ActivityComponent::IsPlayedBy(Entity* player) const { + for (const auto* instance : this->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 : this->m_Instances) { + for (const auto* instancePlayer : instance->GetParticipants()) { + if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID) + return true; + } + } + + return false; +} + +bool ActivityComponent::TakeCost(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; + + inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount); + + return true; +} + +void ActivityComponent::PlayerReady(Entity* player, bool bReady) { + for (Lobby* lobby : m_Queue) { + for (LobbyPlayer* lobbyPlayer : lobby->players) { + if (lobbyPlayer->entityID == player->GetObjectID()) { + + lobbyPlayer->ready = bReady; + + // Update players in lobby on player being ready + std::string matchReadyUpdate = "player=9:" + std::to_string(player->GetObjectID()); + eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY; + if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY; + for (LobbyPlayer* otherPlayer : lobby->players) { + auto* entity = otherPlayer->GetEntity(); + if (entity == nullptr) + continue; + + GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate, readyStatus); + } + } + } + } +} + +ActivityInstance* ActivityComponent::NewInstance() { + auto* instance = new ActivityInstance(m_ParentEntity, m_ActivityInfo); + m_Instances.push_back(instance); + return instance; +} + +void ActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const { + for (LobbyPlayer* player : lobby) { + auto* entity = player->GetEntity(); + if (entity == nullptr || !TakeCost(entity)) { + continue; + } + + instance->AddParticipant(entity); + } +} + +const std::vector& ActivityComponent::GetInstances() const { + return m_Instances; +} + +ActivityInstance* ActivityComponent::GetInstance(const LWOOBJID playerID) { + for (const auto* instance : GetInstances()) { + for (const auto* participant : instance->GetParticipants()) { + if (participant->GetObjectID() == playerID) + return const_cast(instance); + } + } + + return nullptr; +} + +void ActivityComponent::ClearInstances() { + for (ActivityInstance* instance : m_Instances) { + delete instance; + } + m_Instances.clear(); +} + +ActivityPlayer* ActivityComponent::GetActivityPlayerData(LWOOBJID playerID) { + for (auto* activityData : m_ActivityPlayers) { + if (activityData->playerID == playerID) { + return activityData; + } + } + + return nullptr; +} + +void ActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) { + for (size_t i = 0; i < m_ActivityPlayers.size(); i++) { + if (m_ActivityPlayers[i]->playerID == playerID) { + delete m_ActivityPlayers[i]; + m_ActivityPlayers[i] = nullptr; + + m_ActivityPlayers.erase(m_ActivityPlayers.begin() + i); + EntityManager::Instance()->SerializeEntity(m_ParentEntity); + + return; + } + } +} + +ActivityPlayer* ActivityComponent::AddActivityPlayerData(LWOOBJID playerID) { + auto* data = GetActivityPlayerData(playerID); + if (data != nullptr) + return data; + + m_ActivityPlayers.push_back(new ActivityPlayer{ playerID, {} }); + EntityManager::Instance()->SerializeEntity(m_ParentEntity); + + return GetActivityPlayerData(playerID); +} + +float_t ActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) { + auto value = -1.0f; + + auto* data = GetActivityPlayerData(playerID); + if (data != nullptr) { + value = data->values[std::min(index, (uint32_t)9)]; + } + + return value; +} + +void ActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) { + auto* data = AddActivityPlayerData(playerID); + if (data != nullptr) { + data->values[std::min(index, (uint32_t)9)] = value; + } + + EntityManager::Instance()->SerializeEntity(m_ParentEntity); +} + +void ActivityComponent::PlayerRemove(LWOOBJID playerID) { + for (auto* instance : GetInstances()) { + 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(std::find(m_Instances.begin(), m_Instances.end(), instance)); + delete instance; + } + 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; + PacketUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::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 = EntityManager::Instance()->GetEntity(objid); + if (player == nullptr) + return; + + Game::logger->Log("UserManager", "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()) { + 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::Instance().GetTable(); + std::vector activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) { return (entry.objectTemplate == m_ActivityInfo.ActivityID); }); + + if (!activityRewards.empty()) { + uint32_t minCoins = 0; + uint32_t maxCoins = 0; + + auto* currencyTableTable = CDClientManager::Instance().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; + } + + LootGenerator::Instance().DropLoot(participant, m_ParentEntity, 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 = EntityManager::Instance()->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 EntityManager::Instance()->GetEntity(entityID); } diff --git a/dGame/dComponents/ActivityComponent.h b/dGame/dComponents/ActivityComponent.h index 58c5b522..587f982a 100644 --- a/dGame/dComponents/ActivityComponent.h +++ b/dGame/dComponents/ActivityComponent.h @@ -1,17 +1,379 @@ -#ifndef __ACTIVITYCOMPONENT__H__ -#define __ACTIVITYCOMPONENT__H__ +/* + * Darkflame Universe + * Copyright 2018 + */ +#include "CDClientManager.h" + +#ifndef ACTIVITYCOMPONENT_H +#define ACTIVITYCOMPONENT_H + +#include "BitStream.h" +#include "Entity.h" #include "Component.h" #include "eReplicaComponentType.h" -class Entity; +#include "CDActivitiesTable.h" -class ActivityComponent : public Component { + /** + * Represents an instance of an activity, having participants and score + */ +class ActivityInstance { public: - inline static const eReplicaComponentType ComponentType = eReplicaComponentType::INVALID; - ActivityComponent(Entity* parent); + ActivityInstance(Entity* parent, CDActivities activityInfo) { m_ParentEntity = parent; m_ActivityInfo = activityInfo; }; + //~ActivityInstance(); + + /** + * Adds an entity to this activity + * @param participant the entity to add + */ + void AddParticipant(Entity* participant); + + /** + * Removes all the participants from this activity + */ + void ClearParticipants() { m_Participants.clear(); }; + + /** + * Starts the instance world for this activity and sends all participants there + */ + void StartZone(); + + /** + * Gives the rewards for completing this activity to some participant + * @param participant the participant to give rewards to + */ + void RewardParticipant(Entity* participant); + + /** + * Removes a participant from this activity + * @param participant the participant to remove + */ + void RemoveParticipant(const Entity* participant); + + /** + * Returns all the participants of this activity + * @return all the participants of this activity + */ + std::vector GetParticipants() const; + + /** + * Currently unused + */ + uint32_t GetScore() const; + + /** + * Currently unused + */ + void SetScore(uint32_t score); +private: + + /** + * Currently unused + */ + uint32_t score = 0; + + /** + * The instance ID of this activity + */ + uint32_t m_NextZoneCloneID = 0; + + /** + * The database information for this activity + */ + CDActivities m_ActivityInfo; + + /** + * The entity that owns this activity (the entity that has the ActivityComponent) + */ + Entity* m_ParentEntity; + + /** + * All the participants of this activity + */ + std::vector m_Participants; }; -#endif //!__ACTIVITYCOMPONENT__H__ +/** + * Represents an entity in a lobby + */ +struct LobbyPlayer { + /** + * The ID of the entity that is in the lobby + */ + LWOOBJID entityID; + /** + * Whether or not the entity is ready + */ + bool ready = false; + + /** + * Returns the entity that is in the lobby + * @return the entity that is in the lobby + */ + Entity* GetEntity() const; +}; + +/** + * Represents a lobby of players with a timer until it should start the activity + */ +struct Lobby { + + /** + * The lobby of players + */ + std::vector players; + + /** + * The timer that determines when the activity should start + */ + float timer; +}; + +/** + * Represents the score for the player in an activity, one index might represent score, another one time, etc. + */ +struct ActivityPlayer { + + /** + * The entity that the score is tracked for + */ + LWOOBJID playerID; + + /** + * The list of score for this entity + */ + float values[10]; +}; + +/** + * Welcome to the absolute behemoth that is the activity component. I have now clue how this was managed in + * live but I figure somewhat similarly and it's terrible. In a nutshell, this components handles any activity that + * can be done in the game from quick builds to boss fights to races. On top of that, this component handles instancing + * and lobbying. + */ +class ActivityComponent : public Component { +public: + ActivityComponent(Entity* parent); + ~ActivityComponent() override; + + void Update(float deltaTime) override; + void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) const; + + /** + * Makes some entity join the minigame, if it's a lobbied one, the entity will be placed in the lobby + * @param player the entity to join the game + */ + void PlayerJoin(Entity* player); + + /** + * Makes an entity join the lobby for this minigame, if it exists + * @param player the entity to join + */ + void PlayerJoinLobby(Entity* player); + + /** + * Makes the player leave the lobby + * @param playerID the entity to leave the lobby + */ + void PlayerLeave(LWOOBJID playerID); + + /** + * Removes the entity from the minigame (and its score) + * @param playerID the entity to remove from the minigame + */ + void PlayerRemove(LWOOBJID playerID); + + /** + * Adds all the players to an instance of some activity + * @param instance the instance to load the players into + * @param lobby the players to load into the instance + */ + void LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const; + + /** + * Removes a lobby from the activity manager + * @param lobby the lobby to remove + */ + void RemoveLobby(Lobby* lobby); + + /** + * Marks a player as (un)ready in a lobby + * @param player the entity to mark + * @param bReady true if the entity is ready, false otherwise + */ + void PlayerReady(Entity* player, bool bReady); + + /** + * Returns the ID of this activity + * @return the ID of this activity + */ + int GetActivityID() { return m_ActivityInfo.ActivityID; } + + /** + * Returns if this activity has a lobby, e.g. if it needs to instance players to some other map + * @return true if this activity has a lobby, false otherwise + */ + bool HasLobby() const; + + /** + * Checks if a player is currently waiting in a lobby + * @param player the entity to check for + * @return true if the entity is waiting in a lobby, false otherwise + */ + bool PlayerIsInQueue(Entity* player); + + /** + * Checks if an entity is currently playing this activity + * @param player the entity to check + * @return true if the entity is playing this lobby, false otherwise + */ + bool IsPlayedBy(Entity* player) const; + + /** + * Checks if an entity is currently playing this activity + * @param playerID the entity to check + * @return true if the entity is playing this lobby, false otherwise + */ + bool IsPlayedBy(LWOOBJID playerID) const; + + /** + * Legacy: used to check for unimplemented maps, gladly, this now just returns true :) + */ + bool IsValidActivity(Entity* player); + + /** + * Removes the cost of the activity (e.g. green imaginate) for the entity that plays this activity + * @param player the entity to take cost for + * @return true if the cost was successfully deducted, false otherwise + */ + bool TakeCost(Entity* player) const; + + /** + * Handles any response from a player clicking on a lobby / instance menu + * @param player the entity that clicked + * @param id the message that was passed + */ + void HandleMessageBoxResponse(Entity* player, const std::string& id); + + /** + * Creates a new instance for this activity + * @return a new instance for this activity + */ + ActivityInstance* NewInstance(); + + /** + * Returns all the currently active instances of this activity + * @return all the currently active instances of this activity + */ + const std::vector& GetInstances() const; + + /** + * Returns the instance that some entity is currently playing in + * @param playerID the entity to check for + * @return if any, the instance that the entity is currently in + */ + ActivityInstance* GetInstance(const LWOOBJID playerID); + + /** + * @brief Reloads the config settings for this component + * + */ + void ReloadConfig(); + + /** + * Removes all the instances + */ + void ClearInstances(); + + /** + * Returns all the score for the players that are currently playing this activity + * @return + */ + std::vector GetActivityPlayers() { return m_ActivityPlayers; }; + + /** + * Returns activity data for a specific entity (e.g. score and such). + * @param playerID the entity to get data for + * @return the activity data (score) for the passed player in this activity, if it exists + */ + ActivityPlayer* GetActivityPlayerData(LWOOBJID playerID); + + /** + * Sets some score value for an entity + * @param playerID the entity to set score for + * @param index the score index to set + * @param value the value to set in for that index + */ + void SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value); + + /** + * Returns activity score for the passed parameters + * @param playerID the entity to get score for + * @param index the index to get score for + * @return activity score for the passed parameters + */ + float_t GetActivityValue(LWOOBJID playerID, uint32_t index); + + /** + * Removes activity score tracking for some entity + * @param playerID the entity to remove score for + */ + void RemoveActivityPlayerData(LWOOBJID playerID); + + /** + * Adds activity score tracking for some entity + * @param playerID the entity to add the activity score for + * @return the created entry + */ + ActivityPlayer* AddActivityPlayerData(LWOOBJID playerID); + + /** + * Sets the mapID that this activity points to + * @param mapID the map ID to set + */ + void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; }; + + /** + * Returns the LMI that this activity points to for a team size + * @param teamSize the team size to get the LMI for + * @return the LMI that this activity points to for a team size + */ + uint32_t GetLootMatrixForTeamSize(uint32_t teamSize) { return m_ActivityLootMatrices[teamSize]; } +private: + + /** + * The database information for this activity + */ + CDActivities m_ActivityInfo; + + /** + * All the active instances of this activity + */ + std::vector m_Instances; + + /** + * The current lobbies for this activity + */ + std::vector m_Queue; + + /** + * All the activity score for the players in this activity + */ + std::vector m_ActivityPlayers; + + /** + * LMIs for team sizes + */ + std::unordered_map m_ActivityLootMatrices; + + /** + * The activity id + * + */ + int32_t m_ActivityID; +}; + +#endif // ACTIVITYCOMPONENT_H diff --git a/dGame/dComponents/CMakeLists.txt b/dGame/dComponents/CMakeLists.txt index 62894136..88e84506 100644 --- a/dGame/dComponents/CMakeLists.txt +++ b/dGame/dComponents/CMakeLists.txt @@ -1,7 +1,6 @@ set(DGAME_DCOMPONENTS_SOURCES "AchievementVendorComponent.cpp" "ActivityComponent.cpp" "BaseCombatAIComponent.cpp" - "RacingControlComponent.cpp" "BouncerComponent.cpp" "BuffComponent.cpp" "BuildBorderComponent.cpp" @@ -11,7 +10,7 @@ set(DGAME_DCOMPONENTS_SOURCES "AchievementVendorComponent.cpp" "ControllablePhysicsComponent.cpp" "DestroyableComponent.cpp" "DonationVendorComponent.cpp" - "GateRushControlComponent.cpp" + "GateRushComponent.cpp" "InventoryComponent.cpp" "ItemComponent.cpp" "LevelProgressionComponent.cpp" @@ -34,7 +33,8 @@ set(DGAME_DCOMPONENTS_SOURCES "AchievementVendorComponent.cpp" "PropertyManagementComponent.cpp" "PropertyVendorComponent.cpp" "ProximityMonitorComponent.cpp" - "VehicleRacingControlComponent.cpp" + "RacingComponent.cpp" + "RacingControlComponent.cpp" "RacingSoundTriggerComponent.cpp" "RacingStatsComponent.cpp" "RailActivatorComponent.cpp" diff --git a/dGame/dComponents/ComponentHeirachy.md b/dGame/dComponents/ComponentHeirachy.md index e135b6ad..92097108 100644 --- a/dGame/dComponents/ComponentHeirachy.md +++ b/dGame/dComponents/ComponentHeirachy.md @@ -18,8 +18,8 @@ LWOActivityComponent ├── LWOShootingGalleryComponent ├── LWOScriptedActivityComponent | └── LWOBaseRacingControlComponent -> RacingControlComponent -| ├── LWORacingControlComponent -> VehicleRacingControlComponent -| └── LWOGateRushControlComponent +| ├── LWORacingControlComponent -> RacingComponent +| └── LWOGateRushControlComponent -> GateRushComponent LWOBaseCombatAIComponent ├~~ LWOPathfindingControlComponent ├~~ LWOProximityMonitorComponent diff --git a/dGame/dComponents/GateRushComponent.cpp b/dGame/dComponents/GateRushComponent.cpp new file mode 100644 index 00000000..ca48e1df --- /dev/null +++ b/dGame/dComponents/GateRushComponent.cpp @@ -0,0 +1,5 @@ +#include "GateRushComponent.h" + +GateRushComponent::GateRushComponent(Entity* parent) : RacingControlComponent(parent) { + +} diff --git a/dGame/dComponents/GateRushControlComponent.h b/dGame/dComponents/GateRushComponent.h similarity index 71% rename from dGame/dComponents/GateRushControlComponent.h rename to dGame/dComponents/GateRushComponent.h index 7e39a18a..5a60e577 100644 --- a/dGame/dComponents/GateRushControlComponent.h +++ b/dGame/dComponents/GateRushComponent.h @@ -6,10 +6,10 @@ class Entity; -class GateRushControlComponent : public RacingControlComponent { +class GateRushComponent : public RacingControlComponent { public: inline static const eReplicaComponentType ComponentType = eReplicaComponentType::GATE_RUSH_CONTROL; - GateRushControlComponent(Entity* parent, int32_t componentId); + GateRushComponent(Entity* parent); }; #endif //!__GATERUSHCONTROLCOMPONENT__H__ diff --git a/dGame/dComponents/GateRushControlComponent.cpp b/dGame/dComponents/GateRushControlComponent.cpp deleted file mode 100644 index 83ff4f86..00000000 --- a/dGame/dComponents/GateRushControlComponent.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "GateRushControlComponent.h" - -GateRushControlComponent::GateRushControlComponent(Entity* parent, int32_t componentId) : RacingControlComponent(parent, componentId) { - -} diff --git a/dGame/dComponents/MinigameControlComponent.cpp b/dGame/dComponents/MinigameControlComponent.cpp index 5694a967..8677d839 100644 --- a/dGame/dComponents/MinigameControlComponent.cpp +++ b/dGame/dComponents/MinigameControlComponent.cpp @@ -1,6 +1,6 @@ -#include "MinigameControlComponent.h" #include "Entity.h" +#include "MinigameControlComponent.h" MinigameControlComponent::MinigameControlComponent(Entity* parent) : ActivityComponent(parent) { diff --git a/dGame/dComponents/QuickBuildComponent.cpp b/dGame/dComponents/QuickBuildComponent.cpp index 67e1f4cb..eb961a45 100644 --- a/dGame/dComponents/QuickBuildComponent.cpp +++ b/dGame/dComponents/QuickBuildComponent.cpp @@ -25,7 +25,7 @@ #include "CppScripts.h" -QuickBuildComponent::QuickBuildComponent(Entity* entity, uint32_t componentId) : ActivityComponent(entity) { +QuickBuildComponent::QuickBuildComponent(Entity* entity) : ActivityComponent(entity) { m_ComponentId = componentId; std::u16string checkPreconditions = entity->GetVar(u"CheckPrecondition"); diff --git a/dGame/dComponents/RacingComponent.cpp b/dGame/dComponents/RacingComponent.cpp new file mode 100644 index 00000000..50c238c6 --- /dev/null +++ b/dGame/dComponents/RacingComponent.cpp @@ -0,0 +1,5 @@ +#include "RacingComponent.h" + +RacingComponent::RacingComponent(Entity* parent) : RacingControlComponent(parent) { + +} diff --git a/dGame/dComponents/RacingComponent.h b/dGame/dComponents/RacingComponent.h new file mode 100644 index 00000000..e7cce7a4 --- /dev/null +++ b/dGame/dComponents/RacingComponent.h @@ -0,0 +1,16 @@ +#ifndef __RACINGCOMPONENT__H__ +#define __RACINGCOMPONENT__H__ + +#include "RacingControlComponent.h" +#include "eReplicaComponentType.h" + +class Entity; + +class RacingComponent : public RacingControlComponent { +public: + inline static const eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; + RacingComponent(Entity* parent); +}; + + +#endif //!__RACINGCOMPONENT__H__ diff --git a/dGame/dComponents/RacingControlComponent.cpp b/dGame/dComponents/RacingControlComponent.cpp index 2766c471..8f1b7ee7 100644 --- a/dGame/dComponents/RacingControlComponent.cpp +++ b/dGame/dComponents/RacingControlComponent.cpp @@ -1,5 +1,880 @@ +/** + * Thanks to Simon for his early research on the racing system. + */ + #include "RacingControlComponent.h" -RacingControlComponent::RacingControlComponent(Entity* parent, int32_t componentId) : ScriptedActivityComponent(parent, componentId) { +#include "CharacterComponent.h" +#include "DestroyableComponent.h" +#include "EntityManager.h" +#include "GameMessages.h" +#include "InventoryComponent.h" +#include "Item.h" +#include "MissionComponent.h" +#include "ModuleAssemblyComponent.h" +#include "Player.h" +#include "PossessableComponent.h" +#include "PossessionComponent.h" +#include "eRacingTaskParam.h" +#include "Spawner.h" +#include "dServer.h" +#include "dZoneManager.h" +#include "dConfig.h" +#include "Loot.h" +#include "eMissionTaskType.h" +#include "dZoneManager.h" +#include "CDActivitiesTable.h" +#ifndef M_PI +#define M_PI 3.14159265358979323846264338327950288 +#endif + +RacingControlComponent::RacingControlComponent(Entity* parent) : ScriptedActivityComponent(parent) { + m_PathName = u"MainPath"; + m_RemainingLaps = 3; + m_LeadingPlayer = LWOOBJID_EMPTY; + m_RaceBestTime = 0; + m_RaceBestLap = 0; + m_Started = false; + m_StartTimer = 0; + m_Loaded = false; + m_LoadedPlayers = 0; + m_LoadTimer = 0; + m_Finished = 0; + m_StartTime = 0; + m_EmptyTimer = 0; + m_SoloRacing = Game::config->GetValue("solo_racing") == "1"; + + m_MainWorld = 1200; + const auto worldID = Game::server->GetZoneID(); + if (dZoneManager::Instance()->CheckIfAccessibleZone((worldID/10)*10)) m_MainWorld = (worldID/10)*10; + + m_ActivityID = 42; + CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); + std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.instanceMapID == worldID); }); + for (CDActivities activity : activities) m_ActivityID = activity.ActivityID; +} + +RacingControlComponent::~RacingControlComponent() {} + +void RacingControlComponent::OnPlayerLoaded(Entity* player) { + // If the race has already started, send the player back to the main world. + if (m_Loaded) { + auto* playerInstance = dynamic_cast(player); + + playerInstance->SendToZone(m_MainWorld); + + return; + } + + const auto objectID = player->GetObjectID(); + + m_LoadedPlayers++; + + Game::logger->Log("RacingControlComponent", "Loading player %i", + m_LoadedPlayers); + + m_LobbyPlayers.push_back(objectID); +} + +void RacingControlComponent::LoadPlayerVehicle(Entity* player, + uint32_t positionNumber, bool initialLoad) { + // Load the player's vehicle. + + if (player == nullptr) { + return; + } + + auto* inventoryComponent = player->GetComponent(); + + if (inventoryComponent == nullptr) { + return; + } + + // Find the player's vehicle. + + auto* item = inventoryComponent->FindItemByLot(8092); + + if (item == nullptr) { + Game::logger->Log("RacingControlComponent", "Failed to find item"); + + return; + } + + // Calculate the vehicle's starting position. + + auto* path = dZoneManager::Instance()->GetZone()->GetPath( + GeneralUtils::UTF16ToWTF8(m_PathName)); + + auto spawnPointEntities = EntityManager::Instance()->GetEntitiesByLOT(4843); + auto startPosition = NiPoint3::ZERO; + auto startRotation = NiQuaternion::IDENTITY; + const std::string placementAsString = std::to_string(positionNumber); + for (auto entity : spawnPointEntities) { + if (!entity) continue; + if (entity->GetVarAsString(u"placement") == placementAsString) { + startPosition = entity->GetPosition(); + startRotation = entity->GetRotation(); + break; + } + } + + // Make sure the player is at the correct position. + + GameMessages::SendTeleport(player->GetObjectID(), startPosition, + startRotation, player->GetSystemAddress(), true); + + // Spawn the vehicle entity. + + EntityInfo info{}; + info.lot = 8092; + info.pos = startPosition; + info.rot = startRotation; + info.spawnerID = m_ParentEntity->GetObjectID(); + + auto* carEntity = + EntityManager::Instance()->CreateEntity(info, nullptr, m_ParentEntity); + + // Make the vehicle a child of the racing controller. + m_ParentEntity->AddChild(carEntity); + + auto* destroyableComponent = carEntity->GetComponent(); + + // Setup the vehicle stats. + if (destroyableComponent != nullptr) { + destroyableComponent->SetMaxImagination(60); + destroyableComponent->SetImagination(0); + } + + // Setup the vehicle as being possessed by the player. + auto* possessableComponent = carEntity->GetComponent(); + + if (possessableComponent != nullptr) { + possessableComponent->SetPossessor(player->GetObjectID()); + } + + // Load the vehicle's assemblyPartLOTs for display. + auto* moduleAssemblyComponent = carEntity->GetComponent(); + + if (moduleAssemblyComponent) { + moduleAssemblyComponent->SetSubKey(item->GetSubKey()); + moduleAssemblyComponent->SetUseOptionalParts(false); + + for (auto* config : item->GetConfig()) { + if (config->GetKey() == u"assemblyPartLOTs") { + moduleAssemblyComponent->SetAssemblyPartsLOTs( + GeneralUtils::ASCIIToUTF16(config->GetValueAsString())); + } + } + } + + // Setup the player as possessing the vehicle. + auto* possessionComponent = player->GetComponent(); + + if (possessionComponent != nullptr) { + possessionComponent->SetPossessable(carEntity->GetObjectID()); + possessionComponent->SetPossessableType(ePossessionType::ATTACHED_VISIBLE); // for racing it's always Attached_Visible + } + + // Set the player's current activity as racing. + auto* characterComponent = player->GetComponent(); + + if (characterComponent != nullptr) { + characterComponent->SetIsRacing(true); + } + + // Init the player's racing entry. + if (initialLoad) { + m_RacingPlayers.push_back( + { player->GetObjectID(), + carEntity->GetObjectID(), + static_cast(m_RacingPlayers.size()), + false, + {}, + startPosition, + startRotation, + 0, + 0, + 0, + 0 }); + } + + // Construct and serialize everything when done. + + EntityManager::Instance()->ConstructEntity(carEntity); + EntityManager::Instance()->SerializeEntity(player); + EntityManager::Instance()->SerializeEntity(m_ParentEntity); + + GameMessages::SendRacingSetPlayerResetInfo( + m_ParentEntity->GetObjectID(), 0, 0, player->GetObjectID(), startPosition, 1, + UNASSIGNED_SYSTEM_ADDRESS); + + const auto playerID = player->GetObjectID(); + + // Reset the player to the start position during downtime, in case something + // went wrong. + m_ParentEntity->AddCallbackTimer(1, [this, playerID]() { + auto* player = EntityManager::Instance()->GetEntity(playerID); + + if (player == nullptr) { + return; + } + + GameMessages::SendRacingResetPlayerToLastReset( + m_ParentEntity->GetObjectID(), playerID, UNASSIGNED_SYSTEM_ADDRESS); + }); + + GameMessages::SendSetJetPackMode(player, false); + + // Set the vehicle's state. + GameMessages::SendNotifyVehicleOfRacingObject(carEntity->GetObjectID(), + m_ParentEntity->GetObjectID(), + UNASSIGNED_SYSTEM_ADDRESS); + + GameMessages::SendVehicleSetWheelLockState(carEntity->GetObjectID(), false, + initialLoad, + UNASSIGNED_SYSTEM_ADDRESS); + + // Make sure everything has the correct position. + GameMessages::SendTeleport(player->GetObjectID(), startPosition, + startRotation, player->GetSystemAddress(), true); + GameMessages::SendTeleport(carEntity->GetObjectID(), startPosition, + startRotation, player->GetSystemAddress(), true); +} + +void RacingControlComponent::OnRacingClientReady(Entity* player) { + // Notify the other players that this player is ready. + + for (auto& racingPlayer : m_RacingPlayers) { + if (racingPlayer.playerID != player->GetObjectID()) { + if (racingPlayer.playerLoaded) { + GameMessages::SendRacingPlayerLoaded( + m_ParentEntity->GetObjectID(), racingPlayer.playerID, + racingPlayer.vehicleID, UNASSIGNED_SYSTEM_ADDRESS); + } + + continue; + } + + racingPlayer.playerLoaded = true; + + GameMessages::SendRacingPlayerLoaded( + m_ParentEntity->GetObjectID(), racingPlayer.playerID, + racingPlayer.vehicleID, UNASSIGNED_SYSTEM_ADDRESS); + } + + EntityManager::Instance()->SerializeEntity(m_ParentEntity); +} + +void RacingControlComponent::OnRequestDie(Entity* player) { + // Sent by the client when they collide with something which should smash + // them. + + for (auto& racingPlayer : m_RacingPlayers) { + if (racingPlayer.playerID != player->GetObjectID()) { + continue; + } + + auto* vehicle = + EntityManager::Instance()->GetEntity(racingPlayer.vehicleID); + + if (!vehicle) return; + + if (!racingPlayer.noSmashOnReload) { + racingPlayer.smashedTimes++; + GameMessages::SendDie(vehicle, vehicle->GetObjectID(), LWOOBJID_EMPTY, true, + eKillType::VIOLENT, u"", 0, 0, 90.0f, false, true, 0); + + auto* destroyableComponent = vehicle->GetComponent(); + uint32_t respawnImagination = 0; + // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. + // Do not actually change the value yet. Do that on respawn. + if (destroyableComponent) { + respawnImagination = static_cast(ceil(destroyableComponent->GetImagination() / 2.0f / 10.0f)) * 10.0f; + GameMessages::SendSetResurrectRestoreValues(vehicle, -1, -1, respawnImagination); + } + + // Respawn the player in 2 seconds, as was done in live. Not sure if this value is in a setting somewhere else... + vehicle->AddCallbackTimer(2.0f, [=]() { + if (!vehicle || !this->m_ParentEntity) return; + GameMessages::SendRacingResetPlayerToLastReset( + m_ParentEntity->GetObjectID(), racingPlayer.playerID, + UNASSIGNED_SYSTEM_ADDRESS); + + GameMessages::SendVehicleStopBoost(vehicle, player->GetSystemAddress(), true); + + GameMessages::SendRacingSetPlayerResetInfo( + m_ParentEntity->GetObjectID(), racingPlayer.lap, + racingPlayer.respawnIndex, player->GetObjectID(), + racingPlayer.respawnPosition, racingPlayer.respawnIndex + 1, + UNASSIGNED_SYSTEM_ADDRESS); + + GameMessages::SendResurrect(vehicle); + auto* destroyableComponent = vehicle->GetComponent(); + // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. + if (destroyableComponent) destroyableComponent->SetImagination(respawnImagination); + EntityManager::Instance()->SerializeEntity(vehicle); + }); + + auto* characterComponent = player->GetComponent(); + if (characterComponent != nullptr) { + characterComponent->UpdatePlayerStatistic(RacingTimesWrecked); + } + } else { + GameMessages::SendRacingSetPlayerResetInfo( + m_ParentEntity->GetObjectID(), racingPlayer.lap, + racingPlayer.respawnIndex, player->GetObjectID(), + racingPlayer.respawnPosition, racingPlayer.respawnIndex + 1, + UNASSIGNED_SYSTEM_ADDRESS); + GameMessages::SendRacingResetPlayerToLastReset( + m_ParentEntity->GetObjectID(), racingPlayer.playerID, + UNASSIGNED_SYSTEM_ADDRESS); + } + } +} + +void RacingControlComponent::OnRacingPlayerInfoResetFinished(Entity* player) { + // When the player has respawned. + + for (auto& racingPlayer : m_RacingPlayers) { + if (racingPlayer.playerID != player->GetObjectID()) { + continue; + } + + auto* vehicle = + EntityManager::Instance()->GetEntity(racingPlayer.vehicleID); + + if (vehicle == nullptr) { + return; + } + + racingPlayer.noSmashOnReload = false; + + return; + } +} + +void RacingControlComponent::HandleMessageBoxResponse(Entity* player, int32_t button, const std::string& id) { + auto* data = GetPlayerData(player->GetObjectID()); + + if (data == nullptr) { + return; + } + + if (id == "rewardButton") { + if (data->collectedRewards) { + return; + } + + data->collectedRewards = true; + + // Calculate the score, different loot depending on player count + const auto score = m_LoadedPlayers * 10 + data->finished; + + LootGenerator::Instance().GiveActivityLoot(player, m_ParentEntity, m_ActivityID, score); + + // Giving rewards + GameMessages::SendNotifyRacingClient( + m_ParentEntity->GetObjectID(), 2, 0, LWOOBJID_EMPTY, u"", + player->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); + + auto* missionComponent = player->GetComponent(); + + if (missionComponent == nullptr) return; + + missionComponent->Progress(eMissionTaskType::RACING, 0, (LWOOBJID)eRacingTaskParam::COMPETED_IN_RACE); // Progress task for competing in a race + missionComponent->Progress(eMissionTaskType::RACING, data->smashedTimes, (LWOOBJID)eRacingTaskParam::SAFE_DRIVER); // Finish a race without being smashed. + + // If solo racing is enabled OR if there are 3 players in the race, progress placement tasks. + if (m_SoloRacing || m_LoadedPlayers > 2) { + missionComponent->Progress(eMissionTaskType::RACING, data->finished, (LWOOBJID)eRacingTaskParam::FINISH_WITH_PLACEMENT); // Finish in 1st place on a race + if (data->finished == 1) { + missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::FIRST_PLACE_MULTIPLE_TRACKS); // Finish in 1st place on multiple tracks. + missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::WIN_RACE_IN_WORLD); // Finished first place in specific world. + } + if (data->finished == m_LoadedPlayers) { + missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::LAST_PLACE_FINISH); // Finished first place in specific world. + } + } + } else if ((id == "ACT_RACE_EXIT_THE_RACE?" || id == "Exit") && button == m_ActivityExitConfirm) { + auto* vehicle = EntityManager::Instance()->GetEntity(data->vehicleID); + + if (vehicle == nullptr) { + return; + } + + // Exiting race + GameMessages::SendNotifyRacingClient( + m_ParentEntity->GetObjectID(), 3, 0, LWOOBJID_EMPTY, u"", + player->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); + + auto* playerInstance = dynamic_cast(player); + + playerInstance->SendToZone(m_MainWorld); + + vehicle->Kill(); + } +} + +void RacingControlComponent::Serialize(RakNet::BitStream* outBitStream, + bool bIsInitialUpdate, + unsigned int& flags) { + // BEGIN Scripted Activity + + outBitStream->Write1(); + + outBitStream->Write(static_cast(m_RacingPlayers.size())); + for (const auto& player : m_RacingPlayers) { + outBitStream->Write(player.playerID); + + for (int i = 0; i < 10; i++) { + outBitStream->Write(player.data[i]); + } + } + + // END Scripted Activity + + outBitStream->Write1(); // Dirty? + outBitStream->Write(static_cast(m_RacingPlayers.size())); + + outBitStream->Write(!m_RacingPlayers.empty()); + if (!m_RacingPlayers.empty()) { + for (const auto& player : m_RacingPlayers) { + outBitStream->Write1(); // Has more date + + outBitStream->Write(player.playerID); + outBitStream->Write(player.vehicleID); + outBitStream->Write(player.playerIndex); + outBitStream->Write(player.playerLoaded); + } + + outBitStream->Write0(); // No more data + } + + outBitStream->Write(!m_RacingPlayers.empty()); + if (!m_RacingPlayers.empty()) { + for (const auto& player : m_RacingPlayers) { + outBitStream->Write1(); // Has more date + + outBitStream->Write(player.playerID); + outBitStream->Write(0); + } + + outBitStream->Write0(); // No more data + } + + outBitStream->Write1(); // Dirty? + + outBitStream->Write(m_RemainingLaps); + + outBitStream->Write(static_cast(m_PathName.size())); + for (const auto character : m_PathName) { + outBitStream->Write(character); + } + + outBitStream->Write1(); // ??? + outBitStream->Write1(); // ??? + + outBitStream->Write(m_LeadingPlayer); + outBitStream->Write(m_RaceBestLap); + outBitStream->Write(m_RaceBestTime); +} + +RacingPlayerInfo* RacingControlComponent::GetPlayerData(LWOOBJID playerID) { + for (auto& player : m_RacingPlayers) { + if (player.playerID == playerID) { + return &player; + } + } + + return nullptr; +} + +void RacingControlComponent::Update(float deltaTime) { + // This method is a mess. + + // Pre-load routine + if (!m_Loaded) { + // Check if any players has disconnected before loading in + for (size_t i = 0; i < m_LobbyPlayers.size(); i++) { + auto* playerEntity = + EntityManager::Instance()->GetEntity(m_LobbyPlayers[i]); + + if (playerEntity == nullptr) { + --m_LoadedPlayers; + + m_LobbyPlayers.erase(m_LobbyPlayers.begin() + i); + + return; + } + } + + if (m_LoadedPlayers >= 2 || (m_LoadedPlayers == 1 && m_SoloRacing)) { + m_LoadTimer += deltaTime; + } else { + m_EmptyTimer += deltaTime; + } + + // If a player happens to be left alone for more then 30 seconds without + // anyone else loading in, send them back to the main world + if (m_EmptyTimer >= 30) { + for (const auto player : m_LobbyPlayers) { + auto* playerEntity = + EntityManager::Instance()->GetEntity(player); + + if (playerEntity == nullptr) { + continue; + } + + auto* playerInstance = dynamic_cast(playerEntity); + + playerInstance->SendToZone(m_MainWorld); + } + + m_LobbyPlayers.clear(); + } + + // From the first 2 players loading in the rest have a max of 15 seconds + // to load in, can raise this if it's too low + if (m_LoadTimer >= 15) { + Game::logger->Log("RacingControlComponent", + "Loading all players..."); + + for (size_t positionNumber = 0; positionNumber < m_LobbyPlayers.size(); positionNumber++) { + Game::logger->Log("RacingControlComponent", + "Loading player now!"); + + auto* player = + EntityManager::Instance()->GetEntity(m_LobbyPlayers[positionNumber]); + + if (player == nullptr) { + return; + } + + Game::logger->Log("RacingControlComponent", + "Loading player now NOW!"); + + LoadPlayerVehicle(player, positionNumber + 1, true); + + m_Loaded = true; + } + + m_Loaded = true; + } + + return; + } + + // The players who will be participating have loaded + if (!m_Started) { + // Check if anyone has disconnected during this period + for (size_t i = 0; i < m_RacingPlayers.size(); i++) { + auto* playerEntity = EntityManager::Instance()->GetEntity( + m_RacingPlayers[i].playerID); + + if (playerEntity == nullptr) { + m_RacingPlayers.erase(m_RacingPlayers.begin() + i); + + --m_LoadedPlayers; + + return; + } + } + + // If less then 2 players are left, send the rest back to the main world + if (m_LoadedPlayers < 2 && !(m_LoadedPlayers == 1 && m_SoloRacing)) { + for (const auto player : m_LobbyPlayers) { + auto* playerEntity = + EntityManager::Instance()->GetEntity(player); + + if (playerEntity == nullptr) { + continue; + } + + auto* playerInstance = dynamic_cast(playerEntity); + + playerInstance->SendToZone(m_MainWorld); + } + + return; + } + + // Check if all players have send a ready message + + int32_t readyPlayers = 0; + + for (const auto& player : m_RacingPlayers) { + if (player.playerLoaded) { + ++readyPlayers; + } + } + + if (readyPlayers >= m_LoadedPlayers) { + // Setup for racing + if (m_StartTimer == 0) { + GameMessages::SendNotifyRacingClient( + m_ParentEntity->GetObjectID(), 1, 0, LWOOBJID_EMPTY, u"", + LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS); + + for (const auto& player : m_RacingPlayers) { + auto* vehicle = + EntityManager::Instance()->GetEntity(player.vehicleID); + auto* playerEntity = + EntityManager::Instance()->GetEntity(player.playerID); + + if (vehicle != nullptr && playerEntity != nullptr) { + GameMessages::SendTeleport( + player.playerID, player.respawnPosition, + player.respawnRotation, + playerEntity->GetSystemAddress(), true); + + vehicle->SetPosition(player.respawnPosition); + vehicle->SetRotation(player.respawnRotation); + + auto* destroyableComponent = vehicle->GetComponent(); + + if (destroyableComponent != nullptr) { + destroyableComponent->SetImagination(0); + } + + EntityManager::Instance()->SerializeEntity(vehicle); + EntityManager::Instance()->SerializeEntity( + playerEntity); + } + } + + // Spawn imagination pickups + auto* minSpawner = dZoneManager::Instance()->GetSpawnersByName( + "ImaginationSpawn_Min")[0]; + auto* medSpawner = dZoneManager::Instance()->GetSpawnersByName( + "ImaginationSpawn_Med")[0]; + auto* maxSpawner = dZoneManager::Instance()->GetSpawnersByName( + "ImaginationSpawn_Max")[0]; + + minSpawner->Activate(); + + if (m_LoadedPlayers > 2) { + medSpawner->Activate(); + } + + if (m_LoadedPlayers > 4) { + maxSpawner->Activate(); + } + + // Reset players to their start location, without smashing them + for (auto& player : m_RacingPlayers) { + auto* vehicleEntity = + EntityManager::Instance()->GetEntity(player.vehicleID); + auto* playerEntity = + EntityManager::Instance()->GetEntity(player.playerID); + + if (vehicleEntity == nullptr || playerEntity == nullptr) { + continue; + } + + player.noSmashOnReload = true; + + OnRequestDie(playerEntity); + } + } + // This 6 seconds seems to be hardcoded in the client, start race + // after that amount of time + else if (m_StartTimer >= 6) { + // Activate the players movement + for (auto& player : m_RacingPlayers) { + auto* vehicleEntity = + EntityManager::Instance()->GetEntity(player.vehicleID); + auto* playerEntity = + EntityManager::Instance()->GetEntity(player.playerID); + + if (vehicleEntity == nullptr || playerEntity == nullptr) { + continue; + } + + GameMessages::SendVehicleUnlockInput( + player.vehicleID, false, UNASSIGNED_SYSTEM_ADDRESS); + } + + // Start the race + GameMessages::SendActivityStart(m_ParentEntity->GetObjectID(), + UNASSIGNED_SYSTEM_ADDRESS); + + m_Started = true; + + Game::logger->Log("RacingControlComponent", "Starting race"); + + EntityManager::Instance()->SerializeEntity(m_ParentEntity); + + m_StartTime = std::time(nullptr); + } + + m_StartTimer += deltaTime; + } else { + m_StartTimer = 0; + } + + return; + } + + // Race routines + auto* path = dZoneManager::Instance()->GetZone()->GetPath( + GeneralUtils::UTF16ToWTF8(m_PathName)); + + for (auto& player : m_RacingPlayers) { + auto* vehicle = EntityManager::Instance()->GetEntity(player.vehicleID); + auto* playerEntity = + EntityManager::Instance()->GetEntity(player.playerID); + + if (vehicle == nullptr || playerEntity == nullptr) { + continue; + } + + const auto vehiclePosition = vehicle->GetPosition(); + + // If the player is this far below the map, safe to assume they should + // be smashed by death plane + if (vehiclePosition.y < -500) { + GameMessages::SendDie(vehicle, m_ParentEntity->GetObjectID(), + LWOOBJID_EMPTY, true, eKillType::VIOLENT, u"", 0, 0, 0, + true, false, 0); + + OnRequestDie(playerEntity); + + continue; + } + + // Loop through all the waypoints and see if the player has reached a + // new checkpoint + uint32_t respawnIndex = 0; + for (const auto& waypoint : path->pathWaypoints) { + if (player.lap == 3) { + break; + } + + if (player.respawnIndex == respawnIndex) { + ++respawnIndex; + + continue; + } + + const auto& position = waypoint.position; + + if (std::abs((int)respawnIndex - (int)player.respawnIndex) > 10 && + player.respawnIndex != path->pathWaypoints.size() - 1) { + ++respawnIndex; + + continue; + } + + if (Vector3::DistanceSquared(position, vehiclePosition) > 50 * 50) { + ++respawnIndex; + + continue; + } + + // Only go upwards, except if we've lapped + // Not sure how we are supposed to check if they've reach a + // checkpoint, within 50 units seems safe + if (!(respawnIndex > player.respawnIndex || + player.respawnIndex == path->pathWaypoints.size() - 1)) { + ++respawnIndex; + + continue; + } + + // Some offset up to make they don't fall through the terrain on a + // respawn, seems to fix itself to the track anyhow + player.respawnPosition = position + NiPoint3::UNIT_Y * 5; + player.respawnRotation = vehicle->GetRotation(); + player.respawnIndex = respawnIndex; + + // Reached the start point, lapped + if (respawnIndex == 0) { + time_t lapTime = std::time(nullptr) - (player.lap == 0 ? m_StartTime : player.lapTime); + + // Cheating check + if (lapTime < 40) { + continue; + } + + player.lap++; + + player.lapTime = std::time(nullptr); + + if (player.bestLapTime == 0 || player.bestLapTime > lapTime) { + player.bestLapTime = lapTime; + + Game::logger->Log("RacingControlComponent", + "Best lap time (%llu)", lapTime); + } + + auto* missionComponent = playerEntity->GetComponent(); + + if (missionComponent != nullptr) { + + // Progress lap time tasks + missionComponent->Progress(eMissionTaskType::RACING, (lapTime) * 1000, (LWOOBJID)eRacingTaskParam::LAP_TIME); + + if (player.lap == 3) { + m_Finished++; + player.finished = m_Finished; + + const auto raceTime = + (std::time(nullptr) - m_StartTime); + + player.raceTime = raceTime; + + Game::logger->Log("RacingControlComponent", + "Completed time %llu, %llu", + raceTime, raceTime * 1000); + + // Entire race time + missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME); + + auto* characterComponent = playerEntity->GetComponent(); + if (characterComponent != nullptr) { + characterComponent->TrackRaceCompleted(m_Finished == 1); + } + + // TODO: Figure out how to update the GUI leaderboard. + } + } + + Game::logger->Log("RacingControlComponent", + "Lapped (%i) in (%llu)", player.lap, + lapTime); + } + + Game::logger->Log("RacingControlComponent", + "Reached point (%i)/(%i)", player.respawnIndex, + path->pathWaypoints.size()); + + break; + } + } +} + +std::string RacingControlComponent::FormatTimeString(time_t time) { + int32_t min = time / 60; + time -= min * 60; + int32_t sec = time; + + std::string minText; + std::string secText; + + if (min <= 0) { + minText = "0"; + } else { + minText = std::to_string(min); + } + + if (sec <= 0) { + secText = "00"; + } else if (sec <= 9) { + secText = "0" + std::to_string(sec); + } else { + secText = std::to_string(sec); + } + + return minText + ":" + secText + ".00"; } diff --git a/dGame/dComponents/RacingControlComponent.h b/dGame/dComponents/RacingControlComponent.h index 7e7eef61..156403a8 100644 --- a/dGame/dComponents/RacingControlComponent.h +++ b/dGame/dComponents/RacingControlComponent.h @@ -1,16 +1,254 @@ -#ifndef __RACINGCONTROLCOMPONENT__H__ -#define __RACINGCONTROLCOMPONENT__H__ +/** + * Thanks to Simon for his early research on the racing system. + */ +#pragma once + +#include "BitStream.h" +#include "Entity.h" #include "ScriptedActivityComponent.h" #include "eReplicaComponentType.h" -class Entity; + /** + * Information for each player in the race + */ +struct RacingPlayerInfo { + /** + * The ID of the player + */ + LWOOBJID playerID; + + /** + * The ID of the car the player is driving + */ + LWOOBJID vehicleID; + + /** + * The index of this player in the list of players + */ + uint32_t playerIndex; + + /** + * Whether the player has finished loading or not + */ + bool playerLoaded; + + /** + * Scripted activity component score + */ + float data[10]{}; + + /** + * Point that the player will respawn at if they smash their car + */ + NiPoint3 respawnPosition; + + /** + * Rotation that the player will respawn at if they smash their car + */ + NiQuaternion respawnRotation; + + /** + * The index in the respawn point the player is now at + */ + uint32_t respawnIndex; + + /** + * The number of laps the player has completed + */ + uint32_t lap; + + /** + * Whether or not the player has finished the race + */ + uint32_t finished; + + /** + * Unused + */ + uint16_t reachedPoints; + + /** + * The fastest lap time of the player + */ + time_t bestLapTime = 0; + + /** + * The current lap time of the player + */ + time_t lapTime = 0; + + /** + * The number of times this player smashed their car + */ + uint32_t smashedTimes = 0; + + /** + * Whether or not the player should be smashed if the game is reloaded + */ + bool noSmashOnReload = false; + + /** + * Whether or not this player has collected their rewards from completing the race + */ + bool collectedRewards = false; + + /** + * Unused + */ + time_t raceTime = 0; +}; + +/** + * Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc. + */ class RacingControlComponent : public ScriptedActivityComponent { public: inline static const eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; - RacingControlComponent(Entity* parent, int32_t componentId); + + RacingControlComponent(Entity* parentEntity); + ~RacingControlComponent(); + + void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags); + void Update(float deltaTime); + + /** + * Invoked when a player loads into the zone. + */ + void OnPlayerLoaded(Entity* player); + + /** + * Initalize the player's vehicle. + * + * @param player The player who's vehicle to initialize. + * @param initialLoad Is this the first time the player is loading in this race? + */ + void LoadPlayerVehicle(Entity* player, uint32_t positionNumber, bool initialLoad = false); + + /** + * Invoked when the client says it has loaded in. + */ + void OnRacingClientReady(Entity* player); + + /** + * Invoked when the client says it should be smashed. + */ + void OnRequestDie(Entity* player); + + /** + * Invoked when the player has finished respawning. + */ + void OnRacingPlayerInfoResetFinished(Entity* player); + + /** + * Invoked when the player responds to the GUI. + */ + void HandleMessageBoxResponse(Entity* player, int32_t button, const std::string& id); + + /** + * Get the racing data from a player's LWOOBJID. + */ + RacingPlayerInfo* GetPlayerData(LWOOBJID playerID); + + /** + * Formats a time to a string, currently unused + * @param time the time to format + * @return the time formatted as string + */ + static std::string FormatTimeString(time_t time); + +private: + + /** + * The players that are currently racing + */ + std::vector m_RacingPlayers; + + /** + * The paths that are followed for the camera scenes + */ + std::u16string m_PathName; + + /** + * The ID of the activity for participating in this race + */ + uint32_t m_ActivityID; + + /** + * The world the players return to when they finish the race + */ + uint32_t m_MainWorld; + + /** + * The number of laps that are remaining for the winning player + */ + uint16_t m_RemainingLaps; + + /** + * The ID of the player that's currently winning the race + */ + LWOOBJID m_LeadingPlayer; + + /** + * The overall best lap from all the players + */ + float m_RaceBestLap; + + /** + * The overall best time from all the players + */ + float m_RaceBestTime; + + /** + * Whether or not the race has started + */ + bool m_Started; + + /** + * The time left until the race will start + */ + float m_StartTimer; + + /** + * The time left for loading the players + */ + float m_LoadTimer; + + /** + * Whether or not all players have loaded + */ + bool m_Loaded; + + /** + * The number of loaded players + */ + uint32_t m_LoadedPlayers; + + /** + * All the players that are in the lobby, loaded or not + */ + std::vector m_LobbyPlayers; + + /** + * The number of players that have finished the race + */ + uint32_t m_Finished; + + /** + * The time the race was started + */ + time_t m_StartTime; + + /** + * Timer for tracking how long a player was alone in this race + */ + float m_EmptyTimer; + + bool m_SoloRacing; + + /** + * Value for message box response to know if we are exiting the race via the activity dialogue + */ + const int32_t m_ActivityExitConfirm = 1; }; - - -#endif //!__RACINGCONTROLCOMPONENT__H__ diff --git a/dGame/dComponents/ScriptedActivityComponent.cpp b/dGame/dComponents/ScriptedActivityComponent.cpp index e38edad0..03063578 100644 --- a/dGame/dComponents/ScriptedActivityComponent.cpp +++ b/dGame/dComponents/ScriptedActivityComponent.cpp @@ -1,621 +1,5 @@ #include "ScriptedActivityComponent.h" -#include "GameMessages.h" -#include "CDClientManager.h" -#include "MissionComponent.h" -#include "Character.h" -#include "dZoneManager.h" -#include "ZoneInstanceManager.h" -#include "Game.h" -#include "dLogger.h" -#include -#include "EntityManager.h" -#include "ChatPackets.h" -#include "Player.h" -#include "PacketUtils.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 "eConnectionType.h" -#include "eChatInternalMessageType.h" -#include "CDCurrencyTableTable.h" -#include "CDActivityRewardsTable.h" -#include "CDActivitiesTable.h" -#include "LeaderboardManager.h" +ScriptedActivityComponent::ScriptedActivityComponent(Entity* parent) : ActivityComponent(parent) { -ScriptedActivityComponent::ScriptedActivityComponent(Entity* parent, int activityID) : Component(parent) { - m_ActivityID = activityID; - CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); - std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.ActivityID == m_ActivityID); }); - - for (CDActivities activity : activities) { - m_ActivityInfo = activity; - if (static_cast(activity.leaderboardType) == LeaderboardType::Racing && Game::config->GetValue("solo_racing") == "1") { - m_ActivityInfo.minTeamSize = 1; - m_ActivityInfo.minTeams = 1; - } - - const auto& transferOverride = parent->GetVar(u"transferZoneID"); - if (!transferOverride.empty()) { - m_ActivityInfo.instanceMapID = std::stoi(GeneralUtils::UTF16ToWTF8(transferOverride)); - - // TODO: LU devs made me do it (for some reason cannon cove instancer is marked to go to GF survival) - // NOTE: 1301 is GF survival - if (m_ActivityInfo.instanceMapID == 1301) { - m_ActivityInfo.instanceMapID = 1302; - } - } - } - - auto* destroyableComponent = m_ParentEntity->GetComponent(); - - if (destroyableComponent) { - // check for LMIs and set the loot LMIs - CDActivityRewardsTable* activityRewardsTable = CDClientManager::Instance().GetTable(); - std::vector activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) {return (entry.LootMatrixIndex == destroyableComponent->GetLootMatrixID()); }); - - uint32_t startingLMI = 0; - - if (activityRewards.size() > 0) { - startingLMI = activityRewards[0].LootMatrixIndex; - } - - if (startingLMI > 0) { - // now time for bodge :) - - std::vector objectTemplateActivities = activityRewardsTable->Query([=](CDActivityRewards entry) {return (activityRewards[0].objectTemplate == entry.objectTemplate); }); - for (const auto& item : objectTemplateActivities) { - if (item.activityRating > 0 && item.activityRating < 5) { - m_ActivityLootMatrices.insert({ item.activityRating, item.LootMatrixIndex }); - } - } - } - } -} - -ScriptedActivityComponent::~ScriptedActivityComponent() -= default; - -void ScriptedActivityComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) const { - outBitStream->Write(true); - outBitStream->Write(m_ActivityPlayers.size()); - - if (!m_ActivityPlayers.empty()) { - for (const auto& activityPlayer : m_ActivityPlayers) { - - outBitStream->Write(activityPlayer->playerID); - for (const auto& activityValue : activityPlayer->values) { - outBitStream->Write(activityValue); - } - } - } -} - -void ScriptedActivityComponent::ReloadConfig() { - CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); - std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.ActivityID == m_ActivityID); }); - for (auto activity : activities) { - auto mapID = m_ActivityInfo.instanceMapID; - if ((mapID == 1203 || mapID == 1261 || mapID == 1303 || mapID == 1403) && 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 ScriptedActivityComponent::HandleMessageBoxResponse(Entity* player, const std::string& id) { - if (m_ActivityInfo.ActivityID == 103) { - return; - } - - if (id == "LobbyExit") { - PlayerLeave(player->GetObjectID()); - } else if (id == "PlayButton") { - PlayerJoin(player); - } -} - -void ScriptedActivityComponent::PlayerJoin(Entity* player) { - if (m_ActivityInfo.ActivityID == 103 || PlayerIsInQueue(player) || !IsValidActivity(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)) { - auto* instance = NewInstance(); - instance->AddParticipant(player); - } - - EntityManager::Instance()->SerializeEntity(m_ParentEntity); -} - -void ScriptedActivityComponent::PlayerJoinLobby(Entity* player) { - if (!m_ParentEntity->HasComponent(eReplicaComponentType::QUICK_BUILD)) - GameMessages::SendMatchResponse(player, player->GetSystemAddress(), 0); // tell the client they joined a lobby - LobbyPlayer* newLobbyPlayer = new LobbyPlayer(); - newLobbyPlayer->entityID = player->GetObjectID(); - Lobby* playerLobby = nullptr; - - auto* character = player->GetCharacter(); - if (character != nullptr) - character->SetLastNonInstanceZoneID(dZoneManager::Instance()->GetZone()->GetWorldID()); - - for (Lobby* 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); - playerLobby = lobby; - - // Update the joining player on players already in the lobby, and update players already in the lobby on the joining player - std::string matchUpdateJoined = "player=9:" + std::to_string(player->GetObjectID()) + "\nplayerName=0:" + player->GetCharacter()->GetName(); - for (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); - - if (entity == nullptr) { - continue; - } - - std::string matchUpdate = "player=9:" + std::to_string(entity->GetObjectID()) + "\nplayerName=0:" + entity->GetCharacter()->GetName(); - GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchUpdate, eMatchUpdate::PLAYER_ADDED); - PlayerReady(entity, joinedPlayer->ready); - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateJoined, eMatchUpdate::PLAYER_ADDED); - } - } - } - - if (!playerLobby) { - // If all lobbies are full - playerLobby = new Lobby(); - playerLobby->players.push_back(newLobbyPlayer); - playerLobby->timer = m_ActivityInfo.waitTime / 1000; - m_Queue.push_back(playerLobby); - } - - if (m_ActivityInfo.maxTeamSize != 1 && playerLobby->players.size() >= m_ActivityInfo.minTeamSize || m_ActivityInfo.maxTeamSize == 1 && playerLobby->players.size() >= m_ActivityInfo.minTeams) { - // Update the joining player on the match timer - std::string matchTimerUpdate = "time=3:" + std::to_string(playerLobby->timer); - GameMessages::SendMatchUpdate(player, player->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); - } -} - -void ScriptedActivityComponent::PlayerLeave(LWOOBJID playerID) { - - // Removes the player from a lobby and notifies the others, not applicable for non-lobby instances - for (Lobby* lobby : m_Queue) { - for (int i = 0; i < lobby->players.size(); ++i) { - if (lobby->players[i]->entityID == playerID) { - std::string matchUpdateLeft = "player=9:" + std::to_string(playerID); - for (LobbyPlayer* lobbyPlayer : lobby->players) { - auto* entity = lobbyPlayer->GetEntity(); - if (entity == nullptr) - continue; - - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchUpdateLeft, eMatchUpdate::PLAYER_REMOVED); - } - - delete lobby->players[i]; - lobby->players[i] = nullptr; - lobby->players.erase(lobby->players.begin() + i); - - return; - } - } - } -} - -void ScriptedActivityComponent::Update(float deltaTime) { - std::vector lobbiesToRemove{}; - // Ticks all the lobbies, not applicable for non-instance activities - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); - if (entity == nullptr) { - PlayerLeave(player->entityID); - return; - } - } - - if (lobby->players.empty()) { - lobbiesToRemove.push_back(lobby); - 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 (LobbyPlayer* joinedPlayer : lobby->players) { - auto* entity = joinedPlayer->GetEntity(); - - if (entity == nullptr) - continue; - - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_READY); - } - } - - lobby->timer -= deltaTime; - } - - bool lobbyReady = true; - for (LobbyPlayer* 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 - std::string matchTimerUpdate = "time=3:" + std::to_string(lobby->timer); - for (LobbyPlayer* player : lobby->players) { - auto* entity = player->GetEntity(); - - if (entity == nullptr) - continue; - - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchTimerUpdate, eMatchUpdate::PHASE_WAIT_START); - } - } - - // The timer has elapsed, start the instance - if (lobby->timer <= 0.0f) { - Game::logger->Log("ScriptedActivityComponent", "Setting up instance."); - ActivityInstance* instance = NewInstance(); - LoadPlayersIntoInstance(instance, lobby->players); - instance->StartZone(); - lobbiesToRemove.push_back(lobby); - } - } - - while (!lobbiesToRemove.empty()) { - RemoveLobby(lobbiesToRemove.front()); - lobbiesToRemove.erase(lobbiesToRemove.begin()); - } -} - -void ScriptedActivityComponent::RemoveLobby(Lobby* lobby) { - for (int i = 0; i < m_Queue.size(); ++i) { - if (m_Queue[i] == lobby) { - m_Queue.erase(m_Queue.begin() + i); - return; - } - } -} - -bool ScriptedActivityComponent::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 ScriptedActivityComponent::IsValidActivity(Entity* player) { - // Makes it so that scripted activities with an unimplemented map cannot be joined - /*if (player->GetGMLevel() < eGameMasterLevel::DEVELOPER && (m_ActivityInfo.instanceMapID == 1302 || m_ActivityInfo.instanceMapID == 1301)) { - if (m_ParentEntity->GetLOT() == 4860) { - auto* missionComponent = player->GetComponent(); - missionComponent->CompleteMission(229); - } - - ChatPackets::SendSystemMessage(player->GetSystemAddress(), u"Sorry, this activity is not ready."); - static_cast(player)->SendToZone(dZoneManager::Instance()->GetZone()->GetWorldID()); // Gets them out of this stuck state - - return false; - }*/ - - return true; -} - -bool ScriptedActivityComponent::PlayerIsInQueue(Entity* player) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (player->GetObjectID() == lobbyPlayer->entityID) return true; - } - } - - return false; -} - -bool ScriptedActivityComponent::IsPlayedBy(Entity* player) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { - if (instancePlayer != nullptr && instancePlayer->GetObjectID() == player->GetObjectID()) - return true; - } - } - - return false; -} - -bool ScriptedActivityComponent::IsPlayedBy(LWOOBJID playerID) const { - for (const auto* instance : this->m_Instances) { - for (const auto* instancePlayer : instance->GetParticipants()) { - if (instancePlayer != nullptr && instancePlayer->GetObjectID() == playerID) - return true; - } - } - - return false; -} - -bool ScriptedActivityComponent::TakeCost(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; - - inventoryComponent->RemoveItem(m_ActivityInfo.optionalCostLOT, m_ActivityInfo.optionalCostCount); - - return true; -} - -void ScriptedActivityComponent::PlayerReady(Entity* player, bool bReady) { - for (Lobby* lobby : m_Queue) { - for (LobbyPlayer* lobbyPlayer : lobby->players) { - if (lobbyPlayer->entityID == player->GetObjectID()) { - - lobbyPlayer->ready = bReady; - - // Update players in lobby on player being ready - std::string matchReadyUpdate = "player=9:" + std::to_string(player->GetObjectID()); - eMatchUpdate readyStatus = eMatchUpdate::PLAYER_READY; - if (!bReady) readyStatus = eMatchUpdate::PLAYER_NOT_READY; - for (LobbyPlayer* otherPlayer : lobby->players) { - auto* entity = otherPlayer->GetEntity(); - if (entity == nullptr) - continue; - - GameMessages::SendMatchUpdate(entity, entity->GetSystemAddress(), matchReadyUpdate, readyStatus); - } - } - } - } -} - -ActivityInstance* ScriptedActivityComponent::NewInstance() { - auto* instance = new ActivityInstance(m_ParentEntity, m_ActivityInfo); - m_Instances.push_back(instance); - return instance; -} - -void ScriptedActivityComponent::LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const { - for (LobbyPlayer* player : lobby) { - auto* entity = player->GetEntity(); - if (entity == nullptr || !TakeCost(entity)) { - continue; - } - - instance->AddParticipant(entity); - } -} - -const std::vector& ScriptedActivityComponent::GetInstances() const { - return m_Instances; -} - -ActivityInstance* ScriptedActivityComponent::GetInstance(const LWOOBJID playerID) { - for (const auto* instance : GetInstances()) { - for (const auto* participant : instance->GetParticipants()) { - if (participant->GetObjectID() == playerID) - return const_cast(instance); - } - } - - return nullptr; -} - -void ScriptedActivityComponent::ClearInstances() { - for (ActivityInstance* instance : m_Instances) { - delete instance; - } - m_Instances.clear(); -} - -ActivityPlayer* ScriptedActivityComponent::GetActivityPlayerData(LWOOBJID playerID) { - for (auto* activityData : m_ActivityPlayers) { - if (activityData->playerID == playerID) { - return activityData; - } - } - - return nullptr; -} - -void ScriptedActivityComponent::RemoveActivityPlayerData(LWOOBJID playerID) { - for (size_t i = 0; i < m_ActivityPlayers.size(); i++) { - if (m_ActivityPlayers[i]->playerID == playerID) { - delete m_ActivityPlayers[i]; - m_ActivityPlayers[i] = nullptr; - - m_ActivityPlayers.erase(m_ActivityPlayers.begin() + i); - EntityManager::Instance()->SerializeEntity(m_ParentEntity); - - return; - } - } -} - -ActivityPlayer* ScriptedActivityComponent::AddActivityPlayerData(LWOOBJID playerID) { - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) - return data; - - m_ActivityPlayers.push_back(new ActivityPlayer{ playerID, {} }); - EntityManager::Instance()->SerializeEntity(m_ParentEntity); - - return GetActivityPlayerData(playerID); -} - -float_t ScriptedActivityComponent::GetActivityValue(LWOOBJID playerID, uint32_t index) { - auto value = -1.0f; - - auto* data = GetActivityPlayerData(playerID); - if (data != nullptr) { - value = data->values[std::min(index, (uint32_t)9)]; - } - - return value; -} - -void ScriptedActivityComponent::SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value) { - auto* data = AddActivityPlayerData(playerID); - if (data != nullptr) { - data->values[std::min(index, (uint32_t)9)] = value; - } - - EntityManager::Instance()->SerializeEntity(m_ParentEntity); -} - -void ScriptedActivityComponent::PlayerRemove(LWOOBJID playerID) { - for (auto* instance : GetInstances()) { - 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(std::find(m_Instances.begin(), m_Instances.end(), instance)); - delete instance; - } - 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; - PacketUtils::WriteHeader(bitStream, eConnectionType::CHAT_INTERNAL, eChatInternalMessageType::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 = EntityManager::Instance()->GetEntity(objid); - if (player == nullptr) - return; - - Game::logger->Log("UserManager", "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()) { - 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::Instance().GetTable(); - std::vector activityRewards = activityRewardsTable->Query([=](CDActivityRewards entry) { return (entry.objectTemplate == m_ActivityInfo.ActivityID); }); - - if (!activityRewards.empty()) { - uint32_t minCoins = 0; - uint32_t maxCoins = 0; - - auto* currencyTableTable = CDClientManager::Instance().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; - } - - LootGenerator::Instance().DropLoot(participant, m_ParentEntity, 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 = EntityManager::Instance()->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 EntityManager::Instance()->GetEntity(entityID); } diff --git a/dGame/dComponents/ScriptedActivityComponent.h b/dGame/dComponents/ScriptedActivityComponent.h index d9262deb..adcbf42f 100644 --- a/dGame/dComponents/ScriptedActivityComponent.h +++ b/dGame/dComponents/ScriptedActivityComponent.h @@ -1,381 +1,18 @@ -/* - * Darkflame Universe - * Copyright 2018 - */ -#include "CDClientManager.h" +#ifndef __SCRIPTEDACTIVITYCOMPONENT__H__ +#define __SCRIPTEDACTIVITYCOMPONENT__H__ -#ifndef SCRIPTEDACTIVITYCOMPONENT_H -#define SCRIPTEDACTIVITYCOMPONENT_H - -#include "BitStream.h" -#include "Entity.h" -#include "Component.h" +#include "ActivityComponent.h" #include "eReplicaComponentType.h" -#include "CDActivitiesTable.h" +class Entity; - /** - * Represents an instance of an activity, having participants and score - */ -class ActivityInstance { -public: - ActivityInstance(Entity* parent, CDActivities activityInfo) { m_ParentEntity = parent; m_ActivityInfo = activityInfo; }; - //~ActivityInstance(); - - /** - * Adds an entity to this activity - * @param participant the entity to add - */ - void AddParticipant(Entity* participant); - - /** - * Removes all the participants from this activity - */ - void ClearParticipants() { m_Participants.clear(); }; - - /** - * Starts the instance world for this activity and sends all participants there - */ - void StartZone(); - - /** - * Gives the rewards for completing this activity to some participant - * @param participant the participant to give rewards to - */ - void RewardParticipant(Entity* participant); - - /** - * Removes a participant from this activity - * @param participant the participant to remove - */ - void RemoveParticipant(const Entity* participant); - - /** - * Returns all the participants of this activity - * @return all the participants of this activity - */ - std::vector GetParticipants() const; - - /** - * Currently unused - */ - uint32_t GetScore() const; - - /** - * Currently unused - */ - void SetScore(uint32_t score); -private: - - /** - * Currently unused - */ - uint32_t score = 0; - - /** - * The instance ID of this activity - */ - uint32_t m_NextZoneCloneID = 0; - - /** - * The database information for this activity - */ - CDActivities m_ActivityInfo; - - /** - * The entity that owns this activity (the entity that has the ScriptedActivityComponent) - */ - Entity* m_ParentEntity; - - /** - * All the participants of this activity - */ - std::vector m_Participants; -}; - -/** - * Represents an entity in a lobby - */ -struct LobbyPlayer { - - /** - * The ID of the entity that is in the lobby - */ - LWOOBJID entityID; - - /** - * Whether or not the entity is ready - */ - bool ready = false; - - /** - * Returns the entity that is in the lobby - * @return the entity that is in the lobby - */ - Entity* GetEntity() const; -}; - -/** - * Represents a lobby of players with a timer until it should start the activity - */ -struct Lobby { - - /** - * The lobby of players - */ - std::vector players; - - /** - * The timer that determines when the activity should start - */ - float timer; -}; - -/** - * Represents the score for the player in an activity, one index might represent score, another one time, etc. - */ -struct ActivityPlayer { - - /** - * The entity that the score is tracked for - */ - LWOOBJID playerID; - - /** - * The list of score for this entity - */ - float values[10]; -}; - -/** - * Welcome to the absolute behemoth that is the scripted activity component. I have now clue how this was managed in - * live but I figure somewhat similarly and it's terrible. In a nutshell, this components handles any activity that - * can be done in the game from quick builds to boss fights to races. On top of that, this component handles instancing - * and lobbying. - */ -class ScriptedActivityComponent : public Component { +class ScriptedActivityComponent : public ActivityComponent { public: inline static const eReplicaComponentType ComponentType = eReplicaComponentType::SCRIPTED_ACTIVITY; - - ScriptedActivityComponent(Entity* parent, int activityID); - ~ScriptedActivityComponent() override; - - void Update(float deltaTime) override; - void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) const; - - /** - * Makes some entity join the minigame, if it's a lobbied one, the entity will be placed in the lobby - * @param player the entity to join the game - */ - void PlayerJoin(Entity* player); - - /** - * Makes an entity join the lobby for this minigame, if it exists - * @param player the entity to join - */ - void PlayerJoinLobby(Entity* player); - - /** - * Makes the player leave the lobby - * @param playerID the entity to leave the lobby - */ - void PlayerLeave(LWOOBJID playerID); - - /** - * Removes the entity from the minigame (and its score) - * @param playerID the entity to remove from the minigame - */ - void PlayerRemove(LWOOBJID playerID); - - /** - * Adds all the players to an instance of some activity - * @param instance the instance to load the players into - * @param lobby the players to load into the instance - */ - void LoadPlayersIntoInstance(ActivityInstance* instance, const std::vector& lobby) const; - - /** - * Removes a lobby from the activity manager - * @param lobby the lobby to remove - */ - void RemoveLobby(Lobby* lobby); - - /** - * Marks a player as (un)ready in a lobby - * @param player the entity to mark - * @param bReady true if the entity is ready, false otherwise - */ - void PlayerReady(Entity* player, bool bReady); - - /** - * Returns the ID of this activity - * @return the ID of this activity - */ - int GetActivityID() { return m_ActivityInfo.ActivityID; } - - /** - * Returns if this activity has a lobby, e.g. if it needs to instance players to some other map - * @return true if this activity has a lobby, false otherwise - */ - bool HasLobby() const; - - /** - * Checks if a player is currently waiting in a lobby - * @param player the entity to check for - * @return true if the entity is waiting in a lobby, false otherwise - */ - bool PlayerIsInQueue(Entity* player); - - /** - * Checks if an entity is currently playing this activity - * @param player the entity to check - * @return true if the entity is playing this lobby, false otherwise - */ - bool IsPlayedBy(Entity* player) const; - - /** - * Checks if an entity is currently playing this activity - * @param playerID the entity to check - * @return true if the entity is playing this lobby, false otherwise - */ - bool IsPlayedBy(LWOOBJID playerID) const; - - /** - * Legacy: used to check for unimplemented maps, gladly, this now just returns true :) - */ - bool IsValidActivity(Entity* player); - - /** - * Removes the cost of the activity (e.g. green imaginate) for the entity that plays this activity - * @param player the entity to take cost for - * @return true if the cost was successfully deducted, false otherwise - */ - bool TakeCost(Entity* player) const; - - /** - * Handles any response from a player clicking on a lobby / instance menu - * @param player the entity that clicked - * @param id the message that was passed - */ - void HandleMessageBoxResponse(Entity* player, const std::string& id); - - /** - * Creates a new instance for this activity - * @return a new instance for this activity - */ - ActivityInstance* NewInstance(); - - /** - * Returns all the currently active instances of this activity - * @return all the currently active instances of this activity - */ - const std::vector& GetInstances() const; - - /** - * Returns the instance that some entity is currently playing in - * @param playerID the entity to check for - * @return if any, the instance that the entity is currently in - */ - ActivityInstance* GetInstance(const LWOOBJID playerID); - - /** - * @brief Reloads the config settings for this component - * - */ - void ReloadConfig(); - - /** - * Removes all the instances - */ - void ClearInstances(); - - /** - * Returns all the score for the players that are currently playing this activity - * @return - */ - std::vector GetActivityPlayers() { return m_ActivityPlayers; }; - - /** - * Returns activity data for a specific entity (e.g. score and such). - * @param playerID the entity to get data for - * @return the activity data (score) for the passed player in this activity, if it exists - */ - ActivityPlayer* GetActivityPlayerData(LWOOBJID playerID); - - /** - * Sets some score value for an entity - * @param playerID the entity to set score for - * @param index the score index to set - * @param value the value to set in for that index - */ - void SetActivityValue(LWOOBJID playerID, uint32_t index, float_t value); - - /** - * Returns activity score for the passed parameters - * @param playerID the entity to get score for - * @param index the index to get score for - * @return activity score for the passed parameters - */ - float_t GetActivityValue(LWOOBJID playerID, uint32_t index); - - /** - * Removes activity score tracking for some entity - * @param playerID the entity to remove score for - */ - void RemoveActivityPlayerData(LWOOBJID playerID); - - /** - * Adds activity score tracking for some entity - * @param playerID the entity to add the activity score for - * @return the created entry - */ - ActivityPlayer* AddActivityPlayerData(LWOOBJID playerID); - - /** - * Sets the mapID that this activity points to - * @param mapID the map ID to set - */ - void SetInstanceMapID(uint32_t mapID) { m_ActivityInfo.instanceMapID = mapID; }; - - /** - * Returns the LMI that this activity points to for a team size - * @param teamSize the team size to get the LMI for - * @return the LMI that this activity points to for a team size - */ - uint32_t GetLootMatrixForTeamSize(uint32_t teamSize) { return m_ActivityLootMatrices[teamSize]; } -private: - - /** - * The database information for this activity - */ - CDActivities m_ActivityInfo; - - /** - * All the active instances of this activity - */ - std::vector m_Instances; - - /** - * The current lobbies for this activity - */ - std::vector m_Queue; - - /** - * All the activity score for the players in this activity - */ - std::vector m_ActivityPlayers; - - /** - * LMIs for team sizes - */ - std::unordered_map m_ActivityLootMatrices; - - /** - * The activity id - * - */ - int32_t m_ActivityID; + ScriptedActivityComponent(Entity* parent); }; -#endif // SCRIPTEDACTIVITYCOMPONENT_H +#endif //!__SCRIPTEDACTIVITYCOMPONENT__H__ + + diff --git a/dGame/dComponents/VehicleRacingControlComponent.cpp b/dGame/dComponents/VehicleRacingControlComponent.cpp deleted file mode 100644 index 9cfe38b1..00000000 --- a/dGame/dComponents/VehicleRacingControlComponent.cpp +++ /dev/null @@ -1,880 +0,0 @@ -/** - * Thanks to Simon for his early research on the racing system. - */ - -#include "VehicleRacingControlComponent.h" - -#include "CharacterComponent.h" -#include "DestroyableComponent.h" -#include "EntityManager.h" -#include "GameMessages.h" -#include "InventoryComponent.h" -#include "Item.h" -#include "MissionComponent.h" -#include "ModuleAssemblyComponent.h" -#include "Player.h" -#include "PossessableComponent.h" -#include "PossessionComponent.h" -#include "eRacingTaskParam.h" -#include "Spawner.h" -#include "dServer.h" -#include "dZoneManager.h" -#include "dConfig.h" -#include "Loot.h" -#include "eMissionTaskType.h" -#include "dZoneManager.h" -#include "CDActivitiesTable.h" - -#ifndef M_PI -#define M_PI 3.14159265358979323846264338327950288 -#endif - -VehicleRacingControlComponent::VehicleRacingControlComponent(Entity* parent, int32_t componentId) : RacingControlComponent(parent, componentId) { - m_PathName = u"MainPath"; - m_RemainingLaps = 3; - m_LeadingPlayer = LWOOBJID_EMPTY; - m_RaceBestTime = 0; - m_RaceBestLap = 0; - m_Started = false; - m_StartTimer = 0; - m_Loaded = false; - m_LoadedPlayers = 0; - m_LoadTimer = 0; - m_Finished = 0; - m_StartTime = 0; - m_EmptyTimer = 0; - m_SoloRacing = Game::config->GetValue("solo_racing") == "1"; - - m_MainWorld = 1200; - const auto worldID = Game::server->GetZoneID(); - if (dZoneManager::Instance()->CheckIfAccessibleZone((worldID/10)*10)) m_MainWorld = (worldID/10)*10; - - m_ActivityID = 42; - CDActivitiesTable* activitiesTable = CDClientManager::Instance().GetTable(); - std::vector activities = activitiesTable->Query([=](CDActivities entry) {return (entry.instanceMapID == worldID); }); - for (CDActivities activity : activities) m_ActivityID = activity.ActivityID; -} - -VehicleRacingControlComponent::~VehicleRacingControlComponent() {} - -void VehicleRacingControlComponent::OnPlayerLoaded(Entity* player) { - // If the race has already started, send the player back to the main world. - if (m_Loaded) { - auto* playerInstance = dynamic_cast(player); - - playerInstance->SendToZone(m_MainWorld); - - return; - } - - const auto objectID = player->GetObjectID(); - - m_LoadedPlayers++; - - Game::logger->Log("VehicleRacingControlComponent", "Loading player %i", - m_LoadedPlayers); - - m_LobbyPlayers.push_back(objectID); -} - -void VehicleRacingControlComponent::LoadPlayerVehicle(Entity* player, - uint32_t positionNumber, bool initialLoad) { - // Load the player's vehicle. - - if (player == nullptr) { - return; - } - - auto* inventoryComponent = player->GetComponent(); - - if (inventoryComponent == nullptr) { - return; - } - - // Find the player's vehicle. - - auto* item = inventoryComponent->FindItemByLot(8092); - - if (item == nullptr) { - Game::logger->Log("VehicleRacingControlComponent", "Failed to find item"); - - return; - } - - // Calculate the vehicle's starting position. - - auto* path = dZoneManager::Instance()->GetZone()->GetPath( - GeneralUtils::UTF16ToWTF8(m_PathName)); - - auto spawnPointEntities = EntityManager::Instance()->GetEntitiesByLOT(4843); - auto startPosition = NiPoint3::ZERO; - auto startRotation = NiQuaternion::IDENTITY; - const std::string placementAsString = std::to_string(positionNumber); - for (auto entity : spawnPointEntities) { - if (!entity) continue; - if (entity->GetVarAsString(u"placement") == placementAsString) { - startPosition = entity->GetPosition(); - startRotation = entity->GetRotation(); - break; - } - } - - // Make sure the player is at the correct position. - - GameMessages::SendTeleport(player->GetObjectID(), startPosition, - startRotation, player->GetSystemAddress(), true); - - // Spawn the vehicle entity. - - EntityInfo info{}; - info.lot = 8092; - info.pos = startPosition; - info.rot = startRotation; - info.spawnerID = m_ParentEntity->GetObjectID(); - - auto* carEntity = - EntityManager::Instance()->CreateEntity(info, nullptr, m_ParentEntity); - - // Make the vehicle a child of the racing controller. - m_ParentEntity->AddChild(carEntity); - - auto* destroyableComponent = carEntity->GetComponent(); - - // Setup the vehicle stats. - if (destroyableComponent != nullptr) { - destroyableComponent->SetMaxImagination(60); - destroyableComponent->SetImagination(0); - } - - // Setup the vehicle as being possessed by the player. - auto* possessableComponent = carEntity->GetComponent(); - - if (possessableComponent != nullptr) { - possessableComponent->SetPossessor(player->GetObjectID()); - } - - // Load the vehicle's assemblyPartLOTs for display. - auto* moduleAssemblyComponent = carEntity->GetComponent(); - - if (moduleAssemblyComponent) { - moduleAssemblyComponent->SetSubKey(item->GetSubKey()); - moduleAssemblyComponent->SetUseOptionalParts(false); - - for (auto* config : item->GetConfig()) { - if (config->GetKey() == u"assemblyPartLOTs") { - moduleAssemblyComponent->SetAssemblyPartsLOTs( - GeneralUtils::ASCIIToUTF16(config->GetValueAsString())); - } - } - } - - // Setup the player as possessing the vehicle. - auto* possessionComponent = player->GetComponent(); - - if (possessionComponent != nullptr) { - possessionComponent->SetPossessable(carEntity->GetObjectID()); - possessionComponent->SetPossessableType(ePossessionType::ATTACHED_VISIBLE); // for racing it's always Attached_Visible - } - - // Set the player's current activity as racing. - auto* characterComponent = player->GetComponent(); - - if (characterComponent != nullptr) { - characterComponent->SetIsRacing(true); - } - - // Init the player's racing entry. - if (initialLoad) { - m_RacingPlayers.push_back( - { player->GetObjectID(), - carEntity->GetObjectID(), - static_cast(m_RacingPlayers.size()), - false, - {}, - startPosition, - startRotation, - 0, - 0, - 0, - 0 }); - } - - // Construct and serialize everything when done. - - EntityManager::Instance()->ConstructEntity(carEntity); - EntityManager::Instance()->SerializeEntity(player); - EntityManager::Instance()->SerializeEntity(m_ParentEntity); - - GameMessages::SendRacingSetPlayerResetInfo( - m_ParentEntity->GetObjectID(), 0, 0, player->GetObjectID(), startPosition, 1, - UNASSIGNED_SYSTEM_ADDRESS); - - const auto playerID = player->GetObjectID(); - - // Reset the player to the start position during downtime, in case something - // went wrong. - m_ParentEntity->AddCallbackTimer(1, [this, playerID]() { - auto* player = EntityManager::Instance()->GetEntity(playerID); - - if (player == nullptr) { - return; - } - - GameMessages::SendRacingResetPlayerToLastReset( - m_ParentEntity->GetObjectID(), playerID, UNASSIGNED_SYSTEM_ADDRESS); - }); - - GameMessages::SendSetJetPackMode(player, false); - - // Set the vehicle's state. - GameMessages::SendNotifyVehicleOfRacingObject(carEntity->GetObjectID(), - m_ParentEntity->GetObjectID(), - UNASSIGNED_SYSTEM_ADDRESS); - - GameMessages::SendVehicleSetWheelLockState(carEntity->GetObjectID(), false, - initialLoad, - UNASSIGNED_SYSTEM_ADDRESS); - - // Make sure everything has the correct position. - GameMessages::SendTeleport(player->GetObjectID(), startPosition, - startRotation, player->GetSystemAddress(), true); - GameMessages::SendTeleport(carEntity->GetObjectID(), startPosition, - startRotation, player->GetSystemAddress(), true); -} - -void VehicleRacingControlComponent::OnRacingClientReady(Entity* player) { - // Notify the other players that this player is ready. - - for (auto& racingPlayer : m_RacingPlayers) { - if (racingPlayer.playerID != player->GetObjectID()) { - if (racingPlayer.playerLoaded) { - GameMessages::SendRacingPlayerLoaded( - m_ParentEntity->GetObjectID(), racingPlayer.playerID, - racingPlayer.vehicleID, UNASSIGNED_SYSTEM_ADDRESS); - } - - continue; - } - - racingPlayer.playerLoaded = true; - - GameMessages::SendRacingPlayerLoaded( - m_ParentEntity->GetObjectID(), racingPlayer.playerID, - racingPlayer.vehicleID, UNASSIGNED_SYSTEM_ADDRESS); - } - - EntityManager::Instance()->SerializeEntity(m_ParentEntity); -} - -void VehicleRacingControlComponent::OnRequestDie(Entity* player) { - // Sent by the client when they collide with something which should smash - // them. - - for (auto& racingPlayer : m_RacingPlayers) { - if (racingPlayer.playerID != player->GetObjectID()) { - continue; - } - - auto* vehicle = - EntityManager::Instance()->GetEntity(racingPlayer.vehicleID); - - if (!vehicle) return; - - if (!racingPlayer.noSmashOnReload) { - racingPlayer.smashedTimes++; - GameMessages::SendDie(vehicle, vehicle->GetObjectID(), LWOOBJID_EMPTY, true, - eKillType::VIOLENT, u"", 0, 0, 90.0f, false, true, 0); - - auto* destroyableComponent = vehicle->GetComponent(); - uint32_t respawnImagination = 0; - // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. - // Do not actually change the value yet. Do that on respawn. - if (destroyableComponent) { - respawnImagination = static_cast(ceil(destroyableComponent->GetImagination() / 2.0f / 10.0f)) * 10.0f; - GameMessages::SendSetResurrectRestoreValues(vehicle, -1, -1, respawnImagination); - } - - // Respawn the player in 2 seconds, as was done in live. Not sure if this value is in a setting somewhere else... - vehicle->AddCallbackTimer(2.0f, [=]() { - if (!vehicle || !this->m_ParentEntity) return; - GameMessages::SendRacingResetPlayerToLastReset( - m_ParentEntity->GetObjectID(), racingPlayer.playerID, - UNASSIGNED_SYSTEM_ADDRESS); - - GameMessages::SendVehicleStopBoost(vehicle, player->GetSystemAddress(), true); - - GameMessages::SendRacingSetPlayerResetInfo( - m_ParentEntity->GetObjectID(), racingPlayer.lap, - racingPlayer.respawnIndex, player->GetObjectID(), - racingPlayer.respawnPosition, racingPlayer.respawnIndex + 1, - UNASSIGNED_SYSTEM_ADDRESS); - - GameMessages::SendResurrect(vehicle); - auto* destroyableComponent = vehicle->GetComponent(); - // Reset imagination to half its current value, rounded up to the nearest value divisible by 10, as it was done in live. - if (destroyableComponent) destroyableComponent->SetImagination(respawnImagination); - EntityManager::Instance()->SerializeEntity(vehicle); - }); - - auto* characterComponent = player->GetComponent(); - if (characterComponent != nullptr) { - characterComponent->UpdatePlayerStatistic(RacingTimesWrecked); - } - } else { - GameMessages::SendRacingSetPlayerResetInfo( - m_ParentEntity->GetObjectID(), racingPlayer.lap, - racingPlayer.respawnIndex, player->GetObjectID(), - racingPlayer.respawnPosition, racingPlayer.respawnIndex + 1, - UNASSIGNED_SYSTEM_ADDRESS); - GameMessages::SendRacingResetPlayerToLastReset( - m_ParentEntity->GetObjectID(), racingPlayer.playerID, - UNASSIGNED_SYSTEM_ADDRESS); - } - } -} - -void VehicleRacingControlComponent::OnRacingPlayerInfoResetFinished(Entity* player) { - // When the player has respawned. - - for (auto& racingPlayer : m_RacingPlayers) { - if (racingPlayer.playerID != player->GetObjectID()) { - continue; - } - - auto* vehicle = - EntityManager::Instance()->GetEntity(racingPlayer.vehicleID); - - if (vehicle == nullptr) { - return; - } - - racingPlayer.noSmashOnReload = false; - - return; - } -} - -void VehicleRacingControlComponent::HandleMessageBoxResponse(Entity* player, int32_t button, const std::string& id) { - auto* data = GetPlayerData(player->GetObjectID()); - - if (data == nullptr) { - return; - } - - if (id == "rewardButton") { - if (data->collectedRewards) { - return; - } - - data->collectedRewards = true; - - // Calculate the score, different loot depending on player count - const auto score = m_LoadedPlayers * 10 + data->finished; - - LootGenerator::Instance().GiveActivityLoot(player, m_ParentEntity, m_ActivityID, score); - - // Giving rewards - GameMessages::SendNotifyRacingClient( - m_ParentEntity->GetObjectID(), 2, 0, LWOOBJID_EMPTY, u"", - player->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); - - auto* missionComponent = player->GetComponent(); - - if (missionComponent == nullptr) return; - - missionComponent->Progress(eMissionTaskType::RACING, 0, (LWOOBJID)eRacingTaskParam::COMPETED_IN_RACE); // Progress task for competing in a race - missionComponent->Progress(eMissionTaskType::RACING, data->smashedTimes, (LWOOBJID)eRacingTaskParam::SAFE_DRIVER); // Finish a race without being smashed. - - // If solo racing is enabled OR if there are 3 players in the race, progress placement tasks. - if (m_SoloRacing || m_LoadedPlayers > 2) { - missionComponent->Progress(eMissionTaskType::RACING, data->finished, (LWOOBJID)eRacingTaskParam::FINISH_WITH_PLACEMENT); // Finish in 1st place on a race - if (data->finished == 1) { - missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::FIRST_PLACE_MULTIPLE_TRACKS); // Finish in 1st place on multiple tracks. - missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::WIN_RACE_IN_WORLD); // Finished first place in specific world. - } - if (data->finished == m_LoadedPlayers) { - missionComponent->Progress(eMissionTaskType::RACING, dZoneManager::Instance()->GetZone()->GetWorldID(), (LWOOBJID)eRacingTaskParam::LAST_PLACE_FINISH); // Finished first place in specific world. - } - } - } else if ((id == "ACT_RACE_EXIT_THE_RACE?" || id == "Exit") && button == m_ActivityExitConfirm) { - auto* vehicle = EntityManager::Instance()->GetEntity(data->vehicleID); - - if (vehicle == nullptr) { - return; - } - - // Exiting race - GameMessages::SendNotifyRacingClient( - m_ParentEntity->GetObjectID(), 3, 0, LWOOBJID_EMPTY, u"", - player->GetObjectID(), UNASSIGNED_SYSTEM_ADDRESS); - - auto* playerInstance = dynamic_cast(player); - - playerInstance->SendToZone(m_MainWorld); - - vehicle->Kill(); - } -} - -void VehicleRacingControlComponent::Serialize(RakNet::BitStream* outBitStream, - bool bIsInitialUpdate, - unsigned int& flags) { - // BEGIN Scripted Activity - - outBitStream->Write1(); - - outBitStream->Write(static_cast(m_RacingPlayers.size())); - for (const auto& player : m_RacingPlayers) { - outBitStream->Write(player.playerID); - - for (int i = 0; i < 10; i++) { - outBitStream->Write(player.data[i]); - } - } - - // END Scripted Activity - - outBitStream->Write1(); // Dirty? - outBitStream->Write(static_cast(m_RacingPlayers.size())); - - outBitStream->Write(!m_RacingPlayers.empty()); - if (!m_RacingPlayers.empty()) { - for (const auto& player : m_RacingPlayers) { - outBitStream->Write1(); // Has more date - - outBitStream->Write(player.playerID); - outBitStream->Write(player.vehicleID); - outBitStream->Write(player.playerIndex); - outBitStream->Write(player.playerLoaded); - } - - outBitStream->Write0(); // No more data - } - - outBitStream->Write(!m_RacingPlayers.empty()); - if (!m_RacingPlayers.empty()) { - for (const auto& player : m_RacingPlayers) { - outBitStream->Write1(); // Has more date - - outBitStream->Write(player.playerID); - outBitStream->Write(0); - } - - outBitStream->Write0(); // No more data - } - - outBitStream->Write1(); // Dirty? - - outBitStream->Write(m_RemainingLaps); - - outBitStream->Write(static_cast(m_PathName.size())); - for (const auto character : m_PathName) { - outBitStream->Write(character); - } - - outBitStream->Write1(); // ??? - outBitStream->Write1(); // ??? - - outBitStream->Write(m_LeadingPlayer); - outBitStream->Write(m_RaceBestLap); - outBitStream->Write(m_RaceBestTime); -} - -RacingPlayerInfo* VehicleRacingControlComponent::GetPlayerData(LWOOBJID playerID) { - for (auto& player : m_RacingPlayers) { - if (player.playerID == playerID) { - return &player; - } - } - - return nullptr; -} - -void VehicleRacingControlComponent::Update(float deltaTime) { - // This method is a mess. - - // Pre-load routine - if (!m_Loaded) { - // Check if any players has disconnected before loading in - for (size_t i = 0; i < m_LobbyPlayers.size(); i++) { - auto* playerEntity = - EntityManager::Instance()->GetEntity(m_LobbyPlayers[i]); - - if (playerEntity == nullptr) { - --m_LoadedPlayers; - - m_LobbyPlayers.erase(m_LobbyPlayers.begin() + i); - - return; - } - } - - if (m_LoadedPlayers >= 2 || (m_LoadedPlayers == 1 && m_SoloRacing)) { - m_LoadTimer += deltaTime; - } else { - m_EmptyTimer += deltaTime; - } - - // If a player happens to be left alone for more then 30 seconds without - // anyone else loading in, send them back to the main world - if (m_EmptyTimer >= 30) { - for (const auto player : m_LobbyPlayers) { - auto* playerEntity = - EntityManager::Instance()->GetEntity(player); - - if (playerEntity == nullptr) { - continue; - } - - auto* playerInstance = dynamic_cast(playerEntity); - - playerInstance->SendToZone(m_MainWorld); - } - - m_LobbyPlayers.clear(); - } - - // From the first 2 players loading in the rest have a max of 15 seconds - // to load in, can raise this if it's too low - if (m_LoadTimer >= 15) { - Game::logger->Log("VehicleRacingControlComponent", - "Loading all players..."); - - for (size_t positionNumber = 0; positionNumber < m_LobbyPlayers.size(); positionNumber++) { - Game::logger->Log("VehicleRacingControlComponent", - "Loading player now!"); - - auto* player = - EntityManager::Instance()->GetEntity(m_LobbyPlayers[positionNumber]); - - if (player == nullptr) { - return; - } - - Game::logger->Log("VehicleRacingControlComponent", - "Loading player now NOW!"); - - LoadPlayerVehicle(player, positionNumber + 1, true); - - m_Loaded = true; - } - - m_Loaded = true; - } - - return; - } - - // The players who will be participating have loaded - if (!m_Started) { - // Check if anyone has disconnected during this period - for (size_t i = 0; i < m_RacingPlayers.size(); i++) { - auto* playerEntity = EntityManager::Instance()->GetEntity( - m_RacingPlayers[i].playerID); - - if (playerEntity == nullptr) { - m_RacingPlayers.erase(m_RacingPlayers.begin() + i); - - --m_LoadedPlayers; - - return; - } - } - - // If less then 2 players are left, send the rest back to the main world - if (m_LoadedPlayers < 2 && !(m_LoadedPlayers == 1 && m_SoloRacing)) { - for (const auto player : m_LobbyPlayers) { - auto* playerEntity = - EntityManager::Instance()->GetEntity(player); - - if (playerEntity == nullptr) { - continue; - } - - auto* playerInstance = dynamic_cast(playerEntity); - - playerInstance->SendToZone(m_MainWorld); - } - - return; - } - - // Check if all players have send a ready message - - int32_t readyPlayers = 0; - - for (const auto& player : m_RacingPlayers) { - if (player.playerLoaded) { - ++readyPlayers; - } - } - - if (readyPlayers >= m_LoadedPlayers) { - // Setup for racing - if (m_StartTimer == 0) { - GameMessages::SendNotifyRacingClient( - m_ParentEntity->GetObjectID(), 1, 0, LWOOBJID_EMPTY, u"", - LWOOBJID_EMPTY, UNASSIGNED_SYSTEM_ADDRESS); - - for (const auto& player : m_RacingPlayers) { - auto* vehicle = - EntityManager::Instance()->GetEntity(player.vehicleID); - auto* playerEntity = - EntityManager::Instance()->GetEntity(player.playerID); - - if (vehicle != nullptr && playerEntity != nullptr) { - GameMessages::SendTeleport( - player.playerID, player.respawnPosition, - player.respawnRotation, - playerEntity->GetSystemAddress(), true); - - vehicle->SetPosition(player.respawnPosition); - vehicle->SetRotation(player.respawnRotation); - - auto* destroyableComponent = vehicle->GetComponent(); - - if (destroyableComponent != nullptr) { - destroyableComponent->SetImagination(0); - } - - EntityManager::Instance()->SerializeEntity(vehicle); - EntityManager::Instance()->SerializeEntity( - playerEntity); - } - } - - // Spawn imagination pickups - auto* minSpawner = dZoneManager::Instance()->GetSpawnersByName( - "ImaginationSpawn_Min")[0]; - auto* medSpawner = dZoneManager::Instance()->GetSpawnersByName( - "ImaginationSpawn_Med")[0]; - auto* maxSpawner = dZoneManager::Instance()->GetSpawnersByName( - "ImaginationSpawn_Max")[0]; - - minSpawner->Activate(); - - if (m_LoadedPlayers > 2) { - medSpawner->Activate(); - } - - if (m_LoadedPlayers > 4) { - maxSpawner->Activate(); - } - - // Reset players to their start location, without smashing them - for (auto& player : m_RacingPlayers) { - auto* vehicleEntity = - EntityManager::Instance()->GetEntity(player.vehicleID); - auto* playerEntity = - EntityManager::Instance()->GetEntity(player.playerID); - - if (vehicleEntity == nullptr || playerEntity == nullptr) { - continue; - } - - player.noSmashOnReload = true; - - OnRequestDie(playerEntity); - } - } - // This 6 seconds seems to be hardcoded in the client, start race - // after that amount of time - else if (m_StartTimer >= 6) { - // Activate the players movement - for (auto& player : m_RacingPlayers) { - auto* vehicleEntity = - EntityManager::Instance()->GetEntity(player.vehicleID); - auto* playerEntity = - EntityManager::Instance()->GetEntity(player.playerID); - - if (vehicleEntity == nullptr || playerEntity == nullptr) { - continue; - } - - GameMessages::SendVehicleUnlockInput( - player.vehicleID, false, UNASSIGNED_SYSTEM_ADDRESS); - } - - // Start the race - GameMessages::SendActivityStart(m_ParentEntity->GetObjectID(), - UNASSIGNED_SYSTEM_ADDRESS); - - m_Started = true; - - Game::logger->Log("VehicleRacingControlComponent", "Starting race"); - - EntityManager::Instance()->SerializeEntity(m_ParentEntity); - - m_StartTime = std::time(nullptr); - } - - m_StartTimer += deltaTime; - } else { - m_StartTimer = 0; - } - - return; - } - - // Race routines - auto* path = dZoneManager::Instance()->GetZone()->GetPath( - GeneralUtils::UTF16ToWTF8(m_PathName)); - - for (auto& player : m_RacingPlayers) { - auto* vehicle = EntityManager::Instance()->GetEntity(player.vehicleID); - auto* playerEntity = - EntityManager::Instance()->GetEntity(player.playerID); - - if (vehicle == nullptr || playerEntity == nullptr) { - continue; - } - - const auto vehiclePosition = vehicle->GetPosition(); - - // If the player is this far below the map, safe to assume they should - // be smashed by death plane - if (vehiclePosition.y < -500) { - GameMessages::SendDie(vehicle, m_ParentEntity->GetObjectID(), - LWOOBJID_EMPTY, true, eKillType::VIOLENT, u"", 0, 0, 0, - true, false, 0); - - OnRequestDie(playerEntity); - - continue; - } - - // Loop through all the waypoints and see if the player has reached a - // new checkpoint - uint32_t respawnIndex = 0; - for (const auto& waypoint : path->pathWaypoints) { - if (player.lap == 3) { - break; - } - - if (player.respawnIndex == respawnIndex) { - ++respawnIndex; - - continue; - } - - const auto& position = waypoint.position; - - if (std::abs((int)respawnIndex - (int)player.respawnIndex) > 10 && - player.respawnIndex != path->pathWaypoints.size() - 1) { - ++respawnIndex; - - continue; - } - - if (Vector3::DistanceSquared(position, vehiclePosition) > 50 * 50) { - ++respawnIndex; - - continue; - } - - // Only go upwards, except if we've lapped - // Not sure how we are supposed to check if they've reach a - // checkpoint, within 50 units seems safe - if (!(respawnIndex > player.respawnIndex || - player.respawnIndex == path->pathWaypoints.size() - 1)) { - ++respawnIndex; - - continue; - } - - // Some offset up to make they don't fall through the terrain on a - // respawn, seems to fix itself to the track anyhow - player.respawnPosition = position + NiPoint3::UNIT_Y * 5; - player.respawnRotation = vehicle->GetRotation(); - player.respawnIndex = respawnIndex; - - // Reached the start point, lapped - if (respawnIndex == 0) { - time_t lapTime = std::time(nullptr) - (player.lap == 0 ? m_StartTime : player.lapTime); - - // Cheating check - if (lapTime < 40) { - continue; - } - - player.lap++; - - player.lapTime = std::time(nullptr); - - if (player.bestLapTime == 0 || player.bestLapTime > lapTime) { - player.bestLapTime = lapTime; - - Game::logger->Log("VehicleRacingControlComponent", - "Best lap time (%llu)", lapTime); - } - - auto* missionComponent = playerEntity->GetComponent(); - - if (missionComponent != nullptr) { - - // Progress lap time tasks - missionComponent->Progress(eMissionTaskType::RACING, (lapTime) * 1000, (LWOOBJID)eRacingTaskParam::LAP_TIME); - - if (player.lap == 3) { - m_Finished++; - player.finished = m_Finished; - - const auto raceTime = - (std::time(nullptr) - m_StartTime); - - player.raceTime = raceTime; - - Game::logger->Log("VehicleRacingControlComponent", - "Completed time %llu, %llu", - raceTime, raceTime * 1000); - - // Entire race time - missionComponent->Progress(eMissionTaskType::RACING, (raceTime) * 1000, (LWOOBJID)eRacingTaskParam::TOTAL_TRACK_TIME); - - auto* characterComponent = playerEntity->GetComponent(); - if (characterComponent != nullptr) { - characterComponent->TrackRaceCompleted(m_Finished == 1); - } - - // TODO: Figure out how to update the GUI leaderboard. - } - } - - Game::logger->Log("VehicleRacingControlComponent", - "Lapped (%i) in (%llu)", player.lap, - lapTime); - } - - Game::logger->Log("VehicleRacingControlComponent", - "Reached point (%i)/(%i)", player.respawnIndex, - path->pathWaypoints.size()); - - break; - } - } -} - -std::string VehicleRacingControlComponent::FormatTimeString(time_t time) { - int32_t min = time / 60; - time -= min * 60; - int32_t sec = time; - - std::string minText; - std::string secText; - - if (min <= 0) { - minText = "0"; - } else { - minText = std::to_string(min); - } - - if (sec <= 0) { - secText = "00"; - } else if (sec <= 9) { - secText = "0" + std::to_string(sec); - } else { - secText = std::to_string(sec); - } - - return minText + ":" + secText + ".00"; -} diff --git a/dGame/dComponents/VehicleRacingControlComponent.h b/dGame/dComponents/VehicleRacingControlComponent.h deleted file mode 100644 index ed6b15f0..00000000 --- a/dGame/dComponents/VehicleRacingControlComponent.h +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Thanks to Simon for his early research on the racing system. - */ - -#pragma once - -#include "BitStream.h" -#include "Entity.h" -#include "RacingControlComponent.h" -#include "eReplicaComponentType.h" - - /** - * Information for each player in the race - */ -struct RacingPlayerInfo { - - /** - * The ID of the player - */ - LWOOBJID playerID; - - /** - * The ID of the car the player is driving - */ - LWOOBJID vehicleID; - - /** - * The index of this player in the list of players - */ - uint32_t playerIndex; - - /** - * Whether the player has finished loading or not - */ - bool playerLoaded; - - /** - * Scripted activity component score - */ - float data[10]{}; - - /** - * Point that the player will respawn at if they smash their car - */ - NiPoint3 respawnPosition; - - /** - * Rotation that the player will respawn at if they smash their car - */ - NiQuaternion respawnRotation; - - /** - * The index in the respawn point the player is now at - */ - uint32_t respawnIndex; - - /** - * The number of laps the player has completed - */ - uint32_t lap; - - /** - * Whether or not the player has finished the race - */ - uint32_t finished; - - /** - * Unused - */ - uint16_t reachedPoints; - - /** - * The fastest lap time of the player - */ - time_t bestLapTime = 0; - - /** - * The current lap time of the player - */ - time_t lapTime = 0; - - /** - * The number of times this player smashed their car - */ - uint32_t smashedTimes = 0; - - /** - * Whether or not the player should be smashed if the game is reloaded - */ - bool noSmashOnReload = false; - - /** - * Whether or not this player has collected their rewards from completing the race - */ - bool collectedRewards = false; - - /** - * Unused - */ - time_t raceTime = 0; -}; - -/** - * Component that's attached to a manager entity in each race zone that loads player vehicles, keep scores, etc. - */ -class VehicleRacingControlComponent : public RacingControlComponent { -public: - inline static const eReplicaComponentType ComponentType = eReplicaComponentType::RACING_CONTROL; - - VehicleRacingControlComponent(Entity* parentEntity, int32_t componentId); - ~VehicleRacingControlComponent(); - - void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags); - void Update(float deltaTime); - - /** - * Invoked when a player loads into the zone. - */ - void OnPlayerLoaded(Entity* player); - - /** - * Initalize the player's vehicle. - * - * @param player The player who's vehicle to initialize. - * @param initialLoad Is this the first time the player is loading in this race? - */ - void LoadPlayerVehicle(Entity* player, uint32_t positionNumber, bool initialLoad = false); - - /** - * Invoked when the client says it has loaded in. - */ - void OnRacingClientReady(Entity* player); - - /** - * Invoked when the client says it should be smashed. - */ - void OnRequestDie(Entity* player); - - /** - * Invoked when the player has finished respawning. - */ - void OnRacingPlayerInfoResetFinished(Entity* player); - - /** - * Invoked when the player responds to the GUI. - */ - void HandleMessageBoxResponse(Entity* player, int32_t button, const std::string& id); - - /** - * Get the racing data from a player's LWOOBJID. - */ - RacingPlayerInfo* GetPlayerData(LWOOBJID playerID); - - /** - * Formats a time to a string, currently unused - * @param time the time to format - * @return the time formatted as string - */ - static std::string FormatTimeString(time_t time); - -private: - - /** - * The players that are currently racing - */ - std::vector m_RacingPlayers; - - /** - * The paths that are followed for the camera scenes - */ - std::u16string m_PathName; - - /** - * The ID of the activity for participating in this race - */ - uint32_t m_ActivityID; - - /** - * The world the players return to when they finish the race - */ - uint32_t m_MainWorld; - - /** - * The number of laps that are remaining for the winning player - */ - uint16_t m_RemainingLaps; - - /** - * The ID of the player that's currently winning the race - */ - LWOOBJID m_LeadingPlayer; - - /** - * The overall best lap from all the players - */ - float m_RaceBestLap; - - /** - * The overall best time from all the players - */ - float m_RaceBestTime; - - /** - * Whether or not the race has started - */ - bool m_Started; - - /** - * The time left until the race will start - */ - float m_StartTimer; - - /** - * The time left for loading the players - */ - float m_LoadTimer; - - /** - * Whether or not all players have loaded - */ - bool m_Loaded; - - /** - * The number of loaded players - */ - uint32_t m_LoadedPlayers; - - /** - * All the players that are in the lobby, loaded or not - */ - std::vector m_LobbyPlayers; - - /** - * The number of players that have finished the race - */ - uint32_t m_Finished; - - /** - * The time the race was started - */ - time_t m_StartTime; - - /** - * Timer for tracking how long a player was alone in this race - */ - float m_EmptyTimer; - - bool m_SoloRacing; - - /** - * Value for message box response to know if we are exiting the race via the activity dialogue - */ - const int32_t m_ActivityExitConfirm = 1; -}; diff --git a/dGame/dEntity/Entity.cpp b/dGame/dEntity/Entity.cpp index 95dad702..b08d95f4 100644 --- a/dGame/dEntity/Entity.cpp +++ b/dGame/dEntity/Entity.cpp @@ -58,7 +58,6 @@ #include "PossessableComponent.h" #include "PossessionComponent.h" #include "ModuleAssemblyComponent.h" -#include "RacingControlComponent.h" #include "SoundTriggerComponent.h" #include "ShootingGalleryComponent.h" #include "RailActivatorComponent.h" @@ -71,10 +70,11 @@ #include "MinigameControlComponent.h" #include "ItemComponent.h" #include "DonationVendorComponent.h" -#include "GateRushControlComponent.h" +#include "GateRushComponent.h" #include "RacingSoundTriggerComponent.h" #include "AchievementVendorComponent.h" #include "MutableModelBehaviorComponent.h" +#include "RacingComponent.h" // Table includes #include "CDComponentsRegistryTable.h" @@ -430,7 +430,7 @@ void Entity::Initialize() { AddComponent(componentId); break; case eReplicaComponentType::RACING_CONTROL: - AddComponent(componentId); + AddComponent(componentId); m_IsGhostingCandidate = false; break; case eReplicaComponentType::MISSION_OFFER: @@ -470,7 +470,7 @@ void Entity::Initialize() { hasProximityMonitorComponent = true; break; case eReplicaComponentType::GATE_RUSH_CONTROL: - AddComponent(componentId); + AddComponent(componentId); break; case eReplicaComponentType::RACING_SOUND_TRIGGER: AddComponent(); diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index a612566a..eab9488d 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -25,7 +25,7 @@ #include "CDClientManager.h" #include "CDSkillBehaviorTable.h" #include "SkillComponent.h" -#include "VehicleRacingControlComponent.h" +#include "RacingComponent.h" #include "RequestServerProjectileImpact.h" #include "SyncSkill.h" #include "StartSkill.h" @@ -126,8 +126,8 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream* inStream, const System std::vector racingControllers = EntityManager::Instance()->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL); for (Entity* racingController : racingControllers) { - auto* vehicleRacingControlComponent = racingController->GetComponent(); - if (vehicleRacingControlComponent) vehicleRacingControlComponent->OnPlayerLoaded(entity); + auto* racingComponent = racingController->GetComponent(); + if (racingComponent) racingComponent->OnPlayerLoaded(entity); } Entity* zoneControl = EntityManager::Instance()->GetZoneControlEntity(); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 4e1a2c65..351df735 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -73,7 +73,7 @@ #include "RenderComponent.h" #include "PossessableComponent.h" #include "PossessionComponent.h" -#include "VehicleRacingControlComponent.h" +#include "RacingComponent.h" #include "RailActivatorComponent.h" #include "LevelProgressionComponent.h" @@ -3889,9 +3889,9 @@ void GameMessages::HandleMessageBoxResponse(RakNet::BitStream* inStream, Entity* scriptedActivityComponent->HandleMessageBoxResponse(userEntity, GeneralUtils::UTF16ToWTF8(identifier)); } - auto* vehicleRacingControlComponent = entity->GetComponent(); + auto* racingComponent = entity->GetComponent(); - if (vehicleRacingControlComponent) vehicleRacingControlComponent->HandleMessageBoxResponse(userEntity, iButton, GeneralUtils::UTF16ToWTF8(identifier)); + if (racingComponent) racingComponent->HandleMessageBoxResponse(userEntity, iButton, GeneralUtils::UTF16ToWTF8(identifier)); for (auto* shootingGallery : EntityManager::Instance()->GetEntitiesByComponent(eReplicaComponentType::SHOOTING_GALLERY)) { shootingGallery->OnMessageBoxResponse(userEntity, iButton, identifier, userData); @@ -4135,11 +4135,11 @@ void GameMessages::HandleRacingClientReady(RakNet::BitStream* inStream, Entity* return; } - auto* vehicleRacingControlComponent = dZoneManager::Instance()->GetZoneControlObject()->GetComponent(); + auto* racingComponent = dZoneManager::Instance()->GetZoneControlObject()->GetComponent(); - if (!vehicleRacingControlComponent) return; + if (!racingComponent) return; - vehicleRacingControlComponent->OnRacingClientReady(player); + racingComponent->OnRacingClientReady(player); } @@ -4183,11 +4183,11 @@ void GameMessages::HandleRequestDie(RakNet::BitStream* inStream, Entity* entity, auto* zoneController = dZoneManager::Instance()->GetZoneControlObject(); - auto* vehicleRacingControlComponent = zoneController->GetComponent(); + auto* racingComponent = zoneController->GetComponent(); Game::logger->Log("HandleRequestDie", "Got die request: %i", entity->GetLOT()); - if (!vehicleRacingControlComponent) return; + if (!racingComponent) return; auto* possessableComponent = entity->GetComponent(); if (possessableComponent) { @@ -4196,7 +4196,7 @@ void GameMessages::HandleRequestDie(RakNet::BitStream* inStream, Entity* entity, if (!entity) return; } - vehicleRacingControlComponent->OnRequestDie(entity); + racingComponent->OnRequestDie(entity); } @@ -4223,11 +4223,11 @@ void GameMessages::HandleRacingPlayerInfoResetFinished(RakNet::BitStream* inStre auto* zoneController = dZoneManager::Instance()->GetZoneControlObject(); - auto* vehicleRacingControlComponent = zoneController->GetComponent(); + auto* racingComponent = zoneController->GetComponent(); Game::logger->Log("HandleRacingPlayerInfoResetFinished", "Got finished: %i", entity->GetLOT()); - if (vehicleRacingControlComponent) vehicleRacingControlComponent->OnRacingPlayerInfoResetFinished(player); + if (racingComponent) racingComponent->OnRacingPlayerInfoResetFinished(player); } void GameMessages::SendUpdateReputation(const LWOOBJID objectId, const int64_t reputation, const SystemAddress& sysAddr) { diff --git a/dScripts/02_server/Map/FV/Racing/RaceMaelstromGeiser.cpp b/dScripts/02_server/Map/FV/Racing/RaceMaelstromGeiser.cpp index f59e1d3d..4fa38647 100644 --- a/dScripts/02_server/Map/FV/Racing/RaceMaelstromGeiser.cpp +++ b/dScripts/02_server/Map/FV/Racing/RaceMaelstromGeiser.cpp @@ -3,7 +3,7 @@ #include "PossessableComponent.h" #include "PossessionComponent.h" #include "EntityManager.h" -#include "VehicleRacingControlComponent.h" +#include "RacingComponent.h" #include "dZoneManager.h" void RaceMaelstromGeiser::OnStartup(Entity* self) { @@ -59,9 +59,9 @@ void RaceMaelstromGeiser::OnProximityUpdate(Entity* self, Entity* entering, std: auto* zoneController = dZoneManager::Instance()->GetZoneControlObject(); - auto* vehicleRacingControlComponent = zoneController->GetComponent(); + auto* racingComponent = zoneController->GetComponent(); - if (vehicleRacingControlComponent) vehicleRacingControlComponent->OnRequestDie(player); + if (racingComponent) racingComponent->OnRequestDie(player); } void RaceMaelstromGeiser::OnTimerDone(Entity* self, std::string timerName) { diff --git a/dScripts/ai/ACT/ActVehicleDeathTrigger.cpp b/dScripts/ai/ACT/ActVehicleDeathTrigger.cpp index 878bae5c..f0c6e36d 100644 --- a/dScripts/ai/ACT/ActVehicleDeathTrigger.cpp +++ b/dScripts/ai/ACT/ActVehicleDeathTrigger.cpp @@ -1,7 +1,7 @@ #include "ActVehicleDeathTrigger.h" #include "PossessableComponent.h" #include "GameMessages.h" -#include "VehicleRacingControlComponent.h" +#include "RacingComponent.h" #include "dZoneManager.h" #include "EntityManager.h" #include "PossessionComponent.h" @@ -44,7 +44,7 @@ void ActVehicleDeathTrigger::OnCollisionPhantom(Entity* self, Entity* target) { auto* zoneController = dZoneManager::Instance()->GetZoneControlObject(); - auto* vehicleRacingControlComponent = zoneController->GetComponent(); + auto* racingComponent = zoneController->GetComponent(); - if (vehicleRacingControlComponent) vehicleRacingControlComponent->OnRequestDie(player); + if (racingComponent) racingComponent->OnRequestDie(player); }