feat(game): introduce GameHandler domain interfaces and eliminate friend declarations

Add game_interfaces.hpp with five narrow domain contracts that GameHandler now
publishes to its domain handlers, replacing the previous friend-class anti-pattern.

Changes:
- include/game/game_interfaces.hpp (new): IConnectionState, ITargetingState,
  IEntityAccess, ISocialState, IPvpState — each interface exposes only the state
  its consumer legitimately needs
- include/game/game_handler.hpp: GameHandler inherits all five interfaces;
  include of game_interfaces.hpp added
- include/game/movement_handler.hpp: remove `friend class GameHandler`; add
  public named accessors for previously-private fields (monsterMovePacketsThisTickRef,
  timeSinceLastMoveHeartbeatRef, resetMovementClock, setFalling, setFallStartMs)
- include/game/spell_handler.hpp: remove `friend class GameHandler/InventoryHandler/
  CombatHandler/EntityController`; promote private packet handlers (handlePetSpells,
  handleListStabledPets, pet stable commands, DBC loaders) to public; add accessor
  methods for aura cache, known spells, and player aura slot mutation
- src/game/game_handler.cpp, game_handler_callbacks.cpp, game_handler_packets.cpp:
  replace direct private field access with the new accessor API
  (e.g. casting_ → isCasting(), monsterMovePacketsThisTick_ → ...ThisTickRef())
- src/game/inventory_handler.cpp, combat_handler.cpp, entity_controller.cpp:
  replace friend-class private access with public accessor calls

No behaviour change. All 13 test suites pass. Zero build warnings.
This commit is contained in:
Paul 2026-04-05 20:25:02 +03:00
parent 34c0e3ca28
commit 65839287b4
10 changed files with 196 additions and 47 deletions

View file

@ -1,5 +1,6 @@
#pragma once
#include "game/game_interfaces.hpp"
#include "game/world_packets.hpp"
#include "game/character.hpp"
#include "game/opcode_table.hpp"
@ -125,7 +126,11 @@ using WorldConnectFailureCallback = std::function<void(const std::string& reason
* - World entry
* - Game packets
*/
class GameHandler {
class GameHandler : public IConnectionState,
public ITargetingState,
public IEntityAccess,
public ISocialState,
public IPvpState {
public:
// Talent data structures (aliased from handler_types.hpp)
using TalentEntry = game::TalentEntry;

View file

@ -0,0 +1,126 @@
#pragma once
// Domain interfaces for GameHandler decomposition (Phase 1.2A).
// Each interface defines a narrow contract for a specific domain concern,
// enabling domain handlers to depend only on the state they need.
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <array>
#include <unordered_map>
namespace wowee::network { class WorldSocket; }
namespace wowee::game {
// Forward declarations
class Entity;
class EntityManager;
enum class WorldState;
struct ContactEntry;
struct BgQueueSlot;
struct AvailableBgInfo;
struct BgScoreboardData;
struct BgPlayerPosition;
struct ArenaTeamStats;
struct ArenaTeamRoster;
struct GuildRosterData;
struct GuildInfoData;
struct GuildQueryResponseData;
struct CreatureQueryResponseData;
struct GameObjectQueryResponseData;
// ---------------------------------------------------------------------------
// IConnectionState — server connection and authentication state
// ---------------------------------------------------------------------------
class IConnectionState {
public:
virtual ~IConnectionState() = default;
virtual bool isConnected() const = 0;
virtual bool isInWorld() const = 0;
virtual WorldState getState() const = 0;
virtual network::WorldSocket* getSocket() = 0;
virtual const std::vector<uint8_t>& getSessionKey() const = 0;
};
// ---------------------------------------------------------------------------
// ITargetingState — target, focus, and mouseover management
// ---------------------------------------------------------------------------
class ITargetingState {
public:
virtual ~ITargetingState() = default;
virtual void setTarget(uint64_t guid) = 0;
virtual void clearTarget() = 0;
virtual uint64_t getTargetGuid() const = 0;
virtual std::shared_ptr<Entity> getTarget() const = 0;
virtual bool hasTarget() const = 0;
virtual void setFocus(uint64_t guid) = 0;
virtual void clearFocus() = 0;
virtual uint64_t getFocusGuid() const = 0;
virtual bool hasFocus() const = 0;
virtual void setMouseoverGuid(uint64_t guid) = 0;
virtual uint64_t getMouseoverGuid() const = 0;
};
// ---------------------------------------------------------------------------
// IEntityAccess — entity queries and name/info caching
// ---------------------------------------------------------------------------
class IEntityAccess {
public:
virtual ~IEntityAccess() = default;
virtual EntityManager& getEntityManager() = 0;
virtual const EntityManager& getEntityManager() const = 0;
virtual void queryPlayerName(uint64_t guid) = 0;
virtual void queryCreatureInfo(uint32_t entry, uint64_t guid) = 0;
virtual std::string getCachedPlayerName(uint64_t guid) const = 0;
virtual std::string getCachedCreatureName(uint32_t entry) const = 0;
virtual const std::unordered_map<uint64_t, std::string>& getPlayerNameCache() const = 0;
virtual const std::unordered_map<uint32_t, CreatureQueryResponseData>& getCreatureInfoCache() const = 0;
virtual const GameObjectQueryResponseData* getCachedGameObjectInfo(uint32_t entry) const = 0;
};
// ---------------------------------------------------------------------------
// ISocialState — friends, ignore list, contacts, guild info
// ---------------------------------------------------------------------------
class ISocialState {
public:
virtual ~ISocialState() = default;
virtual void addFriend(const std::string& playerName, const std::string& note = "") = 0;
virtual void removeFriend(const std::string& playerName) = 0;
virtual void addIgnore(const std::string& playerName) = 0;
virtual void removeIgnore(const std::string& playerName) = 0;
virtual const std::unordered_map<std::string, uint64_t>& getIgnoreCache() const = 0;
virtual const std::vector<ContactEntry>& getContacts() const = 0;
virtual bool isInGuild() const = 0;
virtual const std::string& getGuildName() const = 0;
virtual const GuildRosterData& getGuildRoster() const = 0;
virtual bool hasGuildRoster() const = 0;
};
// ---------------------------------------------------------------------------
// IPvpState — battleground queues, arena teams, scoreboard
// ---------------------------------------------------------------------------
class IPvpState {
public:
virtual ~IPvpState() = default;
virtual bool hasPendingBgInvite() const = 0;
virtual void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF) = 0;
virtual void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF) = 0;
virtual const std::array<BgQueueSlot, 3>& getBgQueues() const = 0;
virtual const std::vector<AvailableBgInfo>& getAvailableBgs() const = 0;
virtual const BgScoreboardData* getBgScoreboard() const = 0;
virtual const std::vector<ArenaTeamStats>& getArenaTeamStats() const = 0;
};
} // namespace wowee::game

View file

@ -148,6 +148,11 @@ public:
uint32_t& monsterMovePacketsThisTickRef() { return monsterMovePacketsThisTick_; }
uint32_t& monsterMovePacketsDroppedThisTickRef() { return monsterMovePacketsDroppedThisTick_; }
// Movement clock / fall state setters (formerly accessed via friend)
void resetMovementClock() { movementClockStart_ = std::chrono::steady_clock::now(); lastMovementTimestampMs_ = 0; }
void setFalling(bool falling) { isFalling_ = falling; }
void setFallStartMs(uint32_t ms) { fallStartMs_ = ms; }
// Taxi state references for GameHandler update/processing
bool& onTaxiFlightRef() { return onTaxiFlight_; }
bool& taxiMountActiveRef() { return taxiMountActive_; }
@ -197,8 +202,6 @@ private:
void buildTaxiCostMap();
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
friend class GameHandler;
GameHandler& owner_;
// --- Movement state ---

View file

@ -80,6 +80,23 @@ public:
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
}
void clearUnitCastStates() { unitCastStates_.clear(); }
void removeUnitCastState(uint64_t guid) { unitCastStates_.erase(guid); }
// Aura cache mutation (formerly accessed via friend)
void clearUnitAurasCache() { unitAurasCache_.clear(); }
void removeUnitAuraCache(uint64_t guid) { unitAurasCache_.erase(guid); }
// Known spells mutation (formerly accessed via friend)
void addKnownSpell(uint32_t spellId) { knownSpells_.insert(spellId); }
bool hasKnownSpell(uint32_t spellId) const { return knownSpells_.count(spellId) > 0; }
// Target aura mutation (formerly accessed via friend)
void clearTargetAuras() { for (auto& slot : targetAuras_) slot = AuraSlot{}; }
// Player aura mutation (formerly accessed via friend)
void resetPlayerAuras(size_t capacity) { playerAuras_.clear(); playerAuras_.resize(capacity); }
AuraSlot& getPlayerAuraSlotRef(size_t slot) { return playerAuras_[slot]; }
std::vector<AuraSlot>& getPlayerAurasMut() { return playerAuras_; }
// Target cast helpers
bool isTargetCasting() const;
@ -204,6 +221,20 @@ public:
// Update per-frame timers (call from GameHandler::update)
void updateTimers(float dt);
// Packet handlers dispatched from GameHandler's opcode table
void handlePetSpells(network::Packet& packet);
void handleListStabledPets(network::Packet& packet);
// Pet stable commands (called via GameHandler delegation)
void requestStabledPetList();
void stablePet(uint8_t slot);
void unstablePet(uint32_t petNumber);
// DBC cache loading (called from GameHandler during login)
void loadSpellNameCache() const;
void loadSkillLineAbilityDbc();
void categorizeTrainerSpells();
private:
// --- Packet handlers ---
void handleInitialSpells(network::Packet& packet);
@ -214,13 +245,6 @@ private:
void handleCooldownEvent(network::Packet& packet);
void handleAuraUpdate(network::Packet& packet, bool isAll);
void handleLearnedSpell(network::Packet& packet);
void handlePetSpells(network::Packet& packet);
void handleListStabledPets(network::Packet& packet);
// Pet stable
void requestStabledPetList();
void stablePet(uint8_t slot);
void unstablePet(uint32_t petNumber);
void handleCastResult(network::Packet& packet);
void handleSpellFailedOther(network::Packet& packet);
@ -252,20 +276,12 @@ private:
// Find the on-use spell for an item (trigger=0 Use or trigger=5 NoDelay).
// CMSG_USE_ITEM requires a valid spellId or the server silently ignores it.
uint32_t findOnUseSpellId(uint32_t itemId) const;
void loadSpellNameCache() const;
void loadSkillLineAbilityDbc();
void categorizeTrainerSpells();
void handleSupercededSpell(network::Packet& packet);
void handleRemovedSpell(network::Packet& packet);
void handleUnlearnSpells(network::Packet& packet);
void handleTalentsInfo(network::Packet& packet);
void handleAchievementEarned(network::Packet& packet);
friend class GameHandler;
friend class InventoryHandler;
friend class CombatHandler;
friend class EntityController;
GameHandler& owner_;
// --- Spell state ---

View file

@ -1065,7 +1065,7 @@ void CombatHandler::setTarget(uint64_t guid) {
// Clear stale aura data from the previous target so the buff bar shows
// an empty state until the server sends SMSG_AURA_UPDATE_ALL for the new target.
if (owner_.getSpellHandler()) for (auto& slot : owner_.getSpellHandler()->targetAuras_) slot = AuraSlot{};
if (owner_.getSpellHandler()) owner_.getSpellHandler()->clearTargetAuras();
// Clear previous target's cast bar on target change
// (the new target's cast state is naturally fetched from spellHandler_->unitCastStates_ by GUID)

View file

@ -449,15 +449,14 @@ void EntityController::syncClassicAurasFromFields(const std::shared_ptr<Entity>&
}
if (!hasAuraField) return;
owner_.getSpellHandler()->playerAuras_.clear();
owner_.getSpellHandler()->playerAuras_.resize(48);
owner_.getSpellHandler()->resetPlayerAuras(48);
uint64_t nowMs = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
for (int slot = 0; slot < 48; ++slot) {
auto it = allFields.find(static_cast<uint16_t>(ufAuras + slot));
if (it != allFields.end() && it->second != 0) {
AuraSlot& a = owner_.getSpellHandler()->playerAuras_[slot];
AuraSlot& a = owner_.getSpellHandler()->getPlayerAuraSlotRef(slot);
a.spellId = it->second;
// Read aura flag byte: packed 4-per-uint32 at ufAuraFlags
uint8_t aFlag = 0;
@ -492,7 +491,7 @@ void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
if (old == 0 && newMountDisplayId != 0) {
// Just mounted — find the mount aura (indefinite duration, self-cast)
owner_.mountAuraSpellIdRef() = 0;
if (owner_.getSpellHandler()) for (const auto& a : owner_.getSpellHandler()->playerAuras_) {
if (owner_.getSpellHandler()) for (const auto& a : owner_.getSpellHandler()->getPlayerAuras()) {
if (!a.isEmpty() && a.maxDurationMs < 0 && a.casterGuid == owner_.getPlayerGuid()) {
owner_.mountAuraSpellIdRef() = a.spellId;
}
@ -518,7 +517,7 @@ void EntityController::detectPlayerMountChange(uint32_t newMountDisplayId,
uint32_t mountSpell = owner_.mountAuraSpellIdRef();
owner_.mountAuraSpellIdRef() = 0;
if (mountSpell != 0 && owner_.getSpellHandler()) {
for (auto& a : owner_.getSpellHandler()->playerAuras_) {
for (auto& a : owner_.getSpellHandler()->getPlayerAurasMut()) {
if (!a.isEmpty() && a.spellId == mountSpell) {
a = AuraSlot{};
break;
@ -1950,9 +1949,9 @@ void EntityController::handleDestroyObject(network::Packet& packet) {
if (owner_.getCombatHandler()) owner_.getCombatHandler()->removeCombatTextForGuid(data.guid);
// Clean up unit cast owner_.getState() (cast bar) for the destroyed unit
if (owner_.getSpellHandler()) owner_.getSpellHandler()->unitCastStates_.erase(data.guid);
if (owner_.getSpellHandler()) owner_.getSpellHandler()->removeUnitCastState(data.guid);
// Clean up cached auras
if (owner_.getSpellHandler()) owner_.getSpellHandler()->unitAurasCache_.erase(data.guid);
if (owner_.getSpellHandler()) owner_.getSpellHandler()->removeUnitAuraCache(data.guid);
owner_.tabCycleStaleRef() = true;
}

View file

@ -272,8 +272,8 @@ void GameHandler::disconnect() {
otherPlayerVisibleItemEntries_.clear();
otherPlayerVisibleDirty_.clear();
otherPlayerMoveTimeMs_.clear();
if (spellHandler_) spellHandler_->unitCastStates_.clear();
if (spellHandler_) spellHandler_->unitAurasCache_.clear();
if (spellHandler_) spellHandler_->clearUnitCastStates();
if (spellHandler_) spellHandler_->clearUnitAurasCache();
if (combatHandler_) combatHandler_->clearCombatText();
entityController_->clearAll();
setState(WorldState::DISCONNECTED);
@ -315,8 +315,8 @@ bool GameHandler::isConnected() const {
void GameHandler::updateNetworking(float deltaTime) {
// Reset per-tick monster-move budget tracking (Classic/Turtle flood protection).
if (movementHandler_) {
movementHandler_->monsterMovePacketsThisTick_ = 0;
movementHandler_->monsterMovePacketsDroppedThisTick_ = 0;
movementHandler_->monsterMovePacketsThisTickRef() = 0;
movementHandler_->monsterMovePacketsDroppedThisTickRef() = 0;
}
// Update socket (processes incoming data and triggers callbacks)
@ -649,7 +649,7 @@ void GameHandler::updateTimers(float deltaTime) {
if (isInWorld()) {
// Avoid sending CMSG_LOOT while a timed cast is active (e.g. gathering).
// handleSpellGo will trigger loot after the cast completes.
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->currentCastSpellId_ != 0) {
if (spellHandler_ && spellHandler_->isCasting() && spellHandler_->getCurrentCastSpellId() != 0) {
it->timer = 0.20f;
++it;
continue;
@ -785,7 +785,7 @@ void GameHandler::update(float deltaTime) {
// Send periodic heartbeat if in world
if (state == WorldState::IN_WORLD) {
timeSinceLastPing += deltaTime;
if (movementHandler_) movementHandler_->timeSinceLastMoveHeartbeat_ += deltaTime;
if (movementHandler_) movementHandler_->timeSinceLastMoveHeartbeatRef() += deltaTime;
const float currentPingInterval =
(isPreWotlk()) ? 10.0f : pingInterval;
@ -823,9 +823,9 @@ void GameHandler::update(float deltaTime) {
: (classicLikeStationaryCombatSync ? 0.75f
: (classicLikeCombatSync ? 0.20f
: moveHeartbeatInterval_));
if (movementHandler_ && movementHandler_->timeSinceLastMoveHeartbeat_ >= heartbeatInterval) {
if (movementHandler_ && movementHandler_->timeSinceLastMoveHeartbeatRef() >= heartbeatInterval) {
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
movementHandler_->timeSinceLastMoveHeartbeat_ = 0.0f;
movementHandler_->timeSinceLastMoveHeartbeatRef() = 0.0f;
}
// Check area triggers (instance portals, tavern rests, etc.)
@ -845,7 +845,7 @@ void GameHandler::update(float deltaTime) {
}
// Check if client-side cast timer expired (tick-down is in SpellHandler::updateTimers).
// Two paths depending on whether this is a GO interaction cast:
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->castTimeRemaining_ <= 0.0f) {
if (spellHandler_ && spellHandler_->isCasting() && spellHandler_->getCastTimeRemaining() <= 0.0f) {
if (pendingGameObjectInteractGuid_ != 0) {
// GO interaction cast: do NOT call resetCastState() here. The server
// sends SMSG_SPELL_GO when the cast completes server-side (~50-200ms

View file

@ -573,13 +573,12 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
movementInfo.flags = 0;
movementInfo.flags2 = 0;
if (movementHandler_) {
movementHandler_->movementClockStart_ = std::chrono::steady_clock::now();
movementHandler_->lastMovementTimestampMs_ = 0;
movementHandler_->resetMovementClock();
}
movementInfo.time = nextMovementTimestampMs();
if (movementHandler_) {
movementHandler_->isFalling_ = false;
movementHandler_->fallStartMs_ = 0;
movementHandler_->setFalling(false);
movementHandler_->setFallStartMs(0);
}
movementInfo.fallTime = 0;
movementInfo.jumpVelocity = 0.0f;
@ -1945,8 +1944,8 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
if (guid == 0) { LOG_WARNING("[GO-DIAG] BLOCKED: guid==0"); return; }
if (!isInWorld()) { LOG_WARNING("[GO-DIAG] BLOCKED: not in world"); return; }
// Do not overlap an actual spell cast.
if (spellHandler_ && spellHandler_->casting_ && spellHandler_->currentCastSpellId_ != 0) {
LOG_WARNING("[GO-DIAG] BLOCKED: already casting spellId=", spellHandler_->currentCastSpellId_);
if (spellHandler_ && spellHandler_->isCasting() && spellHandler_->getCurrentCastSpellId() != 0) {
LOG_WARNING("[GO-DIAG] BLOCKED: already casting spellId=", spellHandler_->getCurrentCastSpellId());
return;
}
// Always clear melee intent before GO interactions.

View file

@ -1019,10 +1019,11 @@ void GameHandler::registerOpcodeHandlers() {
// SMSG_SPELL_COOLDOWN often arrives before SMSG_ACTION_BUTTONS during login,
// so the per-slot cooldownRemaining would be 0 without this sync.
if (spellHandler_) {
const auto& cooldowns = spellHandler_->getSpellCooldowns();
for (auto& slot : actionBar) {
if (slot.type == ActionBarSlot::SPELL && slot.id != 0) {
auto cdIt = spellHandler_->spellCooldowns_.find(slot.id);
if (cdIt != spellHandler_->spellCooldowns_.end() && cdIt->second > 0.0f) {
auto cdIt = cooldowns.find(slot.id);
if (cdIt != cooldowns.end() && cdIt->second > 0.0f) {
slot.cooldownRemaining = cdIt->second;
slot.cooldownTotal = cdIt->second;
}
@ -1033,8 +1034,8 @@ void GameHandler::registerOpcodeHandlers() {
if (qi && qi->valid) {
for (const auto& sp : qi->spells) {
if (sp.spellId == 0) continue;
auto cdIt = spellHandler_->spellCooldowns_.find(sp.spellId);
if (cdIt != spellHandler_->spellCooldowns_.end() && cdIt->second > 0.0f) {
auto cdIt = cooldowns.find(sp.spellId);
if (cdIt != cooldowns.end() && cdIt->second > 0.0f) {
slot.cooldownRemaining = cdIt->second;
slot.cooldownTotal = cdIt->second;
break;

View file

@ -3219,8 +3219,8 @@ void InventoryHandler::emitAllOtherPlayerEquipment() {
void InventoryHandler::handleTrainerBuySucceeded(network::Packet& packet) {
/*uint64_t guid =*/ packet.readUInt64();
uint32_t spellId = packet.readUInt32();
if (owner_.getSpellHandler() && !owner_.getSpellHandler()->knownSpells_.count(spellId)) {
owner_.getSpellHandler()->knownSpells_.insert(spellId);
if (owner_.getSpellHandler() && !owner_.getSpellHandler()->hasKnownSpell(spellId)) {
owner_.getSpellHandler()->addKnownSpell(spellId);
}
const std::string& name = owner_.getSpellName(spellId);
if (!name.empty())