#include "MissionMessages.h" #include "GameDependencies.h" #include "GameMessageTestUtils.h" #include "Legacy/MissionMessagesLegacy.h" #include "eMissionLockState.h" #include "eMissionState.h" #include "MissionComponent.h" #include "CDClientDatabase.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)); } // Behaviour fix: a MissionDialogueOK naming a responder that does not exist used to dereference a null player. TEST_F(MissionMessagesTests, MissionDialogueOKWithUnknownResponderDoesNotCrash) { Entity missionGiver(15, info); GameMessages::MissionDialogueOK ok; ok.bIsComplete = false; ok.iMissionState = eMissionState::AVAILABLE; ok.missionID = 1727; ok.responder = 0x7777; // no such entity ok.Handle(missionGiver, UNASSIGNED_SYSTEM_ADDRESS); SUCCEED(); } // Packets from 2011/2012 live captures (object ID replaced): the client's SetMissionTypeState (851). Every live // sample left the optional state at its default (NEW) and wrote the subtype before the type. TEST_F(MissionMessagesTests, SetMissionTypeStateMatchesLiveCapture) { const auto location = FromLiveClientCapture( "53040005000000000100000000000010" "5303" "0680000020bb30b73a1023b0b93232b739840000002637b1b0ba34b7b700"); EXPECT_EQ(location.target, 0x1000000000000001LL); EXPECT_EQ(location.state, eMissionLockState::NEW); EXPECT_EQ(location.type, "Location"); EXPECT_EQ(location.subtype, "Avant Gardens"); const auto build = FromLiveClientCapture( "53040005000000000100000000000010" "5303" "0000000002800000213ab4b63200"); EXPECT_EQ(build.type, "Build"); EXPECT_EQ(build.subtype, ""); GameMessages::SetMissionTypeState unlocked; unlocked.state = eMissionLockState::UNLOCKED; unlocked.type = "Battle"; unlocked.subtype = "General"; const auto copy = RoundTrip(unlocked); EXPECT_EQ(copy.state, eMissionLockState::UNLOCKED); EXPECT_EQ(copy.type, "Battle"); EXPECT_EQ(copy.subtype, "General"); ExpectTruncatedFails(unlocked); ExpectTruncatedFails(location); } // The handler records the state on the player's mission component (saved in ). TEST_F(MissionMessagesTests, SetMissionTypeStateIsRecorded) { CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);"); Entity player(16, info); auto* const missionComponent = player.AddComponent(-1); GameMessages::SetMissionTypeState msg; msg.type = "Location"; msg.subtype = "Nimbus Station"; msg.Handle(player, UNASSIGNED_SYSTEM_ADDRESS); ASSERT_EQ(missionComponent->GetMissionTypeStates().at("Location").at("Nimbus Station"), eMissionLockState::NEW); msg.state = eMissionLockState::UNLOCKED; msg.Handle(player, UNASSIGNED_SYSTEM_ADDRESS); EXPECT_EQ(missionComponent->GetMissionTypeStates().at("Location").at("Nimbus Station"), eMissionLockState::UNLOCKED); }