From d599b788d057d21f3ac9047ef27da35c13cfe1a1 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 12:44:51 -0500 Subject: [PATCH] refactor: mission, flag and collectible game messages as structs Converts OfferMission, NotifyMission, NotifyMissionTask, ResetMissions, NotifyClientFlagChange and NotifyLevelRewards (sent with SendToClient: the old functions never broadcast) and the received RespondToMission, MissionDialogueOK, RequestLinkedMission, SetFlag and HasBeenCollected to NetGameMsgs in MissionMessages.{h,cpp}. Callers are switched, OfferMission's send-it-twice behaviour moves to MissionOfferComponent, the received messages are registered in GameMessageHandler's map and the old functions are deleted. Handlers are copied verbatim. The byte-equality helper can now compare SendToClient messages. No wire change and no change in recipients. Verified byte for byte against a frozen verbatim copy of the old functions over an input grid (including OfferMission's two packets), received messages compared with the old handlers' read sequences and every truncated payload rejected, hand computed golden bytes and round trips. Layouts confirmed against the 1.10.64 client in Ghidra. Co-Authored-By: Claude Opus 5.5 --- dGame/Character.cpp | 7 +- .../dComponents/LevelProgressionComponent.cpp | 11 +- dGame/dComponents/MissionComponent.cpp | 6 +- dGame/dComponents/MissionOfferComponent.cpp | 29 +- dGame/dGameMessages/CMakeLists.txt | 1 + dGame/dGameMessages/GameMessageHandler.cpp | 33 +-- dGame/dGameMessages/GameMessages.cpp | 220 -------------- dGame/dGameMessages/GameMessages.h | 12 - dGame/dGameMessages/MissionMessages.cpp | 238 ++++++++++++++++ dGame/dGameMessages/MissionMessages.h | 136 +++++++++ dGame/dMission/Mission.cpp | 8 +- dGame/dMission/MissionTask.cpp | 10 +- .../dGameMessagesTests/CMakeLists.txt | 1 + .../dGameMessagesTests/GameMessageTestUtils.h | 14 +- .../Legacy/MissionMessagesLegacy.h | 185 ++++++++++++ .../MissionMessagesTests.cpp | 268 ++++++++++++++++++ 16 files changed, 904 insertions(+), 275 deletions(-) create mode 100644 dGame/dGameMessages/MissionMessages.cpp create mode 100644 dGame/dGameMessages/MissionMessages.h create mode 100644 tests/dGameTests/dGameMessagesTests/Legacy/MissionMessagesLegacy.h create mode 100644 tests/dGameTests/dGameMessagesTests/MissionMessagesTests.cpp 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)); +}