mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(missions): keep and save the mission journal type states
The client sends SetMissionTypeState (851) for the journal's mission types and subtypes (Missions.defined_type / defined_subtype) at load and after mission updates: 650 live packets, every one with the optional state left at NEW, the subtype written before the type. DLU dropped it and never wrote the states, so the client lost them on every load. - SetMissionTypeState message struct and handler: the state is stored on the player's MissionComponent (byte-sized as the client keeps it, LWOMissionComponent::msgSetMissionTypeState 0x00c90d00). - Saved as live wrote it (226 live charxmls): after <cur>, <ts><type v="Build"><st sub="" val="1"/></type>...</ts>, <ts/> when empty. Read back per <type>; old saves without <ts> load with none. The client's reader (0x00d171c0) reads v from <ts> rather than from each <type>, so it files every state under one type; the server keeps the types apart. - Mission::SetMissionTypeState (on accept) records NEW for the mission's type instead of doing nothing. Check in game: accept missions of a few kinds (a location mission, a battle achievement), open the passport/journal, then log out and back in and change zones: the journal tabs keep their "new" markers as before, nothing errors, and the character still loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,30 @@ namespace GameMessageTestUtils {
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Reads a whole client -> server game message packet copied from a live capture (hex; header 53 04 00 05, then the
|
||||
// object ID and the message ID), requires the message ID to be T's and the payload to be consumed up to the byte
|
||||
// padding, and requires the struct to write the same payload back.
|
||||
template<typename T>
|
||||
T FromLiveClientCapture(const std::string& hex) {
|
||||
auto live = FromHex(hex);
|
||||
EXPECT_GE(live.bytes.size(), 18u);
|
||||
EXPECT_EQ(std::vector<uint8_t>(live.bytes.begin(), live.bytes.begin() + 4), (std::vector<uint8_t>{ 0x53, 0x04, 0x00, 0x05 }));
|
||||
RakNet::BitStream bitStream(live.bytes.data(), live.bytes.size(), false);
|
||||
bitStream.IgnoreBytes(8);
|
||||
T msg;
|
||||
MessageType::Game msgId{};
|
||||
EXPECT_TRUE(bitStream.Read(msg.target));
|
||||
EXPECT_TRUE(bitStream.Read(msgId));
|
||||
EXPECT_EQ(msgId, msg.msgId);
|
||||
EXPECT_TRUE(msg.Deserialize(bitStream));
|
||||
EXPECT_LT(bitStream.GetNumberOfUnreadBits(), 8u);
|
||||
RakNet::BitStream again;
|
||||
again.WriteBits(live.bytes.data(), 18 * 8);
|
||||
msg.Serialize(again);
|
||||
EXPECT_EQ(FromBitStream(again).bytes, live.bytes);
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Checks that every strict prefix of msg's serialized payload fails to deserialize.
|
||||
template<typename T>
|
||||
void ExpectTruncatedFails(const T& msg) {
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
#include "GameMessageTestUtils.h"
|
||||
#include "Legacy/MissionMessagesLegacy.h"
|
||||
|
||||
#include "eMissionLockState.h"
|
||||
#include "eMissionState.h"
|
||||
#include "MissionComponent.h"
|
||||
#include "CDClientDatabase.h"
|
||||
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
@@ -278,3 +281,47 @@ TEST_F(MissionMessagesTests, MissionDialogueOKWithUnknownResponderDoesNotCrash)
|
||||
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<GameMessages::SetMissionTypeState>(
|
||||
"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<GameMessages::SetMissionTypeState>(
|
||||
"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 <mis><ts>).
|
||||
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<MissionComponent>(-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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user