mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
Live filled SetCurrency's source fields per source type (16,816 decoded live SetCurrency): pickups carry the position the client picked the coins up at (the one in its PickupCurrency) and no source; mission, achievement and activity rewards name the player (object and LOT 1); vendor buys, sells and buybacks name the vendor and its LOT; trades carry the trade ID; death, mail and everything else name nothing. No live SetCurrency sets loot_type. DLU wrote a present source LOT of 0, no object and a zero position for all of them. The position is the one the client uses: for pickups it projects it to the screen as the start of the coin counter animation (LWOCharacterComponent::SetCurrency), so it started from the world origin before. PickupCurrency now reads that position (the client always sends it). The trade ID is an 8-byte object ID in the client; DLU declared it 4 bytes, which only never broke because it was never set. Check: pick up coins and watch the coin counter animate from where the coins were; buy and sell at a vendor, finish a mission and trade coins with another player, and the coin total updates each time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
775 lines
34 KiB
C++
775 lines
34 KiB
C++
#include "ActivityMessages.h"
|
|
#include "GameDependencies.h"
|
|
#include "GameMessageTestUtils.h"
|
|
#include "Legacy/RemainingMessagesLegacy.h"
|
|
#include "MovementMessages.h"
|
|
#include "ObjectMessages.h"
|
|
#include "PlayerMessages.h"
|
|
#include "QuickBuildMessages.h"
|
|
#include "InventoryMessages.h"
|
|
#include "SkillMessages.h"
|
|
#include "StatisticID.h"
|
|
#include "ZoneMessages.h"
|
|
|
|
#include <array>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
// Byte equality of the movement, zone, player, object, quickbuild and remaining activity game messages with the
|
|
// frozen hand written functions they replaced (Legacy/RemainingMessagesLegacy.h).
|
|
using namespace GameMessageTestUtils;
|
|
|
|
namespace {
|
|
const std::vector<std::u16string> g_WStrings = { u"", u"ZonePlayer", u"é中", std::u16string(300, u'x') };
|
|
const std::vector<std::string> g_Strings = { "", "base", "\xff\x80z", std::string(300, 'y') };
|
|
const std::vector<LWOOBJID> g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, 0x0102030405060708LL };
|
|
const std::vector<int32_t> g_Ints = { 0, -1, 1, 10, std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::min() };
|
|
const std::vector<float> g_Floats = { 0.0f, 1.0f, 10.0f, 15.0f, -2.5f };
|
|
const std::vector<NiPoint3> g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.0f, 0.0f, -3.5f), NiPoint3(-100.0f, 25.0f, 1e6f) };
|
|
const std::vector<NiQuaternion> g_Rotations = { QuatUtils::IDENTITY, NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f), NiQuaternion(1.0f, 0.0f, 1.0f, 0.0f) };
|
|
|
|
bool Bit(uint32_t mask, int i) { return (mask >> i) & 1; }
|
|
|
|
// Compares every packet (bytes, destination, broadcast flag) the legacy call and the struct send.
|
|
void ExpectSameSends(const std::function<void()>& legacySend, const std::function<void()>& structSend) {
|
|
const auto legacyPackets = Capture(legacySend);
|
|
const auto newPackets = Capture(structSend);
|
|
ASSERT_FALSE(legacyPackets.empty());
|
|
ASSERT_EQ(newPackets.size(), 1);
|
|
for (const auto& legacyPacket : legacyPackets) EXPECT_PACKET_EQ(FromCapture(legacyPacket), FromCapture(newPackets[0]));
|
|
EXPECT_EQ(legacyPackets[0].broadcast, newPackets[0].broadcast);
|
|
EXPECT_EQ(legacyPackets[0].sysAddr, newPackets[0].sysAddr);
|
|
}
|
|
|
|
// The broadcast-only legacy functions compared with Send(UNASSIGNED).
|
|
void ExpectSameAsLegacyBroadcast(const std::function<void()>& legacySend, const GameMessages::NetGameMsg& msg) {
|
|
ExpectSameSends(legacySend, [&] { msg.Send(UNASSIGNED_SYSTEM_ADDRESS); });
|
|
}
|
|
|
|
// Reads msg's payload with a legacy read sequence and checks it consumed every bit.
|
|
template<typename Result>
|
|
Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function<Result(RakNet::BitStream&)>& 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 RemainingMessagesTests : public GameDependenciesTest {
|
|
protected:
|
|
void SetUp() override { SetUpDependencies(); }
|
|
void TearDown() override { TearDownDependencies(); }
|
|
};
|
|
|
|
// ---------------------------------------------------------------- Movement
|
|
|
|
TEST_F(RemainingMessagesTests, TeleportMatchesLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& pos : g_Points) {
|
|
for (const auto& rot : g_Rotations) {
|
|
for (const bool setRotation : { false, true }) {
|
|
GameMessages::Teleport msg(target, pos, rot, setRotation);
|
|
for (const auto& address : g_Addresses) {
|
|
ExpectSameSends([&] { LegacyGameMessages::SendTeleport(target, pos, rot, address, setRotation); }, [&] { msg.SendToClient(address); });
|
|
}
|
|
const auto copy = RoundTrip(msg);
|
|
EXPECT_EQ(copy.pos, pos);
|
|
EXPECT_EQ(copy.bIgnoreY, pos.y == 0.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, StartPathingAndPlatformResyncMatchLegacy) {
|
|
for (const LOT lot : { 999, 12341, 9483, 11306, 6267 }) {
|
|
info.lot = lot;
|
|
Entity entity(0x1000000000000001LL, info);
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendStartPathing(&entity); }, [&] { GameMessages::StartPathing msg; msg.target = entity.GetObjectID(); return msg; }());
|
|
for (uint32_t mask = 0; mask < 4; mask++) {
|
|
for (const auto state : { eMovementPlatformState::Moving, eMovementPlatformState::Stationary, eMovementPlatformState::Stopped }) {
|
|
for (const int32_t index : { 0, 1, 5 }) {
|
|
GameMessages::PlatformResync msg(entity, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1));
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) {
|
|
LegacyGameMessages::SendPlatformResync(&entity, address, Bit(mask, 0), index, index + 1, index + 2, state, Bit(mask, 1));
|
|
}, msg);
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
GameMessages::PlatformResync rotated;
|
|
rotated.qUnexpectedRotation = NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f);
|
|
const auto copy = RoundTrip(rotated);
|
|
EXPECT_EQ(copy.qUnexpectedRotation, rotated.qUnexpectedRotation);
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, SmallMovementMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
Entity entity(target, info);
|
|
for (const float value : g_Floats) {
|
|
for (const bool relative : { false, true }) {
|
|
GameMessages::OrientToAngle orient;
|
|
orient.target = target;
|
|
orient.bRelativeToCurrent = relative;
|
|
orient.fAngle = value;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendOrientToAngle(target, relative, value, address); }, orient);
|
|
RoundTrip(orient);
|
|
}
|
|
GameMessages::SetGravityScale gravity;
|
|
gravity.target = target;
|
|
gravity.scale = value;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendSetGravityScale(target, value, address); }, gravity, SendMode::SendToClient);
|
|
RoundTrip(gravity);
|
|
}
|
|
for (const auto& name : g_Strings) {
|
|
GameMessages::LockNodeRotation lock;
|
|
lock.target = target;
|
|
lock.nodeName = name;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendLockNodeRotation(&entity, name); }, lock);
|
|
EXPECT_EQ(RoundTrip(lock).nodeName, name);
|
|
}
|
|
for (const auto id : g_Ids) {
|
|
GameMessages::SetMountInventoryID mount;
|
|
mount.target = target;
|
|
mount.inventoryMountID = id;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendSetMountInventoryID(&entity, id, UNASSIGNED_SYSTEM_ADDRESS); }, mount);
|
|
RoundTrip(mount);
|
|
}
|
|
for (const auto scheme : { eControlScheme::SCHEME_A, eControlScheme::SCHEME_D, static_cast<eControlScheme>(9) }) {
|
|
GameMessages::SetPlayerControlScheme msg;
|
|
msg.target = target;
|
|
msg.iScheme = scheme;
|
|
ExpectSameSends([&] { LegacyGameMessages::SendSetPlayerControlScheme(&entity, scheme); }, [&] { msg.SendToClient(entity.GetSystemAddress()); });
|
|
RoundTrip(msg);
|
|
}
|
|
for (const auto& pos : g_Points) {
|
|
for (const auto& rot : g_Rotations) {
|
|
GameMessages::PlayerReachedRespawnCheckpoint msg;
|
|
msg.target = target;
|
|
msg.pos = pos;
|
|
msg.rot = rot;
|
|
ExpectSameSends([&] { LegacyGameMessages::SendPlayerReachedRespawnCheckpoint(&entity, pos, rot); }, [&] { msg.SendToClient(entity.GetSystemAddress()); });
|
|
EXPECT_EQ(RoundTrip(msg).rot, rot);
|
|
}
|
|
}
|
|
for (const auto mode : { eCameraTargetCyclingMode::ALLOW_CYCLE_TEAMMATES, eCameraTargetCyclingMode::DISALLOW_CYCLING }) {
|
|
for (const auto id : g_Ids) {
|
|
for (const bool force : { false, true }) {
|
|
GameMessages::ForceCameraTargetCycle msg;
|
|
msg.target = target;
|
|
msg.bForceCycling = force;
|
|
msg.cyclingMode = mode;
|
|
msg.optionalTargetID = id;
|
|
ExpectSameSends([&] { LegacyGameMessages::SendForceCameraTargetCycle(&entity, force, mode, id); }, [&] { msg.SendToClient(entity.GetSystemAddress()); });
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
for (const auto mode : { eCyclingMode::ALLOW_CYCLE_TEAMMATES, eCyclingMode::DISALLOW_CYCLING }) {
|
|
for (const bool deadOnly : { false, true }) {
|
|
GameMessages::PlayerSetCameraCyclingMode msg;
|
|
msg.target = target;
|
|
msg.bAllowCyclingWhileDeadOnly = deadOnly;
|
|
msg.cyclingMode = mode;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendPlayerSetCameraCyclingMode(target, address, deadOnly, mode); }, msg, SendMode::SendToClient);
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, SetJetPackModeMatchesLegacy) {
|
|
Entity entity(0x0102030405060708LL, info);
|
|
for (uint32_t mask = 0; mask < 8; mask++) {
|
|
for (const int32_t effect : { -1, 167 }) {
|
|
for (const float speed : g_Floats) {
|
|
for (const int32_t warning : { -1, 5 }) {
|
|
GameMessages::SetJetPackMode msg;
|
|
msg.target = entity.GetObjectID();
|
|
msg.bUse = Bit(mask, 0);
|
|
msg.bBypassChecks = Bit(mask, 1);
|
|
msg.bDoHover = Bit(mask, 2);
|
|
msg.effectID = effect;
|
|
msg.fAirspeed = speed;
|
|
msg.fMaxAirspeed = speed;
|
|
msg.fVertVel = speed;
|
|
msg.iWarningEffectID = warning;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendSetJetPackMode(&entity, Bit(mask, 0), Bit(mask, 1), Bit(mask, 2), effect, speed, speed, speed, warning); }, msg);
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, RailMovementMatchesLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& path : g_WStrings) {
|
|
for (const uint32_t start : { 0u, 3u }) {
|
|
for (const int32_t component : { -1, 0, 42 }) {
|
|
for (const auto activator : g_Ids) {
|
|
for (uint32_t mask = 0; mask < 2; mask++) {
|
|
GameMessages::SetRailMovement set;
|
|
set.target = target;
|
|
set.pathGoForward = Bit(mask, 0);
|
|
set.pathName = path;
|
|
set.pathStart = start;
|
|
set.railActivatorComponentID = component;
|
|
set.railActivatorObjectID = activator;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendSetRailMovement(target, Bit(mask, 0), path, start, address, component, activator); }, set);
|
|
EXPECT_EQ(RoundTrip(set).pathName, path);
|
|
}
|
|
for (uint32_t mask = 0; mask < 256; mask += 37) {
|
|
GameMessages::StartRailMovement msg;
|
|
msg.target = target;
|
|
msg.pathName = path;
|
|
msg.startSound = u"start";
|
|
msg.loopSound = path;
|
|
msg.stopSound = u"";
|
|
msg.pathStart = start;
|
|
msg.goForward = Bit(mask, 0);
|
|
msg.bDamageImmune = Bit(mask, 1);
|
|
msg.bNoAggro = Bit(mask, 2);
|
|
msg.bNotifyActor = Bit(mask, 3);
|
|
msg.bShowNameBillboard = Bit(mask, 4);
|
|
msg.bCameraLocked = Bit(mask, 5);
|
|
msg.bCollisionEnabled = Bit(mask, 6);
|
|
msg.bUseDB = Bit(mask, 7);
|
|
msg.railActivatorComponentID = component;
|
|
msg.railActivatorObjectID = activator;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) {
|
|
LegacyGameMessages::SendStartRailMovement(target, path, u"start", path, u"", address, start, Bit(mask, 0), Bit(mask, 1), Bit(mask, 2),
|
|
Bit(mask, 3), Bit(mask, 4), Bit(mask, 5), Bit(mask, 6), Bit(mask, 7), component, activator);
|
|
}, msg);
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, InboundMovementMatchesLegacy) {
|
|
for (const auto id : g_Ids) {
|
|
GameMessages::DismountComplete dismount;
|
|
dismount.mountID = id;
|
|
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(dismount, LegacyGameMessages::ReadDismountComplete), id);
|
|
EXPECT_EQ(RoundTrip(dismount).mountID, id);
|
|
ExpectTruncatedFails(dismount);
|
|
|
|
GameMessages::AcknowledgePossession ack;
|
|
ack.possessedObjID = id;
|
|
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(ack, LegacyGameMessages::ReadAcknowledgePossession), id);
|
|
EXPECT_EQ(RoundTrip(ack).possessedObjID, id);
|
|
ExpectTruncatedFails(ack);
|
|
}
|
|
for (const bool value : { false, true }) {
|
|
GameMessages::ToggleGhostReferenceOverride toggle;
|
|
toggle.bOverride = value;
|
|
EXPECT_EQ(ReadWithLegacy<bool>(toggle, LegacyGameMessages::ReadToggleGhostReferenceOverride), value);
|
|
ExpectTruncatedFails(toggle);
|
|
|
|
GameMessages::CancelRailMovement cancel;
|
|
cancel.bImmediate = value;
|
|
EXPECT_EQ(ReadWithLegacy<bool>(cancel, LegacyGameMessages::ReadCancelRailMovement), value);
|
|
ExpectTruncatedFails(cancel);
|
|
}
|
|
for (const auto& pos : g_Points) {
|
|
GameMessages::SetGhostReferencePosition ghost;
|
|
ghost.pos = pos;
|
|
EXPECT_EQ(ReadWithLegacy<NiPoint3>(ghost, LegacyGameMessages::ReadSetGhostReferencePosition), pos);
|
|
ExpectTruncatedFails(ghost);
|
|
}
|
|
for (const auto& path : g_WStrings) {
|
|
for (const int32_t waypoint : g_Ints) {
|
|
GameMessages::PlayerRailArrivedNotification arrived;
|
|
arrived.pathName = path;
|
|
arrived.waypointNumber = waypoint;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyRailArrived>(arrived, LegacyGameMessages::ReadPlayerRailArrivedNotification);
|
|
EXPECT_EQ(legacy.pathName, path);
|
|
EXPECT_EQ(legacy.waypointNumber, waypoint);
|
|
RoundTrip(arrived);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Zone
|
|
|
|
TEST_F(RemainingMessagesTests, ZoneMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
Entity entity(target, info);
|
|
GameMessages::PlayerReady ready;
|
|
ready.target = target;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendPlayerReady(&entity, address); }, ready, SendMode::SendToClient);
|
|
GameMessages::RestoreToPostLoadStats restore;
|
|
restore.target = target;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendRestoreToPostLoadStats(&entity, address); }, restore, SendMode::SendToClient);
|
|
GameMessages::ServerDoneLoadingAllObjects done;
|
|
done.target = target;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendServerDoneLoadingAllObjects(&entity, address); }, done, SendMode::SendToClient);
|
|
|
|
for (const auto& url : g_WStrings) {
|
|
for (uint32_t mask = 0; mask < 4; mask++) {
|
|
GameMessages::InvalidZoneTransferList list;
|
|
list.target = target;
|
|
list.customerFeedbackURL = url;
|
|
list.invalidMapTransferList = u"1000,1001";
|
|
list.bCustomerFeedbackOnExit = Bit(mask, 0);
|
|
list.bCustomerFeedbackOnInvalidMapTransfer = Bit(mask, 1);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendInvalidZoneTransferList(&entity, address, url, u"1000,1001", Bit(mask, 0), Bit(mask, 1)); }, list, SendMode::SendToClient);
|
|
RoundTrip(list);
|
|
}
|
|
GameMessages::LocalizedAnnouncementServerToSingleClient announcement;
|
|
announcement.target = target;
|
|
announcement.body = url;
|
|
announcement.title = u"UI_TITLE";
|
|
ExpectSameSends([&] { LegacyGameMessages::SendLocalizedAnnouncement(&entity, url, u"UI_TITLE"); }, [&] { announcement.SendToClient(entity.GetSystemAddress()); });
|
|
EXPECT_EQ(RoundTrip(announcement).body, url);
|
|
}
|
|
for (const auto sender : g_Ids) {
|
|
for (uint32_t mask = 0; mask < 4; mask++) {
|
|
GameMessages::DisplayZoneSummary summary;
|
|
summary.target = target;
|
|
summary.isPropertyMap = Bit(mask, 0);
|
|
summary.isZoneStart = Bit(mask, 1);
|
|
summary.sender = sender;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendDisplayZoneSummary(target, address, Bit(mask, 0), Bit(mask, 1), sender); }, summary);
|
|
RoundTrip(summary);
|
|
}
|
|
GameMessages::ZoneSummaryDismissed dismissed;
|
|
dismissed.playerID = sender;
|
|
EXPECT_EQ(ReadWithLegacy<LWOOBJID>(dismissed, LegacyGameMessages::ReadZoneSummaryDismissed), sender);
|
|
ExpectTruncatedFails(dismissed);
|
|
}
|
|
for (const auto state : { eObjectWorldState::INWORLD, eObjectWorldState::ATTACHED, eObjectWorldState::INVENTORY }) {
|
|
GameMessages::ChangeObjectWorldState msg;
|
|
msg.target = target;
|
|
msg.newState = state;
|
|
EXPECT_EQ(RoundTrip(msg).newState, state);
|
|
}
|
|
}
|
|
// Name-value text: a null terminator follows non-empty text and is not counted.
|
|
GameMessages::LocalizedAnnouncementServerToSingleClient withParams;
|
|
withParams.bodyParams = u"name=0:Bob";
|
|
withParams.titleParams = u"x=1:2";
|
|
const auto copy = RoundTrip(withParams);
|
|
EXPECT_EQ(copy.bodyParams, withParams.bodyParams);
|
|
EXPECT_EQ(copy.titleParams, withParams.titleParams);
|
|
ExpectTruncatedFails(withParams);
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, ZoneGoldenBytes) {
|
|
GameMessages::DisplayZoneSummary summary;
|
|
summary.target = 0x0102030405060708LL;
|
|
summary.isZoneStart = true;
|
|
summary.sender = 0x1122334455667788LL;
|
|
// header 53 05 00 0c 00 00 00 00, target, msgId 1043 (u16), bits 0 1 1 then the sender
|
|
EXPECT_PACKET_EQ(FromHex("5305000c00000000" "0807060504030201" "1304" "710eeccaa886644220", 211), StructPacket(summary));
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Player
|
|
|
|
TEST_F(RemainingMessagesTests, PlayerMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
Entity entity(target, info);
|
|
for (const auto level : { eGameMasterLevel::CIVILIAN, eGameMasterLevel::DEVELOPER, eGameMasterLevel::OPERATOR }) {
|
|
GameMessages::UpdateChatMode chat;
|
|
chat.target = target;
|
|
chat.level = level;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendChatModeUpdate(target, level); }, chat);
|
|
RoundTrip(chat);
|
|
GameMessages::SetGMLevel gm;
|
|
gm.target = target;
|
|
gm.level = level;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendGMLevelBroadcast(target, level); }, gm);
|
|
RoundTrip(gm);
|
|
}
|
|
for (const int64_t value : { 0LL, -5LL, 123456789012LL }) {
|
|
for (const auto source : { eLootSourceType::NONE, eLootSourceType::MISSION, eLootSourceType::PICKUP }) {
|
|
GameMessages::ModifyLEGOScore score;
|
|
score.target = target;
|
|
score.score = value;
|
|
score.sourceType = source;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendModifyLEGOScore(&entity, address, value, source); }, score, SendMode::SendToClient);
|
|
RoundTrip(score);
|
|
for (const int32_t smallValue : { 0, 7 }) {
|
|
GameMessages::SetCurrency currency;
|
|
currency.target = target;
|
|
currency.currency = value;
|
|
currency.lootType = smallValue;
|
|
currency.sourceID = smallValue;
|
|
currency.sourceLOT = smallValue;
|
|
currency.sourceType = source;
|
|
// The legacy sender wrote a set trade ID as 4 bytes; the client reads 8 (see SetCurrencyTradeIdIsAnObjectId).
|
|
ExpectSameSends([&] { LegacyGameMessages::SendSetCurrency(&entity, value, smallValue, smallValue, smallValue, 0, true, source); }, [&] { currency.SendToClient(entity.GetSystemAddress()); });
|
|
RoundTrip(currency);
|
|
}
|
|
}
|
|
GameMessages::UpdateReputation reputation;
|
|
reputation.target = target;
|
|
reputation.reputation = value;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendUpdateReputation(target, value, address); }, reputation, SendMode::SendToClient);
|
|
RoundTrip(reputation);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, InboundPlayerMessagesMatchLegacy) {
|
|
for (const uint32_t amount : { 0u, 1u, 0xffffffffu }) {
|
|
GameMessages::PickupCurrency pickup;
|
|
pickup.currency = amount;
|
|
pickup.position = NiPoint3(1.5f, -2.0f, 300.25f);
|
|
// The old reader stopped after the amount; the client also sends where it picked the coins up.
|
|
RakNet::BitStream wire;
|
|
pickup.Serialize(wire);
|
|
RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false);
|
|
EXPECT_EQ(LegacyGameMessages::ReadPickupCurrency(legacyStream), amount);
|
|
const auto copy = RoundTrip(pickup);
|
|
EXPECT_EQ(copy.currency, amount);
|
|
EXPECT_EQ(copy.position, pickup.position);
|
|
EXPECT_EQ(wire.GetNumberOfBitsUsed(), 16u * 8u);
|
|
ExpectTruncatedFails(pickup);
|
|
}
|
|
for (const auto& name : g_WStrings) {
|
|
for (const int32_t value : g_Ints) {
|
|
for (const LWOMAPID zone : { LWOMAPID_INVALID, LWOMAPID{ 1100 } }) {
|
|
GameMessages::ModifyPlayerZoneStatistic stat;
|
|
stat.bSet = value > 0;
|
|
stat.statName = name;
|
|
stat.statValue = value;
|
|
stat.zoneID = zone;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyZoneStatistic>(stat, LegacyGameMessages::ReadModifyPlayerZoneStatistic);
|
|
EXPECT_EQ(legacy.set, stat.bSet);
|
|
EXPECT_EQ(legacy.statisticsName, name);
|
|
EXPECT_EQ(legacy.value, value);
|
|
EXPECT_EQ(legacy.zone, zone);
|
|
RoundTrip(stat);
|
|
}
|
|
}
|
|
GameMessages::ParseChatMessage chat;
|
|
chat.iClientState = 3;
|
|
chat.wsString = name;
|
|
const auto legacyChat = ReadWithLegacy<LegacyGameMessages::LegacyParseChatMessage>(chat, LegacyGameMessages::ReadParseChatMessage);
|
|
EXPECT_EQ(legacyChat.iClientState, 3);
|
|
EXPECT_EQ(legacyChat.wsString, name);
|
|
RoundTrip(chat);
|
|
|
|
GameMessages::ReportBug report;
|
|
report.body = name;
|
|
report.clientVersion = "1.10.64";
|
|
report.otherPlayerID = "0";
|
|
report.selection = "selection";
|
|
const auto legacyReport = ReadWithLegacy<LegacyGameMessages::LegacyBugReport>(report, LegacyGameMessages::ReadReportBug);
|
|
std::string narrowed;
|
|
for (const auto character : name) narrowed.push_back(static_cast<char>(character));
|
|
EXPECT_EQ(legacyReport.body, narrowed);
|
|
EXPECT_EQ(legacyReport.clientVersion, "1.10.64");
|
|
EXPECT_EQ(legacyReport.otherPlayer, "0");
|
|
EXPECT_EQ(legacyReport.selection, "selection");
|
|
RoundTrip(report);
|
|
}
|
|
for (const int32_t id : g_Ints) {
|
|
for (const int64_t value : { int64_t{ 1 }, int64_t{ 0 }, int64_t{ -7 }, int64_t{ 1 } << 40 }) {
|
|
GameMessages::UpdatePlayerStatistic stat;
|
|
stat.updateID = id;
|
|
stat.updateValue = value;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyPlayerStatistic>(stat, LegacyGameMessages::ReadUpdatePlayerStatistic);
|
|
EXPECT_EQ(legacy.updateID, id);
|
|
EXPECT_EQ(legacy.updateValue, value);
|
|
ExpectTruncatedFails(stat);
|
|
}
|
|
}
|
|
for (const auto& bytes : g_Strings) {
|
|
for (const uint32_t handle : { 0u, 9u }) {
|
|
GameMessages::VerifyAck ack;
|
|
ack.bDifferent = handle != 0;
|
|
ack.sBitStream = bytes;
|
|
ack.uiHandle = handle;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyVerifyAck>(ack, LegacyGameMessages::ReadVerifyAck);
|
|
EXPECT_EQ(legacy.bDifferent, ack.bDifferent);
|
|
EXPECT_EQ(legacy.sBitStream, bytes);
|
|
EXPECT_EQ(legacy.uiHandle, handle);
|
|
RoundTrip(ack);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Objects and scripts
|
|
|
|
TEST_F(RemainingMessagesTests, NotifyMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
for (const auto& name : g_WStrings) {
|
|
for (const int32_t param : g_Ints) {
|
|
for (const auto& paramStr : g_Strings) {
|
|
GameMessages::NotifyClientObject notify(target, name, param, -param, target, paramStr);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendNotifyClientObject(target, name, param, -param, target, paramStr, address); }, notify);
|
|
EXPECT_EQ(RoundTrip(notify).paramStr, paramStr);
|
|
GameMessages::NotifyClientZoneObject zoneNotify(target, name, param, -param, target, paramStr);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendNotifyClientZoneObject(target, name, param, -param, target, paramStr, address); }, zoneNotify);
|
|
RoundTrip(zoneNotify);
|
|
}
|
|
GameMessages::NotifyObject notifyObject(target, 0x1000000000000001LL, name, param, param / 2);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendNotifyObject(target, 0x1000000000000001LL, name, address, param, param / 2); }, notifyObject);
|
|
RoundTrip(notifyObject);
|
|
GameMessages::NotifyClientFailedPrecondition precondition;
|
|
precondition.target = target;
|
|
precondition.failedReason = name;
|
|
precondition.preconditionID = param;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendNotifyClientFailedPrecondition(target, address, name, param); }, precondition);
|
|
RoundTrip(precondition);
|
|
}
|
|
for (const auto object : g_Ids) {
|
|
// The old function never wrote param1 or param2 (both flags always 0), whatever it was given.
|
|
GameMessages::FireEventClientSide fire(target, name, object, 0x0102030405060708LL);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendFireEventClientSide(target, address, name, object, 5, 6, 0x0102030405060708LL); }, fire, SendMode::SendToClient);
|
|
RoundTrip(fire);
|
|
}
|
|
GameMessages::SetName setName;
|
|
setName.target = target;
|
|
setName.name = name;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendSetName(target, name, address); }, setName);
|
|
RoundTrip(setName);
|
|
}
|
|
Entity entity(target, info);
|
|
for (const auto& data : g_Strings) {
|
|
GameMessages::ScriptNetworkVarUpdate scriptVar;
|
|
scriptVar.target = target;
|
|
scriptVar.tableOfVars = GeneralUtils::ASCIIToUTF16(data);
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendSetNetworkScriptVar(&entity, address, data); }, scriptVar);
|
|
RoundTrip(scriptVar);
|
|
}
|
|
for (const auto type : { eTerminateType::RANGE, eTerminateType::USER, eTerminateType::FROM_INTERACTION }) {
|
|
for (const auto terminator : g_Ids) {
|
|
GameMessages::TerminateInteraction terminate(target, type, terminator);
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendTerminateInteraction(target, type, terminator); }, terminate);
|
|
RoundTrip(terminate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, FireEventServerSideMatchesLegacy) {
|
|
for (const auto& args : g_WStrings) {
|
|
for (const int32_t param : g_Ints) {
|
|
for (const auto sender : g_Ids) {
|
|
GameMessages::FireEventServerSide fire;
|
|
fire.args = args;
|
|
fire.param1 = param;
|
|
fire.param2 = -1;
|
|
fire.param3 = param / 3;
|
|
fire.senderID = sender;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyFireEventServerSide>(fire, LegacyGameMessages::ReadFireEventServerSide);
|
|
EXPECT_EQ(legacy.args, args);
|
|
EXPECT_EQ(legacy.param1, param);
|
|
EXPECT_EQ(legacy.param2, -1);
|
|
EXPECT_EQ(legacy.param3, param / 3);
|
|
EXPECT_EQ(legacy.senderID, sender);
|
|
RoundTrip(fire);
|
|
}
|
|
}
|
|
}
|
|
GameMessages::FireEventServerSide fire;
|
|
fire.args = u"ZonePlayer";
|
|
fire.param3 = 1100;
|
|
ExpectTruncatedFails(fire);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Quickbuild
|
|
|
|
TEST_F(RemainingMessagesTests, QuickBuildMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
Entity entity(target, info);
|
|
for (const auto player : g_Ids) {
|
|
for (const auto state : { eQuickBuildState::OPEN, eQuickBuildState::COMPLETED, eQuickBuildState::BUILDING }) {
|
|
GameMessages::RebuildNotifyState notify;
|
|
notify.target = target;
|
|
notify.prevState = eQuickBuildState::RESETTING;
|
|
notify.state = state;
|
|
notify.player = player;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendQuickBuildNotifyState(&entity, eQuickBuildState::RESETTING, state, player); }, notify);
|
|
RoundTrip(notify);
|
|
}
|
|
for (uint32_t mask = 0; mask < 8; mask++) {
|
|
for (const auto reason : { eQuickBuildFailReason::NOT_GIVEN, eQuickBuildFailReason::CANCELED_EARLY, eQuickBuildFailReason::OUT_OF_IMAGINATION }) {
|
|
for (const float duration : g_Floats) {
|
|
GameMessages::EnableRebuild enable;
|
|
enable.target = target;
|
|
enable.bEnable = Bit(mask, 0);
|
|
enable.bFail = Bit(mask, 1);
|
|
enable.bSuccess = Bit(mask, 2);
|
|
enable.eFailReason = reason;
|
|
enable.fDuration = duration;
|
|
enable.user = player;
|
|
ExpectSameAsLegacyBroadcast([&] { LegacyGameMessages::SendEnableQuickBuild(&entity, Bit(mask, 0), Bit(mask, 1), Bit(mask, 2), reason, duration, player); }, enable);
|
|
RoundTrip(enable);
|
|
}
|
|
}
|
|
}
|
|
for (const bool early : { false, true }) {
|
|
GameMessages::RebuildCancel cancel;
|
|
cancel.bEarlyRelease = early;
|
|
cancel.userID = player;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyQuickBuildCancel>(cancel, LegacyGameMessages::ReadQuickBuildCancel);
|
|
EXPECT_EQ(legacy.bEarlyRelease, early);
|
|
EXPECT_EQ(legacy.userID, player);
|
|
ExpectTruncatedFails(cancel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Activities
|
|
|
|
TEST_F(RemainingMessagesTests, ActivityMessagesMatchLegacy) {
|
|
for (const auto target : g_Targets) {
|
|
Entity entity(target, info);
|
|
for (const int32_t value : g_Ints) {
|
|
GameMessages::MatchResponse response;
|
|
response.target = target;
|
|
response.response = value;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendMatchResponse(&entity, address, value); }, response, SendMode::SendToClient);
|
|
RoundTrip(response);
|
|
}
|
|
for (const auto& data : g_Strings) {
|
|
for (const auto type : { eMatchUpdate::PLAYER_ADDED, eMatchUpdate::PHASE_WAIT_READY }) {
|
|
GameMessages::MatchUpdate update;
|
|
update.target = target;
|
|
update.data = data;
|
|
update.type = type;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendMatchUpdate(&entity, address, data, type); }, update, SendMode::SendToClient);
|
|
EXPECT_EQ(RoundTrip(update).data, data);
|
|
|
|
GameMessages::MatchRequest request;
|
|
request.activator = target;
|
|
request.playerChoices = data;
|
|
request.type = 1;
|
|
request.value = static_cast<int32_t>(type);
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyMatchRequest>(request, LegacyGameMessages::ReadMatchRequest);
|
|
EXPECT_EQ(legacy.activator, target);
|
|
EXPECT_EQ(legacy.playerChoices, data);
|
|
EXPECT_EQ(legacy.type, 1);
|
|
EXPECT_EQ(legacy.value, request.value);
|
|
RoundTrip(request);
|
|
}
|
|
}
|
|
for (const auto& pos : g_Points) {
|
|
for (const int32_t score : { 0, 1500, -1 }) {
|
|
GameMessages::NotifyClientShootingGalleryScore galleryScore;
|
|
galleryScore.target = target;
|
|
galleryScore.addTime = 2.5f;
|
|
galleryScore.score = score;
|
|
galleryScore.targetID = 0x1000000000000001LL;
|
|
galleryScore.targetPos = pos;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendNotifyClientShootingGalleryScore(target, address, 2.5f, score, 0x1000000000000001LL, pos); }, galleryScore);
|
|
RoundTrip(galleryScore);
|
|
}
|
|
GameMessages::UpdateShootingGalleryRotation rotation;
|
|
rotation.angle = 1.25f;
|
|
rotation.facing = pos;
|
|
rotation.muzzlePos = NiPoint3(1.0f, 2.0f, 3.0f);
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyShootingGalleryRotation>(rotation, LegacyGameMessages::ReadUpdateShootingGalleryRotation);
|
|
EXPECT_EQ(legacy.angle, 1.25f);
|
|
EXPECT_EQ(legacy.facing, pos);
|
|
EXPECT_EQ(legacy.muzzlePos, rotation.muzzlePos);
|
|
ExpectTruncatedFails(rotation);
|
|
}
|
|
for (const auto& text : g_WStrings) {
|
|
GameMessages::ActivityStateChangeRequest change;
|
|
change.objectID = target;
|
|
change.value1 = 4;
|
|
change.value2 = -4;
|
|
change.stringValue = text;
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyActivityStateChangeRequest>(change, LegacyGameMessages::ReadActivityStateChangeRequest);
|
|
EXPECT_EQ(legacy.objectID, target);
|
|
EXPECT_EQ(legacy.value1, 4);
|
|
EXPECT_EQ(legacy.value2, -4);
|
|
EXPECT_EQ(legacy.stringValue, text);
|
|
RoundTrip(change);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_F(RemainingMessagesTests, LeaderboardMessagesMatchLegacy) {
|
|
for (const int32_t gameID : { 0, 1864 }) {
|
|
for (const int32_t queryType : { 0, 1, 2 }) {
|
|
for (const int32_t end : { 10, 0 }) {
|
|
for (const int32_t start : { 0, 5 }) {
|
|
for (const bool weekly : { false, true }) {
|
|
GameMessages::RequestActivitySummaryLeaderboardData msg;
|
|
msg.target = 0x0102030405060708LL;
|
|
msg.gameID = gameID;
|
|
msg.queryType = queryType;
|
|
msg.resultsEnd = end;
|
|
msg.resultsStart = start;
|
|
msg.weekly = weekly;
|
|
msg.targetID = 0x0102030405060708LL;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) {
|
|
LegacyGameMessages::SendRequestActivitySummaryLeaderboardData(0x0102030405060708LL, 0x0102030405060708LL, address, gameID, queryType, end, start, weekly);
|
|
}, msg, SendMode::SendToClient);
|
|
const auto legacy = ReadWithLegacy<LegacyGameMessages::LegacyLeaderboardRequest>(msg, LegacyGameMessages::ReadRequestActivitySummaryLeaderboardData);
|
|
EXPECT_EQ(legacy.gameID, gameID);
|
|
EXPECT_EQ(static_cast<int32_t>(legacy.queryType), queryType);
|
|
EXPECT_EQ(legacy.resultsEnd, end);
|
|
EXPECT_EQ(legacy.resultsStart, start);
|
|
EXPECT_EQ(legacy.weekly, weekly);
|
|
RoundTrip(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Leaderboard leaderboard(1864, Leaderboard::InfoType::Top, false, 0x1000000000000001LL, 10, Leaderboard::Type::ShootingGallery);
|
|
GameMessages::SendActivitySummaryLeaderboardData data;
|
|
data.target = 0x1000000000000001LL;
|
|
data.leaderboard = &leaderboard;
|
|
ExpectSameAsLegacy([&](const SystemAddress& address) { LegacyGameMessages::SendActivitySummaryLeaderboardData(0x1000000000000001LL, &leaderboard, address); }, data, SendMode::SendToClient);
|
|
}
|
|
|
|
// Packets from 2011/2012 live captures: live sent UpdatePlayerStatistic (1481) server -> client. The client reads
|
|
// a u32 statistic and an optional i64 amount that defaults to 1 (0x00d8c870 in 1.10.64).
|
|
TEST_F(RemainingMessagesTests, UpdatePlayerStatisticMatchesLiveCapture) {
|
|
// CurrencyCollected after a SetCurrency, amount left at the default.
|
|
const auto currency = FromLiveCapture<GameMessages::UpdatePlayerStatistic>("5305000c000000005e7dea8e00000010c9050100000000");
|
|
EXPECT_EQ(currency.target, 0x100000008eea7d5eLL);
|
|
EXPECT_EQ(currency.updateID, static_cast<int32_t>(StatisticID::CurrencyCollected));
|
|
EXPECT_EQ(currency.updateValue, 1);
|
|
|
|
// CurrencyCollected with an amount.
|
|
const auto coins = FromLiveCapture<GameMessages::UpdatePlayerStatistic>("5305000c000000005e7dea8e00000010c90501000000fa0080000000000000");
|
|
EXPECT_EQ(coins.updateID, static_cast<int32_t>(StatisticID::CurrencyCollected));
|
|
EXPECT_EQ(coins.updateValue, 500);
|
|
|
|
// MetersTraveled, sent periodically while the player moves.
|
|
const auto meters = FromLiveCapture<GameMessages::UpdatePlayerStatistic>("5305000c000000005e7dea8e00000010c9050c0000008e8000000000000000");
|
|
EXPECT_EQ(meters.updateID, static_cast<int32_t>(StatisticID::MetersTraveled));
|
|
EXPECT_EQ(meters.updateValue, 29);
|
|
}
|
|
|
|
// Packets from 2011/2012 live captures, sent when items were equipped and unequipped. DLU wrote ChangeObjectWorldState's
|
|
// state without the flag the client reads first, so the client read ATTACHED as INWORLD.
|
|
TEST_F(RemainingMessagesTests, EquipMessagesMatchLiveCapture) {
|
|
const auto attached = FromLiveCapture<GameMessages::ChangeObjectWorldState>("5305000c000000002d21026701000010c7048080000000");
|
|
EXPECT_EQ(attached.target, 0x100000016702212dLL);
|
|
EXPECT_EQ(attached.newState, eObjectWorldState::ATTACHED);
|
|
const auto inventory = FromLiveCapture<GameMessages::ChangeObjectWorldState>("5305000c00000000b5915b6701000010c7048100000000");
|
|
EXPECT_EQ(inventory.newState, eObjectWorldState::INVENTORY);
|
|
|
|
const auto uncast = FromLiveCapture<GameMessages::UncastSkill>("5305000c000000001f147b5b01000010b6046a010000");
|
|
EXPECT_EQ(uncast.skillID, 362);
|
|
|
|
const auto unequip = FromLiveCapture<GameMessages::UnEquipInventory>("5305000c000000001f147b5b01000010e9004cb607ae4020000200");
|
|
EXPECT_FALSE(unequip.bEvenIfDead);
|
|
EXPECT_TRUE(unequip.bIgnoreCooldown);
|
|
EXPECT_FALSE(unequip.bOutSuccess);
|
|
EXPECT_EQ(unequip.replacementObjectID, LWOOBJID_EMPTY);
|
|
}
|