diff --git a/dGame/Character.cpp b/dGame/Character.cpp index 102db91aa..06cc5dbc1 100644 --- a/dGame/Character.cpp +++ b/dGame/Character.cpp @@ -9,6 +9,7 @@ #include "Entity.h" #include "EntityManager.h" #include "GameMessages.h" +#include "MissionMessages.h" #include "MissionComponent.h" #include "dZoneManager.h" #include "dServer.h" @@ -393,7 +394,11 @@ void Character::SetPlayerFlag(const uint32_t flagId, const bool value) { } } // Notify the client that a flag has changed server-side - GameMessages::SendNotifyClientFlagChange(m_ObjectID, flagId, value, m_ParentUser->GetSystemAddress()); + GameMessages::NotifyClientFlagChange flagChange; + flagChange.target = m_ObjectID; + flagChange.iFlagID = flagId; + flagChange.bFlag = value; + flagChange.SendToClient(m_ParentUser->GetSystemAddress()); } bool Character::GetPlayerFlag(const uint32_t flagId) const { diff --git a/dGame/dComponents/LevelProgressionComponent.cpp b/dGame/dComponents/LevelProgressionComponent.cpp index 8dd1d5f0a..2ef5532ab 100644 --- a/dGame/dComponents/LevelProgressionComponent.cpp +++ b/dGame/dComponents/LevelProgressionComponent.cpp @@ -1,4 +1,5 @@ #include "LevelProgressionComponent.h" +#include "MissionMessages.h" #include "ControllablePhysicsComponent.h" #include "InventoryComponent.h" #include "CharacterComponent.h" @@ -53,8 +54,13 @@ void LevelProgressionComponent::HandleLevelUp() { auto* controllablePhysicsComponent = m_Parent->GetComponent(); if (!inventoryComponent || !controllablePhysicsComponent) return; + GameMessages::NotifyLevelRewards levelRewards; + levelRewards.target = m_Parent->GetObjectID(); + levelRewards.level = m_Level; + // Tell the client we beginning to send level rewards. - if (rewardingItem) GameMessages::NotifyLevelRewards(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), m_Level, rewardingItem); + levelRewards.sendingRewards = rewardingItem; + if (rewardingItem) levelRewards.SendToClient(m_Parent->GetSystemAddress()); for (const auto& reward : rewards) { switch (reward.rewardType) { @@ -79,7 +85,8 @@ void LevelProgressionComponent::HandleLevelUp() { } } // Tell the client we have finished sending level rewards. - if (rewardingItem) GameMessages::NotifyLevelRewards(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), m_Level, !rewardingItem); + levelRewards.sendingRewards = !rewardingItem; + if (rewardingItem) levelRewards.SendToClient(m_Parent->GetSystemAddress()); } void LevelProgressionComponent::SetRetroactiveBaseSpeed(){ diff --git a/dGame/dComponents/MissionComponent.cpp b/dGame/dComponents/MissionComponent.cpp index 7ba24a4a8..8e1baee1f 100644 --- a/dGame/dComponents/MissionComponent.cpp +++ b/dGame/dComponents/MissionComponent.cpp @@ -13,6 +13,7 @@ #include "CDMissionTasksTable.h" #include "InventoryComponent.h" #include "GameMessages.h" +#include "MissionMessages.h" #include "Game.h" #include "Amf3.h" #include "dZoneManager.h" @@ -570,7 +571,10 @@ void MissionComponent::ResetMission(const int32_t missionId) { if (!mission) return; m_Missions.erase(missionId); - GameMessages::SendResetMissions(m_Parent, m_Parent->GetSystemAddress(), missionId); + GameMessages::ResetMissions resetMissions; + resetMissions.target = m_Parent->GetObjectID(); + resetMissions.missionID = missionId; + resetMissions.SendToClient(m_Parent->GetSystemAddress()); } void PushMissions(const std::map& missions, AMFArrayValue& V, bool verbose) { diff --git a/dGame/dComponents/MissionOfferComponent.cpp b/dGame/dComponents/MissionOfferComponent.cpp index 9bc6bc313..7bc3c2932 100644 --- a/dGame/dComponents/MissionOfferComponent.cpp +++ b/dGame/dComponents/MissionOfferComponent.cpp @@ -9,6 +9,7 @@ #include "CDMissionsTable.h" #include "CDMissionNPCComponentTable.h" #include "GameMessages.h" +#include "MissionMessages.h" #include "Entity.h" #include "MissionComponent.h" #include "Logger.h" @@ -54,6 +55,24 @@ MissionOfferComponent::MissionOfferComponent(Entity* parent, const int32_t compo } } +namespace { + // "Why are we sending it twice, once to a non-player object?" + // The first one (targeting the offerer) makes the client zoom into the object. + // The second (targeting the player) actually makes the UI pop up so the mission can be offered. + // Why is it like this? Because LU isn't just a clown, it's the entire circus. + void OfferMissionToPlayer(const Entity& player, const int32_t missionID, const LWOOBJID offerer) { + GameMessages::OfferMission offerMission; + offerMission.missionID = missionID; + offerMission.offerer = offerer; + + offerMission.target = offerer; + offerMission.SendToClient(player.GetSystemAddress()); + + offerMission.target = player.GetObjectID(); + offerMission.SendToClient(player.GetSystemAddress()); + } +} + void MissionOfferComponent::OnUse(Entity* originator) { OfferMissions(originator); } @@ -95,7 +114,7 @@ void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifi // We have the mission, if it is not complete, offer it if (mission->IsActive() || mission->IsReadyToComplete()) { - GameMessages::SendOfferMission(entity->GetObjectID(), entity->GetSystemAddress(), missionId, m_Parent->GetObjectID()); + OfferMissionToPlayer(*entity, missionId, m_Parent->GetObjectID()); continue; } @@ -132,7 +151,7 @@ void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifi const auto& iter = std::find(randomMissionPool.begin(), randomMissionPool.end(), specifiedMissionId); if (iter != randomMissionPool.end() && MissionPrerequisites::CanAccept(specifiedMissionId, missionComponent->GetMissions())) { - GameMessages::SendOfferMission(entity->GetObjectID(), entity->GetSystemAddress(), specifiedMissionId, m_Parent->GetObjectID()); + OfferMissionToPlayer(*entity, specifiedMissionId, m_Parent->GetObjectID()); return; } @@ -152,7 +171,7 @@ void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifi if (mission == nullptr || mission->IsAchievement()) continue; - GameMessages::SendOfferMission(entity->GetObjectID(), entity->GetSystemAddress(), sample, m_Parent->GetObjectID()); + OfferMissionToPlayer(*entity, sample, m_Parent->GetObjectID()); canAcceptPool.clear(); @@ -169,9 +188,9 @@ void MissionOfferComponent::OfferMissions(Entity* entity, const uint32_t specifi const auto selected = canAcceptPool[GeneralUtils::GenerateRandomNumber(0, canAcceptPool.size() - 1)]; - GameMessages::SendOfferMission(entity->GetObjectID(), entity->GetSystemAddress(), selected, m_Parent->GetObjectID()); + OfferMissionToPlayer(*entity, selected, m_Parent->GetObjectID()); } else if (offeredMission.GetOffersMission()) { - GameMessages::SendOfferMission(entity->GetObjectID(), entity->GetSystemAddress(), missionId, m_Parent->GetObjectID()); + OfferMissionToPlayer(*entity, missionId, m_Parent->GetObjectID()); } } } diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index 6ac06cf80..3e6c0ad1c 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -2,6 +2,7 @@ set(DGAME_DGAMEMESSAGES_SOURCES "ActivityMessages.cpp" "GameMessageHandler.cpp" "GameMessages.cpp" + "MissionMessages.cpp" "RacingMessages.cpp" "PropertyDataMessage.cpp" "PropertySelectQueryProperty.cpp") diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 59a3664fd..b77e7f86d 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -32,6 +32,7 @@ #include "EchoSyncSkill.h" #include "ActivityMessages.h" #include "RacingMessages.h" +#include "MissionMessages.h" #include "eMissionTaskType.h" #include "eReplicaComponentType.h" #include "ServiceType.h" @@ -62,6 +63,13 @@ namespace { { NOTIFY_SERVER_VEHICLE_REMOVE_PASSIVE_BOOST_ACTION, []() { return std::make_unique(); } }, { RACING_PLAYER_INFO_RESET_FINISHED, []() { return std::make_unique(); } }, { VEHICLE_NOTIFY_HIT_IMAGINATION_SERVER, []() { return std::make_unique(); } }, + + // Missions, flags, collectibles + { RESPOND_TO_MISSION, []() { return std::make_unique(); } }, + { MISSION_DIALOGUE_OK, []() { return std::make_unique(); } }, + { REQUEST_LINKED_MISSION, []() { return std::make_unique(); } }, + { SET_FLAG, []() { return std::make_unique(); } }, + { HAS_BEEN_COLLECTED, []() { return std::make_unique(); } }, }; }; @@ -137,21 +145,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleUnequipItem(inStream, entity); break; - case MessageType::Game::RESPOND_TO_MISSION: { - GameMessages::HandleRespondToMission(inStream, entity); - break; - } - - case MessageType::Game::SET_FLAG: { - GameMessages::HandleSetFlag(inStream, entity); - break; - } - - case MessageType::Game::HAS_BEEN_COLLECTED: { - GameMessages::HandleHasBeenCollected(inStream, entity); - break; - } - // Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client. case MessageType::Game::SELECT_SKILL: { auto var = entity->GetVar(u"dlu_first_time_load"); @@ -252,16 +245,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; } - case MessageType::Game::REQUEST_LINKED_MISSION: { - GameMessages::HandleRequestLinkedMission(inStream, entity); - break; - } - - case MessageType::Game::MISSION_DIALOGUE_OK: { - GameMessages::HandleMissionDialogOK(inStream, entity); - break; - } - case MessageType::Game::MISSION_DIALOGUE_CANCELLED: { // This message is pointless for our implementation, as the client just carries on after // rejecting a mission offer. We dont need to do anything. This is just here to remove a warning in our logs :) diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 9f57d5555..24baed730 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -351,19 +351,6 @@ void GameMessages::SendStartPathing(Entity* entity) { SEND_PACKET_BROADCAST; } -void GameMessages::SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::RESET_MISSIONS); - - bitStream.Write(missionid != -1); - if (missionid != -1) bitStream.Write(missionid); - - SEND_PACKET; -} - void GameMessages::SendPlatformResync(Entity* entity, const SystemAddress& sysAddr, bool bStopAtDesiredWaypoint, int iIndex, int iDesiredWaypointIndex, int nextIndex, eMovementPlatformState movementState, bool special) { @@ -514,18 +501,6 @@ void GameMessages::SendAddItemToInventoryClientSync(Entity* entity, const System SEND_PACKET; } -void GameMessages::SendNotifyClientFlagChange(const LWOOBJID& objectID, uint32_t iFlagID, bool bFlag, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectID); - bitStream.Write(MessageType::Game::NOTIFY_CLIENT_FLAG_CHANGE); - bitStream.Write(bFlag); - bitStream.Write(iFlagID); - - SEND_PACKET; -} - void GameMessages::SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -538,67 +513,6 @@ void GameMessages::SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectW SEND_PACKET; } -void GameMessages::SendOfferMission(const LWOOBJID& entity, const SystemAddress& sysAddr, int32_t missionID, const LWOOBJID& offererID) { - //You might be wondering. - //"Why are we sending it twice, once to a non-player object? - //Well, the first one (sent to the offerer) makes the client zoom into the object. - //The second, actually makes the UI pop up so you can be offered the mission. - //Why is it like this? Because LU isn't just a clown, it's the entire circus. - - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(offererID); - bitStream.Write(MessageType::Game::OFFER_MISSION); - bitStream.Write(missionID); - bitStream.Write(offererID); - - SEND_PACKET; - - { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity); - bitStream.Write(MessageType::Game::OFFER_MISSION); - bitStream.Write(missionID); - bitStream.Write(offererID); - - SEND_PACKET; - } -} - -void GameMessages::SendNotifyMission(Entity* entity, const SystemAddress& sysAddr, int missionID, int missionState, bool sendingRewards) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::NOTIFY_MISSION); - bitStream.Write(missionID); - bitStream.Write(missionState); - bitStream.Write(sendingRewards); - - SEND_PACKET; -} - -void GameMessages::SendNotifyMissionTask(Entity* entity, const SystemAddress& sysAddr, int missionID, int taskMask, std::vector updates) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::NOTIFY_MISSION_TASK); - - bitStream.Write(missionID); - bitStream.Write(taskMask); - bitStream.Write(updates.size()); - - for (uint32_t i = 0; i < updates.size(); ++i) { - bitStream.Write(updates[i]); - } - - SEND_PACKET; -} - void GameMessages::SendModifyLEGOScore(Entity* entity, const SystemAddress& sysAddr, int64_t score, eLootSourceType sourceType) { CBITSTREAM; CMSGHEADER; @@ -1460,19 +1374,6 @@ void GameMessages::SendRequestActivitySummaryLeaderboardData(const LWOOBJID& obj SEND_PACKET; } -void GameMessages::NotifyLevelRewards(LWOOBJID objectID, const SystemAddress& sysAddr, int level, bool sending_rewards) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectID); - bitStream.Write(MessageType::Game::NOTIFY_LEVEL_REWARDS); - - bitStream.Write(level); - bitStream.Write(sending_rewards); - - SEND_PACKET; -} - void GameMessages::SendSetShootingGalleryParams(LWOOBJID objectId, const SystemAddress& sysAddr, float cameraFOV, float cooldown, @@ -4626,127 +4527,6 @@ void GameMessages::HandleModularBuildConvertModel(RakNet::BitStream& inStream, E item->SetCount(item->GetCount() - 1, false, false, true, eLootSourceType::QUICKBUILD); } -void GameMessages::HandleSetFlag(RakNet::BitStream& inStream, Entity* entity) { - bool bFlag{}; - int32_t iFlagID{}; - - inStream.Read(bFlag); - inStream.Read(iFlagID); - - auto character = entity->GetCharacter(); - if (character) character->SetPlayerFlag(iFlagID, bFlag); - - // This is always set the first time a player loads into a world from character select - // and is used to know when to refresh the players inventory items so they show up. - if (iFlagID == ePlayerFlag::IS_NEWS_SCREEN_VISIBLE && bFlag) { - entity->SetVar(u"dlu_first_time_load", true); - } -} - -void GameMessages::HandleRespondToMission(RakNet::BitStream& inStream, Entity* entity) { - int missionID{}; - LWOOBJID playerID{}; - LWOOBJID receiverID{}; - bool isDefaultReward{}; - LOT reward = LOT_NULL; - - inStream.Read(missionID); - inStream.Read(playerID); - inStream.Read(receiverID); - inStream.Read(isDefaultReward); - if (isDefaultReward) inStream.Read(reward); - - MissionComponent* missionComponent = static_cast(entity->GetComponent(eReplicaComponentType::MISSION)); - if (!missionComponent) { - LOG("Unable to get mission component for entity %llu to handle RespondToMission", playerID); - return; - } - - Mission* mission = missionComponent->GetMission(missionID); - if (mission) { - mission->SetReward(reward); - } else { - LOG("Unable to get mission %i for entity %llu to update reward in RespondToMission", missionID, playerID); - } - - Entity* offerer = Game::entityManager->GetEntity(receiverID); - - if (offerer == nullptr) { - LOG("Unable to get receiver entity %llu for RespondToMission", receiverID); - return; - } - - offerer->GetScript()->OnRespondToMission(offerer, missionID, Game::entityManager->GetEntity(playerID), reward); -} - -void GameMessages::HandleMissionDialogOK(RakNet::BitStream& inStream, Entity* entity) { - bool bIsComplete{}; - eMissionState iMissionState{}; - int missionID{}; - LWOOBJID responder{}; - Entity* player = nullptr; - - inStream.Read(bIsComplete); - inStream.Read(iMissionState); - inStream.Read(missionID); - inStream.Read(responder); - player = Game::entityManager->GetEntity(responder); - - if (entity) entity->GetScript()->OnMissionDialogueOK(entity, player, missionID, iMissionState); - - // Get the player's mission component - MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); - if (!missionComponent) { - LOG("Unable to get mission component for entity %llu to handle MissionDialogueOK", player->GetObjectID()); - return; - } - - if (iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE) { - missionComponent->AcceptMission(missionID); - } else if (iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE) { - missionComponent->CompleteMission(missionID); - } - - if (Game::config->GetValue("allow_players_to_skip_cinematics") != "1" - || !player->GetCharacter() - || !player->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return; - player->AddCallbackTimer(0.5f, [player]() { - if (!player) return; - GameMessages::SendEndCinematic(player->GetObjectID(), u"", player->GetSystemAddress()); - }); -} - -void GameMessages::HandleRequestLinkedMission(RakNet::BitStream& inStream, Entity* entity) { - LWOOBJID playerId{}; - int missionId{}; - bool bMissionOffered{}; - - inStream.Read(playerId); - inStream.Read(missionId); - inStream.Read(bMissionOffered); - - auto* player = Game::entityManager->GetEntity(playerId); - - auto* missionOfferComponent = static_cast(entity->GetComponent(eReplicaComponentType::MISSION_OFFER)); - - if (missionOfferComponent != nullptr) { - missionOfferComponent->OfferMissions(player, 0); - } -} - -void GameMessages::HandleHasBeenCollected(RakNet::BitStream& inStream, Entity* entity) { - LWOOBJID playerID; - inStream.Read(playerID); - - Entity* player = Game::entityManager->GetEntity(playerID); - if (!player || !entity || entity->GetCollectibleID() == 0) return; - - MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); - if (missionComponent) { - missionComponent->Progress(eMissionTaskType::COLLECTION, entity->GetLOT(), entity->GetObjectID()); - } -} - void GameMessages::HandleNotifyServerLevelProcessingComplete(RakNet::BitStream& inStream, Entity* entity) { auto* levelComp = entity->GetComponent(); if (!levelComp) return; diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 9c4aa8a5f..2cf39d4f6 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -161,21 +161,14 @@ namespace GameMessages { int iIndex = 0, int iDesiredWaypointIndex = 1, int nextIndex = 1, eMovementPlatformState movementState = eMovementPlatformState::Moving, bool special = false); - void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid = -1); void SendRestoreToPostLoadStats(Entity* entity, const SystemAddress& sysAddr); void SendServerDoneLoadingAllObjects(Entity* entity, const SystemAddress& sysAddr); void SendGMLevelBroadcast(const LWOOBJID& objectID, eGameMasterLevel level); void SendChatModeUpdate(const LWOOBJID& objectID, eGameMasterLevel level); void SendAddItemToInventoryClientSync(Entity* entity, const SystemAddress& sysAddr, Item* item, const LWOOBJID& objectID, bool showFlyingLoot, int itemCount, LWOOBJID subKey = LWOOBJID_EMPTY, eLootSourceType lootSourceType = eLootSourceType::NONE); - void SendNotifyClientFlagChange(const LWOOBJID& objectID, uint32_t iFlagID, bool bFlag, const SystemAddress& sysAddr); void SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr); - void SendOfferMission(const LWOOBJID& entity, const SystemAddress& sysAddr, int32_t missionID, const LWOOBJID& offererID); - void SendNotifyMission(Entity* entity, const SystemAddress& sysAddr, int missionID, int missionState, bool sendingRewards); - void SendNotifyMissionTask(Entity* entity, const SystemAddress& sysAddr, int missionID, int taskMask, std::vector updates); - void NotifyLevelRewards(LWOOBJID objectID, const SystemAddress& sysAddr, int level, bool sending_rewards); - void SendModifyLEGOScore(Entity* entity, const SystemAddress& sysAddr, int64_t score, eLootSourceType sourceType); void SendUIMessageServerToSingleClient(Entity* entity, const SystemAddress& sysAddr, const std::string& message, AMFBaseValue& args); @@ -646,11 +639,6 @@ namespace GameMessages { void HandleQuickBuildCancel(RakNet::BitStream& inStream, Entity* entity); void HandlePlayEmote(RakNet::BitStream& inStream, Entity* entity); void HandleModularBuildConvertModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleSetFlag(RakNet::BitStream& inStream, Entity* entity); - void HandleRespondToMission(RakNet::BitStream& inStream, Entity* entity); - void HandleMissionDialogOK(RakNet::BitStream& inStream, Entity* entity); - void HandleRequestLinkedMission(RakNet::BitStream& inStream, Entity* entity); - void HandleHasBeenCollected(RakNet::BitStream& inStream, Entity* entity); void HandleNotifyServerLevelProcessingComplete(RakNet::BitStream& inStream, Entity* entity); void HandlePickupCurrency(RakNet::BitStream& inStream, Entity* entity); void HandleRequestDie(RakNet::BitStream& inStream, Entity* entity); diff --git a/dGame/dGameMessages/MissionMessages.cpp b/dGame/dGameMessages/MissionMessages.cpp new file mode 100644 index 000000000..f31791875 --- /dev/null +++ b/dGame/dGameMessages/MissionMessages.cpp @@ -0,0 +1,238 @@ +#include "MissionMessages.h" + +#include "BitStreamUtils.h" +#include "Character.h" +#include "CppScripts.h" +#include "dConfig.h" +#include "eMissionState.h" +#include "eMissionTaskType.h" +#include "ePlayerFlag.h" +#include "eReplicaComponentType.h" +#include "Entity.h" +#include "EntityManager.h" +#include "Game.h" +#include "Logger.h" +#include "Mission.h" +#include "MissionComponent.h" +#include "MissionOfferComponent.h" + +namespace GameMessages { + void OfferMission::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(missionID); + bitStream.Write(offerer); + } + + bool OfferMission::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(offerer)); + return true; + } + + void RespondToMission::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(missionID); + bitStream.Write(playerID); + bitStream.Write(receiver); + BitStreamUtils::WriteOptional(bitStream, rewardItem, LOT_NULL); + } + + bool RespondToMission::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(receiver)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, rewardItem, LOT_NULL)); + return true; + } + + void RespondToMission::Handle(Entity& entity, const SystemAddress& sysAddr) { + const auto reward = rewardItem; + MissionComponent* missionComponent = static_cast(entity.GetComponent(eReplicaComponentType::MISSION)); + if (!missionComponent) { + LOG("Unable to get mission component for entity %llu to handle RespondToMission", playerID); + return; + } + + Mission* mission = missionComponent->GetMission(missionID); + if (mission) { + mission->SetReward(reward); + } else { + LOG("Unable to get mission %i for entity %llu to update reward in RespondToMission", missionID, playerID); + } + + Entity* offerer = Game::entityManager->GetEntity(receiver); + + if (offerer == nullptr) { + LOG("Unable to get receiver entity %llu for RespondToMission", receiver); + return; + } + + offerer->GetScript()->OnRespondToMission(offerer, missionID, Game::entityManager->GetEntity(playerID), reward); + } + + void NotifyMission::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(missionID); + bitStream.Write(missionState); + bitStream.Write(sendingRewards); + } + + bool NotifyMission::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(missionState)); + VALIDATE_READ(bitStream.Read(sendingRewards)); + return true; + } + + void NotifyMissionTask::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(missionID); + bitStream.Write(taskMask); + bitStream.Write(updates.size()); + for (const auto update : updates) bitStream.Write(update); + } + + bool NotifyMissionTask::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(taskMask)); + uint8_t count{}; + VALIDATE_READ(bitStream.Read(count)); + updates.resize(count); + for (auto& update : updates) VALIDATE_READ(bitStream.Read(update)); + return true; + } + + void ResetMissions::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, missionID, -1); + } + + bool ResetMissions::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, missionID, -1)); + return true; + } + + void MissionDialogueOK::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bIsComplete); + bitStream.Write(iMissionState); + bitStream.Write(missionID); + bitStream.Write(responder); + } + + bool MissionDialogueOK::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bIsComplete)); + VALIDATE_READ(bitStream.Read(iMissionState)); + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(responder)); + return true; + } + + void MissionDialogueOK::Handle(Entity& entityRef, const SystemAddress& sysAddr) { + Entity* entity = &entityRef; + Entity* player = Game::entityManager->GetEntity(responder); + + if (entity) entity->GetScript()->OnMissionDialogueOK(entity, player, missionID, iMissionState); + + // Get the player's mission component + MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); + if (!missionComponent) { + LOG("Unable to get mission component for entity %llu to handle MissionDialogueOK", player->GetObjectID()); + return; + } + + if (iMissionState == eMissionState::AVAILABLE || iMissionState == eMissionState::COMPLETE_AVAILABLE) { + missionComponent->AcceptMission(missionID); + } else if (iMissionState == eMissionState::READY_TO_COMPLETE || iMissionState == eMissionState::COMPLETE_READY_TO_COMPLETE) { + missionComponent->CompleteMission(missionID); + } + + if (Game::config->GetValue("allow_players_to_skip_cinematics") != "1" + || !player->GetCharacter() + || !player->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return; + player->AddCallbackTimer(0.5f, [player]() { + if (!player) return; + GameMessages::SendEndCinematic(player->GetObjectID(), u"", player->GetSystemAddress()); + }); + } + + void RequestLinkedMission::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + bitStream.Write(missionID); + bitStream.Write(bMissionOffered); + } + + bool RequestLinkedMission::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + VALIDATE_READ(bitStream.Read(missionID)); + VALIDATE_READ(bitStream.Read(bMissionOffered)); + return true; + } + + void RequestLinkedMission::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* player = Game::entityManager->GetEntity(playerID); + + auto* missionOfferComponent = static_cast(entity.GetComponent(eReplicaComponentType::MISSION_OFFER)); + + if (missionOfferComponent != nullptr) { + missionOfferComponent->OfferMissions(player, 0); + } + } + + void SetFlag::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFlag); + bitStream.Write(iFlagID); + } + + bool SetFlag::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bFlag)); + VALIDATE_READ(bitStream.Read(iFlagID)); + return true; + } + + void SetFlag::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto character = entity.GetCharacter(); + if (character) character->SetPlayerFlag(iFlagID, bFlag); + + // This is always set the first time a player loads into a world from character select + // and is used to know when to refresh the players inventory items so they show up. + if (iFlagID == ePlayerFlag::IS_NEWS_SCREEN_VISIBLE && bFlag) { + entity.SetVar(u"dlu_first_time_load", true); + } + } + + void NotifyClientFlagChange::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFlag); + bitStream.Write(iFlagID); + } + + bool NotifyClientFlagChange::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bFlag)); + VALIDATE_READ(bitStream.Read(iFlagID)); + return true; + } + + void HasBeenCollected::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(playerID); + } + + bool HasBeenCollected::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(playerID)); + return true; + } + + void HasBeenCollected::Handle(Entity& entity, const SystemAddress& sysAddr) { + Entity* player = Game::entityManager->GetEntity(playerID); + if (!player || entity.GetCollectibleID() == 0) return; + + MissionComponent* missionComponent = static_cast(player->GetComponent(eReplicaComponentType::MISSION)); + if (missionComponent) { + missionComponent->Progress(eMissionTaskType::COLLECTION, entity.GetLOT(), entity.GetObjectID()); + } + } + + void NotifyLevelRewards::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(level); + bitStream.Write(sendingRewards); + } + + bool NotifyLevelRewards::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(level)); + VALIDATE_READ(bitStream.Read(sendingRewards)); + return true; + } +} diff --git a/dGame/dGameMessages/MissionMessages.h b/dGame/dGameMessages/MissionMessages.h new file mode 100644 index 000000000..1613bccfa --- /dev/null +++ b/dGame/dGameMessages/MissionMessages.h @@ -0,0 +1,136 @@ +#ifndef MISSIONMESSAGES_H +#define MISSIONMESSAGES_H + +#include "GameMessages.h" + +#include + +enum class eMissionState : int; + +// Game messages for missions, player flags, collectibles and level rewards. +// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +// The server -> client messages here are sent with SendToClient: they were never broadcast. +namespace GameMessages { + // Server -> client. Sent twice by MissionOfferComponent: once targeting the offerer (zooms the camera to it) + // and once targeting the player (opens the offer UI). + struct OfferMission : public NetGameMsg { + OfferMission() : NetGameMsg(MessageType::Game::OFFER_MISSION) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t missionID{}; + LWOOBJID offerer{}; + }; + + // Client -> server. + struct RespondToMission : public NetGameMsg { + RespondToMission() : NetGameMsg(MessageType::Game::RESPOND_TO_MISSION) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t missionID{}; + LWOOBJID playerID{}; + LWOOBJID receiver{}; + LOT rewardItem{ LOT_NULL }; // optional + }; + + // Server -> client. + struct NotifyMission : public NetGameMsg { + NotifyMission() : NetGameMsg(MessageType::Game::NOTIFY_MISSION) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t missionID{}; + int32_t missionState{}; + bool sendingRewards{}; + }; + + // Server -> client. + struct NotifyMissionTask : public NetGameMsg { + NotifyMissionTask() : NetGameMsg(MessageType::Game::NOTIFY_MISSION_TASK) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t missionID{}; + int32_t taskMask{}; + std::vector updates{}; // u8 count, then the floats + }; + + // Server -> client. + struct ResetMissions : public NetGameMsg { + ResetMissions() : NetGameMsg(MessageType::Game::RESET_MISSIONS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t missionID{ -1 }; // optional + }; + + // Client -> server. + struct MissionDialogueOK : public NetGameMsg { + MissionDialogueOK() : NetGameMsg(MessageType::Game::MISSION_DIALOGUE_OK) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bIsComplete{}; + eMissionState iMissionState{}; + int32_t missionID{}; + LWOOBJID responder{}; + }; + + // Client -> server. + struct RequestLinkedMission : public NetGameMsg { + RequestLinkedMission() : NetGameMsg(MessageType::Game::REQUEST_LINKED_MISSION) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID playerID{}; + int32_t missionID{}; + bool bMissionOffered{}; + }; + + // Client -> server. + struct SetFlag : public NetGameMsg { + SetFlag() : NetGameMsg(MessageType::Game::SET_FLAG) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bFlag{}; + int32_t iFlagID{}; + }; + + // Server -> client. + struct NotifyClientFlagChange : public NetGameMsg { + NotifyClientFlagChange() : NetGameMsg(MessageType::Game::NOTIFY_CLIENT_FLAG_CHANGE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bFlag{}; + uint32_t iFlagID{}; + }; + + // Client -> server. Sent when the player collects a collectible. + struct HasBeenCollected : public NetGameMsg { + HasBeenCollected() : NetGameMsg(MessageType::Game::HAS_BEEN_COLLECTED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID playerID{}; + }; + + // Server -> client. + struct NotifyLevelRewards : public NetGameMsg { + NotifyLevelRewards() : NetGameMsg(MessageType::Game::NOTIFY_LEVEL_REWARDS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t level{}; + bool sendingRewards{}; + }; +}; + +#endif // MISSIONMESSAGES_H diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 4afb2207b..9705a4d51 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -10,6 +10,7 @@ #include "EntityManager.h" #include "Game.h" #include "GameMessages.h" +#include "MissionMessages.h" #include "Mail.h" #include "MissionComponent.h" #include "eRacingTaskParam.h" @@ -628,7 +629,12 @@ void Mission::SetMissionState(const eMissionState state, const bool sendingRewar auto* characterComponent = entity->GetComponent(); if (!characterComponent) return; - GameMessages::SendNotifyMission(entity, characterComponent->GetSystemAddress(), info.id, static_cast(state), sendingRewards); + GameMessages::NotifyMission notifyMission; + notifyMission.target = entity->GetObjectID(); + notifyMission.missionID = info.id; + notifyMission.missionState = static_cast(state); + notifyMission.sendingRewards = sendingRewards; + notifyMission.SendToClient(characterComponent->GetSystemAddress()); } void Mission::SetMissionTypeState(eMissionLockState state, const std::string& type, const std::string& subType) { diff --git a/dGame/dMission/MissionTask.cpp b/dGame/dMission/MissionTask.cpp index f23deb6e4..779505357 100644 --- a/dGame/dMission/MissionTask.cpp +++ b/dGame/dMission/MissionTask.cpp @@ -10,6 +10,7 @@ #include "EntityManager.h" #include "ScriptedActivityComponent.h" #include "GameMessages.h" +#include "MissionMessages.h" #include "dZoneManager.h" #include "InventoryComponent.h" #include "MissionComponent.h" @@ -68,9 +69,12 @@ void MissionTask::SetProgress(const uint32_t value, const bool echo) { return; } - std::vector updates; - updates.push_back(static_cast(progress)); - GameMessages::SendNotifyMissionTask(entity, entity->GetSystemAddress(), static_cast(info->id), static_cast(1 << (mask + 1)), updates); + GameMessages::NotifyMissionTask notifyMissionTask; + notifyMissionTask.target = entity->GetObjectID(); + notifyMissionTask.missionID = static_cast(info->id); + notifyMissionTask.taskMask = static_cast(1 << (mask + 1)); + notifyMissionTask.updates.push_back(static_cast(progress)); + notifyMissionTask.SendToClient(entity->GetSystemAddress()); } diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 487e7b038..06ca96810 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -2,6 +2,7 @@ SET(DGAMEMESSAGES_TESTS "ActivityMessagesTests.cpp" "GameMessageTests.cpp" "GameMsgSplitTests.cpp" + "MissionMessagesTests.cpp" "RacingMessagesTests.cpp" "UnSmashTests.cpp" "LegacyGameMessageTests.cpp") diff --git a/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h b/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h index c9c323c61..9cfccfb57 100644 --- a/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h +++ b/tests/dGameTests/dGameMessagesTests/GameMessageTestUtils.h @@ -32,14 +32,18 @@ namespace GameMessageTestUtils { return FromBitStream(bitStream); } - // Sends the same message through the frozen legacy function and through the struct, to one client and as a - // broadcast, and requires identical bytes and the same effective destination. + // How the struct is sent in a comparison: Send (UNASSIGNED broadcasts; legacy functions that did + // "if (UNASSIGNED) SEND_PACKET_BROADCAST; SEND_PACKET;") or SendToClient (legacy functions with only SEND_PACKET). + enum class SendMode { Send, SendToClient }; + + // Sends the same message through the frozen legacy function and through the struct, to one client and to + // UNASSIGNED_SYSTEM_ADDRESS, and requires identical bytes and the same effective destination. // legacySend receives the address to send to. - inline void ExpectSameAsLegacy(const std::function& legacySend, const GameMessages::NetGameMsg& msg) { + inline void ExpectSameAsLegacy(const std::function& legacySend, const GameMessages::NetGameMsg& msg, SendMode mode = SendMode::Send) { for (const auto& address : g_Addresses) { - SCOPED_TRACE(address == UNASSIGNED_SYSTEM_ADDRESS ? "broadcast" : "single client"); + SCOPED_TRACE(address == UNASSIGNED_SYSTEM_ADDRESS ? "unassigned address" : "single client"); const auto legacyPackets = Capture([&] { legacySend(address); }); - const auto newPackets = Capture([&] { msg.Send(address); }); + const auto newPackets = Capture([&] { mode == SendMode::Send ? msg.Send(address) : msg.SendToClient(address); }); ASSERT_FALSE(legacyPackets.empty()); ASSERT_EQ(newPackets.size(), 1); diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/MissionMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/MissionMessagesLegacy.h new file mode 100644 index 000000000..abbe72af9 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/MissionMessagesLegacy.h @@ -0,0 +1,185 @@ +#ifndef MISSIONMESSAGESLEGACY_H +#define MISSIONMESSAGESLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written GameMessages functions that MissionMessages.h replaced +// (dGame/dGameMessages/GameMessages.cpp, branched from origin/main 129199e4). Only the namespace changed. +// The Read* functions are the read sequences of the replaced GameMessages::Handle* functions. + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "Entity.h" +#include "eMissionState.h" +#include "Game.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "ServiceType.h" + +#include + +namespace LegacyGameMessages { + inline void SendResetMissions(Entity* entity, const SystemAddress& sysAddr, const int32_t missionid) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::RESET_MISSIONS); + + bitStream.Write(missionid != -1); + if (missionid != -1) bitStream.Write(missionid); + + SEND_PACKET; + } + + inline void SendNotifyClientFlagChange(const LWOOBJID& objectID, uint32_t iFlagID, bool bFlag, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectID); + bitStream.Write(MessageType::Game::NOTIFY_CLIENT_FLAG_CHANGE); + bitStream.Write(bFlag); + bitStream.Write(iFlagID); + + SEND_PACKET; + } + + inline void SendOfferMission(const LWOOBJID& entity, const SystemAddress& sysAddr, int32_t missionID, const LWOOBJID& offererID) { + //You might be wondering. + //"Why are we sending it twice, once to a non-player object? + //Well, the first one (sent to the offerer) makes the client zoom into the object. + //The second, actually makes the UI pop up so you can be offered the mission. + //Why is it like this? Because LU isn't just a clown, it's the entire circus. + + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(offererID); + bitStream.Write(MessageType::Game::OFFER_MISSION); + bitStream.Write(missionID); + bitStream.Write(offererID); + + SEND_PACKET; + + { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity); + bitStream.Write(MessageType::Game::OFFER_MISSION); + bitStream.Write(missionID); + bitStream.Write(offererID); + + SEND_PACKET; + } + } + + inline void SendNotifyMission(Entity* entity, const SystemAddress& sysAddr, int missionID, int missionState, bool sendingRewards) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::NOTIFY_MISSION); + bitStream.Write(missionID); + bitStream.Write(missionState); + bitStream.Write(sendingRewards); + + SEND_PACKET; + } + + inline void SendNotifyMissionTask(Entity* entity, const SystemAddress& sysAddr, int missionID, int taskMask, std::vector updates) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::NOTIFY_MISSION_TASK); + + bitStream.Write(missionID); + bitStream.Write(taskMask); + bitStream.Write(updates.size()); + + for (uint32_t i = 0; i < updates.size(); ++i) { + bitStream.Write(updates[i]); + } + + SEND_PACKET; + } + + inline void NotifyLevelRewards(LWOOBJID objectID, const SystemAddress& sysAddr, int level, bool sending_rewards) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectID); + bitStream.Write(MessageType::Game::NOTIFY_LEVEL_REWARDS); + + bitStream.Write(level); + bitStream.Write(sending_rewards); + + SEND_PACKET; + } + + // GameMessages::HandleSetFlag + struct LegacySetFlag { bool bFlag{}; int32_t iFlagID{}; }; + inline LegacySetFlag ReadSetFlag(RakNet::BitStream& inStream) { + bool bFlag{}; + int32_t iFlagID{}; + + inStream.Read(bFlag); + inStream.Read(iFlagID); + return { bFlag, iFlagID }; + } + + // GameMessages::HandleRespondToMission + struct LegacyRespondToMission { int missionID{}; LWOOBJID playerID{}; LWOOBJID receiverID{}; LOT reward = LOT_NULL; }; + inline LegacyRespondToMission ReadRespondToMission(RakNet::BitStream& inStream) { + int missionID{}; + LWOOBJID playerID{}; + LWOOBJID receiverID{}; + bool isDefaultReward{}; + LOT reward = LOT_NULL; + + inStream.Read(missionID); + inStream.Read(playerID); + inStream.Read(receiverID); + inStream.Read(isDefaultReward); + if (isDefaultReward) inStream.Read(reward); + return { missionID, playerID, receiverID, reward }; + } + + // GameMessages::HandleMissionDialogOK + struct LegacyMissionDialogOK { bool bIsComplete{}; eMissionState iMissionState{}; int missionID{}; LWOOBJID responder{}; }; + inline LegacyMissionDialogOK ReadMissionDialogOK(RakNet::BitStream& inStream) { + bool bIsComplete{}; + eMissionState iMissionState{}; + int missionID{}; + LWOOBJID responder{}; + + inStream.Read(bIsComplete); + inStream.Read(iMissionState); + inStream.Read(missionID); + inStream.Read(responder); + return { bIsComplete, iMissionState, missionID, responder }; + } + + // GameMessages::HandleRequestLinkedMission + struct LegacyRequestLinkedMission { LWOOBJID playerId{}; int missionId{}; bool bMissionOffered{}; }; + inline LegacyRequestLinkedMission ReadRequestLinkedMission(RakNet::BitStream& inStream) { + LWOOBJID playerId{}; + int missionId{}; + bool bMissionOffered{}; + + inStream.Read(playerId); + inStream.Read(missionId); + inStream.Read(bMissionOffered); + return { playerId, missionId, bMissionOffered }; + } + + // GameMessages::HandleHasBeenCollected + inline LWOOBJID ReadHasBeenCollected(RakNet::BitStream& inStream) { + LWOOBJID playerID; + inStream.Read(playerID); + return playerID; + } +} + +#endif // MISSIONMESSAGESLEGACY_H diff --git a/tests/dGameTests/dGameMessagesTests/MissionMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/MissionMessagesTests.cpp new file mode 100644 index 000000000..f6c591ca9 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/MissionMessagesTests.cpp @@ -0,0 +1,268 @@ +#include "MissionMessages.h" +#include "GameDependencies.h" +#include "GameMessageTestUtils.h" +#include "Legacy/MissionMessagesLegacy.h" + +#include "eMissionState.h" + +#include +#include +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + const std::vector g_Ints = { 0, 1, -1, 1727, std::numeric_limits::max(), std::numeric_limits::min() }; + + PacketBytes Payload(const GameMessages::NetGameMsg& msg) { + RakNet::BitStream bitStream; + msg.Serialize(bitStream); + return FromBitStream(bitStream); + } + + // Serializes msg, then reads it with the legacy read sequence; both must consume exactly the same bits. + template + Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function& read) { + RakNet::BitStream wire; + msg.Serialize(wire); + RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false); + auto result = read(legacyStream); + EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed()); + return result; + } +} + +class MissionMessagesTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +TEST_F(MissionMessagesTests, OfferMissionMatchesLegacy) { + // The legacy function sent the message twice, first targeting the offerer and then the player. + for (const auto player : g_Targets) { + for (const auto offerer : g_Targets) { + for (const auto missionID : g_Ints) { + for (const auto& address : g_Addresses) { + const auto legacy = Capture([&] { LegacyGameMessages::SendOfferMission(player, address, missionID, offerer); }); + const auto ours = Capture([&] { + GameMessages::OfferMission msg; + msg.missionID = missionID; + msg.offerer = offerer; + msg.target = offerer; + msg.SendToClient(address); + msg.target = player; + msg.SendToClient(address); + }); + ASSERT_EQ(legacy.size(), 2); + ASSERT_EQ(ours.size(), 2); + for (size_t i = 0; i < 2; i++) { + EXPECT_PACKET_EQ(FromCapture(legacy[i]), FromCapture(ours[i])); + EXPECT_EQ(legacy[i].broadcast, ours[i].broadcast); + EXPECT_EQ(legacy[i].sysAddr, ours[i].sysAddr); + } + } + GameMessages::OfferMission msg; + msg.missionID = missionID; + msg.offerer = offerer; + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.missionID, missionID); + EXPECT_EQ(copy.offerer, offerer); + } + } + } +} + +TEST_F(MissionMessagesTests, NotifyMissionMatchesLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto missionID : g_Ints) { + for (const int missionState : { 0, 1, 2, 4, 8, 12, -1 }) { + for (const bool sendingRewards : { false, true }) { + GameMessages::NotifyMission msg; + msg.target = target; + msg.missionID = missionID; + msg.missionState = missionState; + msg.sendingRewards = sendingRewards; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyMission(&entity, a, missionID, missionState, sendingRewards); }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.missionID, missionID); + EXPECT_EQ(copy.missionState, missionState); + EXPECT_EQ(copy.sendingRewards, sendingRewards); + } + } + } + } +} + +TEST_F(MissionMessagesTests, NotifyMissionTaskMatchesLegacy) { + std::vector many(255); + for (size_t i = 0; i < many.size(); i++) many[i] = static_cast(i) * 0.5f; + const std::vector> updateSets = { {}, { 1.0f }, { 0.0f, -1.0f, 3.5f }, many }; + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto missionID : g_Ints) { + for (const int taskMask : { 0, 2, 4, 1 << 30 }) { + for (const auto& updates : updateSets) { + GameMessages::NotifyMissionTask msg; + msg.target = target; + msg.missionID = missionID; + msg.taskMask = taskMask; + msg.updates = updates; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyMissionTask(&entity, a, missionID, taskMask, updates); }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.missionID, missionID); + EXPECT_EQ(copy.taskMask, taskMask); + EXPECT_EQ(copy.updates, updates); + } + } + } + } +} + +TEST_F(MissionMessagesTests, ResetMissionsMatchesLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto missionID : g_Ints) { + GameMessages::ResetMissions msg; + msg.target = target; + msg.missionID = missionID; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendResetMissions(&entity, a, missionID); }, msg, SendMode::SendToClient); + EXPECT_EQ(RoundTrip(msg).missionID, missionID); + } + } +} + +TEST_F(MissionMessagesTests, NotifyClientFlagChangeMatchesLegacy) { + for (const auto target : g_Targets) { + for (const uint32_t flagID : { 0u, 42u, 1110u, std::numeric_limits::max() }) { + for (const bool bFlag : { false, true }) { + GameMessages::NotifyClientFlagChange msg; + msg.target = target; + msg.iFlagID = flagID; + msg.bFlag = bFlag; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyClientFlagChange(target, flagID, bFlag, a); }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.iFlagID, flagID); + EXPECT_EQ(copy.bFlag, bFlag); + } + } + } +} + +TEST_F(MissionMessagesTests, NotifyLevelRewardsMatchesLegacy) { + for (const auto target : g_Targets) { + for (const auto level : g_Ints) { + for (const bool sendingRewards : { false, true }) { + GameMessages::NotifyLevelRewards msg; + msg.target = target; + msg.level = level; + msg.sendingRewards = sendingRewards; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::NotifyLevelRewards(target, a, level, sendingRewards); }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.level, level); + EXPECT_EQ(copy.sendingRewards, sendingRewards); + } + } + } +} + +TEST_F(MissionMessagesTests, InboundReadsLikeLegacy) { + for (const auto id : g_Targets) { + for (const auto value : g_Ints) { + for (const bool flag : { false, true }) { + GameMessages::SetFlag setFlag; + setFlag.bFlag = flag; + setFlag.iFlagID = value; + const auto legacySetFlag = ReadWithLegacy(setFlag, LegacyGameMessages::ReadSetFlag); + const auto setFlagCopy = RoundTrip(setFlag); + EXPECT_EQ(setFlagCopy.bFlag, legacySetFlag.bFlag); + EXPECT_EQ(setFlagCopy.iFlagID, legacySetFlag.iFlagID); + ExpectTruncatedFails(setFlag); + + for (const LOT reward : { LOT_NULL, LOT{ 1727 }, LOT{ 0 } }) { + GameMessages::RespondToMission respond; + respond.missionID = value; + respond.playerID = id; + respond.receiver = 0x0102030405060708LL; + respond.rewardItem = reward; + const auto legacyRespond = ReadWithLegacy(respond, LegacyGameMessages::ReadRespondToMission); + const auto respondCopy = RoundTrip(respond); + EXPECT_EQ(respondCopy.missionID, legacyRespond.missionID); + EXPECT_EQ(respondCopy.playerID, legacyRespond.playerID); + EXPECT_EQ(respondCopy.receiver, legacyRespond.receiverID); + EXPECT_EQ(respondCopy.rewardItem, legacyRespond.reward); + ExpectTruncatedFails(respond); + } + + for (const auto state : { eMissionState::AVAILABLE, eMissionState::READY_TO_COMPLETE, eMissionState::COMPLETE_AVAILABLE, static_cast(-1) }) { + GameMessages::MissionDialogueOK ok; + ok.bIsComplete = flag; + ok.iMissionState = state; + ok.missionID = value; + ok.responder = id; + const auto legacyOk = ReadWithLegacy(ok, LegacyGameMessages::ReadMissionDialogOK); + const auto okCopy = RoundTrip(ok); + EXPECT_EQ(okCopy.bIsComplete, legacyOk.bIsComplete); + EXPECT_EQ(okCopy.iMissionState, legacyOk.iMissionState); + EXPECT_EQ(okCopy.missionID, legacyOk.missionID); + EXPECT_EQ(okCopy.responder, legacyOk.responder); + ExpectTruncatedFails(ok); + } + + GameMessages::RequestLinkedMission linked; + linked.playerID = id; + linked.missionID = value; + linked.bMissionOffered = flag; + const auto legacyLinked = ReadWithLegacy(linked, LegacyGameMessages::ReadRequestLinkedMission); + const auto linkedCopy = RoundTrip(linked); + EXPECT_EQ(linkedCopy.playerID, legacyLinked.playerId); + EXPECT_EQ(linkedCopy.missionID, legacyLinked.missionId); + EXPECT_EQ(linkedCopy.bMissionOffered, legacyLinked.bMissionOffered); + ExpectTruncatedFails(linked); + } + } + + GameMessages::HasBeenCollected collected; + collected.playerID = id; + EXPECT_EQ(ReadWithLegacy(collected, LegacyGameMessages::ReadHasBeenCollected), id); + EXPECT_EQ(RoundTrip(collected).playerID, id); + ExpectTruncatedFails(collected); + } +} + +// Independent of the legacy code: hand computed payloads (RakNet writes MSB first). +TEST_F(MissionMessagesTests, GoldenBytes) { + GameMessages::ResetMissions reset; + EXPECT_PACKET_EQ(FromHex("00", 1), Payload(reset)); + reset.missionID = 5; + EXPECT_PACKET_EQ(FromHex("82 80 00 00 00", 33), Payload(reset)); + + GameMessages::RespondToMission respond; + respond.missionID = 7; + respond.playerID = 0x11; + respond.receiver = 0x22; + EXPECT_PACKET_EQ(FromHex("07 00 00 00 11 00 00 00 00 00 00 00 22 00 00 00 00 00 00 00 00", 161), Payload(respond)); + respond.rewardItem = 1727; + EXPECT_PACKET_EQ(FromHex("07 00 00 00 11 00 00 00 00 00 00 00 22 00 00 00 00 00 00 00 df 83 00 00 00", 193), Payload(respond)); + + GameMessages::MissionDialogueOK ok; + ok.bIsComplete = true; + ok.iMissionState = static_cast(2); + ok.missionID = 7; + ok.responder = 0x33; + EXPECT_PACKET_EQ(FromHex("81 00 00 00 03 80 00 00 19 80 00 00 00 00 00 00 00", 129), Payload(ok)); + + GameMessages::NotifyClientFlagChange flag; + flag.bFlag = true; + flag.iFlagID = 42; + EXPECT_PACKET_EQ(FromHex("95 00 00 00 00", 33), Payload(flag)); + + GameMessages::NotifyMissionTask task; + task.missionID = 1; + task.taskMask = 4; + task.updates = { 1.5f, 2.0f }; + EXPECT_PACKET_EQ(FromHex("01 00 00 00 04 00 00 00 02 00 00 c0 3f 00 00 00 40"), Payload(task)); +}