mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-15 00:07:13 +00:00
Jetpack mode is not saved, and the equip skill that turns it on was cast only when the item was put on, so a player still wearing a jetpack lost it in the next world. Live sent SetJetPackMode (use, bypass checks, effect 167, the values of the jetpack's equip skill) shortly after the player's own construction in 12 of 28 world loads of a player with a jetpack, and constructed the player in jetpack mode in the other 16. The jetpack behaviors among the equip skills of the equipped items are run again when the player has loaded. Nothing new is saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
11 KiB
C++
254 lines
11 KiB
C++
#include "GameDependencies.h"
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <ranges>
|
|
|
|
#include "CDClientDatabase.h"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDItemComponentTable.h"
|
|
#include "CDBehaviorTemplateTable.h"
|
|
#include "CDObjectSkillsTable.h"
|
|
#include "CDSkillBehaviorTable.h"
|
|
#include "BehaviorTemplate.h"
|
|
#include "Character.h"
|
|
#include "CharacterComponent.h"
|
|
#include "EffectsMessages.h"
|
|
#include "Entity.h"
|
|
#include "InventoryComponent.h"
|
|
#include "InventoryMessages.h"
|
|
#include "Item.h"
|
|
#include "SkillMessages.h"
|
|
#include "ZoneMessages.h"
|
|
#include "eReplicaComponentType.h"
|
|
|
|
#include "dGameMessagesTests/GameMessageTestUtils.h"
|
|
|
|
// Equipping and unequipping as live did (captures, e.g. equipping a head item over a ninja hood): ChangeObjectWorldState
|
|
// on the item and the equip-<slot> / unequip-<slot> effect on the wearer to everyone; the replaced item's proxies taken
|
|
// away (RemoveItemFromInventory, UnEquipInventory, ChangeObjectWorldState) and its equip skills uncast, to the player.
|
|
|
|
using namespace GameMessageTestUtils;
|
|
|
|
class EquipMessagesTests : public GameDependenciesTest {
|
|
protected:
|
|
static constexpr LOT HOOD = 2641; // hair, with the proxy below and equip skills 362 and 371
|
|
static constexpr LOT HOOD_PROXY = 10482; // clavicle, no equip animation
|
|
static constexpr LOT HAT = 2632; // hair
|
|
static constexpr LOT SWORD = 7000; // special_r
|
|
static constexpr LOT ROCKET = 6416; // Extra_1: no effect
|
|
|
|
std::unique_ptr<Entity> entity;
|
|
std::unique_ptr<Character> character;
|
|
InventoryComponent* inventory{};
|
|
LWOOBJID nextId = 0x1000000000007000LL;
|
|
|
|
void SetUp() override {
|
|
SetUpDependencies();
|
|
// Equipping looks the item's set up in the CDClient; none of these items is in one
|
|
CDClientDatabase::Connect(":memory:");
|
|
CDClientDatabase::ExecuteDML("CREATE TABLE ItemSets (setID INTEGER, itemIDs TEXT);");
|
|
RegisterLot(HOOD, "hair", std::to_string(HOOD_PROXY));
|
|
RegisterLot(HOOD_PROXY, "clavicle", "", true);
|
|
RegisterLot(HAT, "hair");
|
|
RegisterLot(SWORD, "special_r");
|
|
RegisterLot(ROCKET, "Extra_1");
|
|
auto& skills = CDClientManager::GetEntriesMutable<CDObjectSkillsTable>();
|
|
skills.push_back({ HOOD, 362, 1, 0 });
|
|
skills.push_back({ HOOD, 371, 1, 0 });
|
|
skills.push_back({ HOOD_PROXY, 372, 1, 0 });
|
|
|
|
info.lot = 1; // a player
|
|
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
|
entity = std::make_unique<Entity>(0x1000000000000001LL, info);
|
|
inventory = entity->AddComponent<InventoryComponent>(-1); // before the character, so it doesn't load a save
|
|
character = std::make_unique<Character>(1, nullptr);
|
|
entity->SetCharacter(character.get());
|
|
entity->AddComponent<CharacterComponent>(-1, character.get(), ClientAddress())->InitializeStatisticsFromString("");
|
|
}
|
|
|
|
void TearDown() override {
|
|
entity->SetCharacter(nullptr);
|
|
entity.reset();
|
|
character.reset();
|
|
TearDownDependencies();
|
|
}
|
|
|
|
static void RegisterLot(const LOT lot, const std::string& equipLocation, const std::string& subItems = "", const bool noEquipAnimation = false) {
|
|
const auto componentID = static_cast<uint32_t>(96000 + lot);
|
|
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
|
|
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
|
|
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
|
|
CDItemComponent component{};
|
|
component.id = componentID;
|
|
component.equipLocation = equipLocation;
|
|
component.subItems = subItems;
|
|
component.noEquipAnimation = noEquipAnimation;
|
|
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
|
|
}
|
|
|
|
// The proxies (sub-items) equipped with an item
|
|
std::vector<Item*> Proxies(const Item* item) {
|
|
std::vector<Item*> proxies;
|
|
for (auto* candidate : inventory->GetInventory(eInventoryType::ITEM_SETS)->GetItems() | std::views::values) {
|
|
if (candidate->GetParent() == item->GetId()) proxies.push_back(candidate);
|
|
}
|
|
return proxies;
|
|
}
|
|
|
|
Item* Give(const LOT lot) {
|
|
auto* const bag = inventory->GetInventory(eInventoryType::ITEMS);
|
|
return new Item(nextId++, lot, bag, static_cast<uint32_t>(bag->GetItems().size()), 1, false, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
|
|
}
|
|
|
|
// Each game message sent: what it is, about which object, to whom, and the fields the tests look at
|
|
struct Sent {
|
|
MessageType::Game id{};
|
|
LWOOBJID target{};
|
|
bool toEveryone{};
|
|
std::string detail;
|
|
bool operator==(const Sent&) const = default;
|
|
};
|
|
|
|
std::vector<Sent> Messages(const std::vector<CapturedPacket>& packets) const {
|
|
std::vector<Sent> out;
|
|
for (const auto& packet : packets) {
|
|
RakNet::BitStream bitStream(const_cast<uint8_t*>(packet.bytes.data()), packet.bytes.size(), false);
|
|
Sent sent;
|
|
if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, sent.target, sent.id)) continue;
|
|
sent.toEveryone = packet.broadcast && packet.sysAddr == UNASSIGNED_SYSTEM_ADDRESS;
|
|
if (!sent.toEveryone) EXPECT_EQ(packet.sysAddr, ClientAddress());
|
|
switch (sent.id) {
|
|
case MessageType::Game::CHANGE_OBJECT_WORLD_STATE: {
|
|
GameMessages::ChangeObjectWorldState msg;
|
|
EXPECT_TRUE(msg.Deserialize(bitStream));
|
|
sent.detail = msg.newState == eObjectWorldState::ATTACHED ? "ATTACHED" : "INVENTORY";
|
|
break;
|
|
}
|
|
case MessageType::Game::PLAY_FX_EFFECT: {
|
|
GameMessages::PlayFXEffect msg;
|
|
EXPECT_TRUE(msg.Deserialize(bitStream));
|
|
EXPECT_EQ(msg.effectID, -1);
|
|
EXPECT_FLOAT_EQ(msg.priority, 1.07f);
|
|
sent.detail = GeneralUtils::UTF16ToWTF8(msg.effectType);
|
|
break;
|
|
}
|
|
case MessageType::Game::UN_EQUIP_INVENTORY: {
|
|
GameMessages::UnEquipInventory msg;
|
|
EXPECT_TRUE(msg.Deserialize(bitStream));
|
|
EXPECT_TRUE(msg.bIgnoreCooldown);
|
|
sent.detail = std::to_string(msg.itemToUnequip);
|
|
break;
|
|
}
|
|
case MessageType::Game::REMOVE_ITEM_FROM_INVENTORY: {
|
|
GameMessages::RemoveItemFromInventory msg;
|
|
EXPECT_TRUE(msg.Deserialize(bitStream));
|
|
sent.detail = std::to_string(msg.iObjID);
|
|
break;
|
|
}
|
|
case MessageType::Game::UNCAST_SKILL: {
|
|
GameMessages::UncastSkill msg;
|
|
EXPECT_TRUE(msg.Deserialize(bitStream));
|
|
sent.detail = std::to_string(msg.skillID);
|
|
break;
|
|
}
|
|
case MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC:
|
|
break;
|
|
default:
|
|
continue; // not part of equipping
|
|
}
|
|
out.push_back(sent);
|
|
}
|
|
return out;
|
|
}
|
|
};
|
|
|
|
TEST_F(EquipMessagesTests, EquippingAndReplacingAsLiveDid) {
|
|
using enum MessageType::Game;
|
|
const auto player = entity->GetObjectID();
|
|
auto* const hood = Give(HOOD);
|
|
auto* const hat = Give(HAT);
|
|
|
|
// The hood: attached with its effect, then its proxy added and attached (no effect: no equip animation)
|
|
auto sent = Messages(Capture([&] { hood->Equip(); }));
|
|
ASSERT_EQ(sent.size(), 4);
|
|
EXPECT_EQ(sent[0], (Sent{ CHANGE_OBJECT_WORLD_STATE, hood->GetId(), true, "ATTACHED" }));
|
|
EXPECT_EQ(sent[1], (Sent{ PLAY_FX_EFFECT, player, true, "equip-head" }));
|
|
EXPECT_EQ(sent[2].id, ADD_ITEM_TO_INVENTORY_CLIENT_SYNC);
|
|
const auto proxies = Proxies(hood);
|
|
ASSERT_EQ(proxies.size(), 1);
|
|
const auto proxy = proxies[0]->GetId();
|
|
EXPECT_EQ(sent[3], (Sent{ CHANGE_OBJECT_WORLD_STATE, proxy, true, "ATTACHED" }));
|
|
|
|
// The hat replaces it: the hat first, then the hood goes back with its effect, its proxy is taken away and its
|
|
// equip skills are uncast (the proxy's aren't: they were never cast)
|
|
sent = Messages(Capture([&] { hat->Equip(); }));
|
|
const std::vector<Sent> expected = {
|
|
{ CHANGE_OBJECT_WORLD_STATE, hat->GetId(), true, "ATTACHED" },
|
|
{ PLAY_FX_EFFECT, player, true, "equip-head" },
|
|
{ CHANGE_OBJECT_WORLD_STATE, hood->GetId(), true, "INVENTORY" },
|
|
{ PLAY_FX_EFFECT, player, true, "unequip-head" },
|
|
{ REMOVE_ITEM_FROM_INVENTORY, player, false, std::to_string(proxy) },
|
|
{ UN_EQUIP_INVENTORY, player, false, std::to_string(proxy) },
|
|
{ CHANGE_OBJECT_WORLD_STATE, proxy, true, "INVENTORY" },
|
|
{ UNCAST_SKILL, player, false, "362" },
|
|
{ UNCAST_SKILL, player, false, "371" },
|
|
};
|
|
EXPECT_EQ(sent, expected);
|
|
EXPECT_TRUE(Proxies(hood).empty());
|
|
EXPECT_TRUE(hat->IsEquipped());
|
|
EXPECT_FALSE(hood->IsEquipped());
|
|
}
|
|
|
|
TEST_F(EquipMessagesTests, UnequippingAndSlotsWithoutAnEffect) {
|
|
using enum MessageType::Game;
|
|
const auto player = entity->GetObjectID();
|
|
auto* const sword = Give(SWORD);
|
|
auto* const rocket = Give(ROCKET);
|
|
|
|
sword->Equip();
|
|
auto sent = Messages(Capture([&] { sword->UnEquip(); }));
|
|
EXPECT_EQ(sent, (std::vector<Sent>{ { CHANGE_OBJECT_WORLD_STATE, sword->GetId(), true, "INVENTORY" }, { PLAY_FX_EFFECT, player, true, "unequip-right" } }));
|
|
|
|
// Extra_1 (a rocket) has no effect; the world state still goes out
|
|
sent = Messages(Capture([&] { rocket->Equip(true); }));
|
|
EXPECT_EQ(sent, (std::vector<Sent>{ { CHANGE_OBJECT_WORLD_STATE, rocket->GetId(), true, "ATTACHED" } }));
|
|
}
|
|
|
|
// A worn jetpack is found again after a world load, so jetpack mode can be sent again: live sent SetJetPackMode (use,
|
|
// bypass checks, effect 167) about 100 packets after the player's own construction in 12 of 28 world loads of a player
|
|
// with a jetpack, and constructed the player in jetpack mode in the other 16.
|
|
TEST_F(EquipMessagesTests, AWornJetPackIsFoundAfterAWorldLoad) {
|
|
constexpr LOT JETPACK = 1727; // skill 306, behavior 3569 on equip
|
|
RegisterLot(JETPACK, "back");
|
|
CDClientManager::GetEntriesMutable<CDObjectSkillsTable>().push_back({ JETPACK, 306, 1, 0 });
|
|
CDClientManager::GetEntriesMutable<CDObjectSkillsTable>().push_back({ SWORD, 400, 0, 0 }); // a use skill, not an equip one
|
|
auto& skills = CDClientManager::GetEntriesMutable<CDSkillBehaviorTable>();
|
|
CDSkillBehavior jetPackSkill{};
|
|
jetPackSkill.skillID = 306;
|
|
jetPackSkill.behaviorID = 3569;
|
|
skills.insert_or_assign(306, jetPackSkill);
|
|
CDSkillBehavior hoodSkill{};
|
|
hoodSkill.skillID = 362;
|
|
hoodSkill.behaviorID = 4000;
|
|
skills.insert_or_assign(362, hoodSkill);
|
|
auto& templates = CDClientManager::GetEntriesMutable<CDBehaviorTemplateTable>();
|
|
CDBehaviorTemplate jetPackTemplate{};
|
|
jetPackTemplate.behaviorID = 3569;
|
|
jetPackTemplate.templateID = static_cast<uint32_t>(BehaviorTemplate::JETPACK);
|
|
templates.insert_or_assign(3569, jetPackTemplate);
|
|
CDBehaviorTemplate buffTemplate{};
|
|
buffTemplate.behaviorID = 4000;
|
|
buffTemplate.templateID = static_cast<uint32_t>(BehaviorTemplate::BUFF);
|
|
templates.insert_or_assign(4000, buffTemplate);
|
|
|
|
EXPECT_TRUE(inventory->FindEquippedJetPacks().empty());
|
|
// What a save puts back: the equipped items, without their equip skills cast
|
|
auto* const hood = Give(HOOD);
|
|
auto* const jetPack = Give(JETPACK);
|
|
Give(SWORD);
|
|
inventory->UpdateSlot("hair", { hood->GetId(), HOOD, 1, 0 });
|
|
EXPECT_TRUE(inventory->FindEquippedJetPacks().empty());
|
|
inventory->UpdateSlot("back", { jetPack->GetId(), JETPACK, 1, 1 });
|
|
EXPECT_EQ(inventory->FindEquippedJetPacks(), std::vector<uint32_t>{ 3569 });
|
|
}
|