Missing followers

This commit is contained in:
luis
2026-09-17 20:01:21 -03:00
parent 6f93adbb2e
commit d9fa1aa73a
2 changed files with 9 additions and 63 deletions
@@ -105,13 +105,6 @@ namespace WorldPackets::Garrison
return data; return data;
} }
// Sniff-verified for 12.0.1.66102 ? observed in the 8316-byte SMSG_GET_GARRISON_INFO_RESULT
// body (SNIFF_AUDIT_12.0.1.66102.md -5.3). int32 ItemFileDataID is always present
// (sometimes zero, sometimes a real DBD ID like 1599042); the trailing OptionalInit bit
// gates an optional ItemInstance blob.
// The brief's Deserialize_JamGarrisonMissionReward @ 0x7FF75C1754C0 (Byte-gated VarUInt32)
// decodes the JAM-mirror code path, NOT this dedicated SMSG. Both formats coexist for
// different sub-collection sync paths.
ByteBuffer& operator<<(ByteBuffer& data, GarrisonMissionReward const& missionRewardItem) ByteBuffer& operator<<(ByteBuffer& data, GarrisonMissionReward const& missionRewardItem)
{ {
data << int32(missionRewardItem.ItemID); data << int32(missionRewardItem.ItemID);
@@ -121,9 +114,7 @@ namespace WorldPackets::Garrison
data << uint32(missionRewardItem.FollowerXP); data << uint32(missionRewardItem.FollowerXP);
data << uint32(missionRewardItem.GarrMssnBonusAbilityID); data << uint32(missionRewardItem.GarrMssnBonusAbilityID);
data << int32(missionRewardItem.ItemFileDataID); data << int32(missionRewardItem.ItemFileDataID);
// itemInstance = OptionalInit bit + FlushBits. This is the SNIFF-CONFIRMED encoding (ADD_MISSION_RESULT
// decoded byte-exact / 0 trailing bytes). The talent socketData is the SAME reflection kind and must match
// this (bit), NOT a uint32 count - see operator<<(GarrisonTalent).
data << OptionalInit(missionRewardItem.ItemInstance); data << OptionalInit(missionRewardItem.ItemInstance);
data.FlushBits(); data.FlushBits();
@@ -133,13 +124,6 @@ namespace WorldPackets::Garrison
return data; return data;
} }
// Sniff-verified for 12.0.1.66102 (SNIFF_AUDIT_12.0.1.66102.md -3.15, embedded in
// the 188-byte SMSG_GET_GARRISON_INFO_RESULT). MissionScalar(float) sits between
// Flags and ContentTuningID followed by 3 size fields. Note: SMSG_GARRISON_START_MISSION_RESULT
// embeds a Mission whose exact byte layout has a residual ~2-byte misalignment vs this
// writer (audit -4.1) ? needs IDA pseudocode of Build_GarrisonStartMissionResult to fully
// resolve. The brief's Deserialize_JamGarrisonMission @ 0x7FF75C1755E0 with Currency/
// BonusActions/AutoMissionData tail is the JAM-mirror path, NOT this dedicated SMSG.
ByteBuffer& operator<<(ByteBuffer& data, GarrisonMission const& mission) ByteBuffer& operator<<(ByteBuffer& data, GarrisonMission const& mission)
{ {
data << uint64(mission.DbID); data << uint64(mission.DbID);
@@ -188,43 +172,16 @@ namespace WorldPackets::Garrison
ByteBuffer& operator<<(ByteBuffer& data, GarrisonTalent const& talent) ByteBuffer& operator<<(ByteBuffer& data, GarrisonTalent const& talent)
{ {
// Wire order (client JamGarrisonTalent, Ghidra-confirmed via ctor @client 0x729093890): garrTalentID(i32),
// garrTalentRank(i32), researchStartTime(i64), flags(i32), socketData(OptionalInit bit + FlushBits).
//
// The client reads the wire `flags` field with its OWN semantics, which differ from TC's internal
// Garrison::Talent::Flags. Bits were pinned by live client testing (the builder that computes these is
// control-flow-flattened, so it isn't offline-decompilable):
// client bit 0x4 = "this talent is being researched" ? gates whether the Order Advancement UI reads
// researchStartTime and renders the research timer. Verified live: flags=0 kept the client's startTime in
// memory (proven by a server-side diag log + a sentinel injection) but drew the talent idle; setting 0x4
// makes C_Garrison.GetTalentInfo(id).isBeingResearched=true with the real DB2 researchDuration.
// client bit 0x2 = a duration override that forces researchDuration to a fixed 24h; MUST NEVER be set.
// client bit 0x1 = "researched / talent taken". Recovered by static RE of the 12.0.7 client, which also
// proves it is PURELY server-supplied: the complete set of writers to the client-side talent record's
// flags word (+0x40) is three sites - RVA 0x22992A5 and 0x2299395, both verbatim copies of the wire
// struct inside Garrison::SetTalent (RVA 0x2298FF0), and RVA 0x2299448 in Garrison::RemoveTalent,
// which only zeroes it. There is no or/bts against that field anywhere in the image and no
// research-timer expiry that recomputes it, so the client can never derive "researched" on its own.
// Readers gated on the bit: ModifierTree 201/202/317 (evaluator RVA 0x20664B0 - the covenant/taxi
// PlayerCondition path), C_Garrison.GetTalentInfo's `researched` (builder RVA 0x22B95F0 via
// 0x228E2F0), GetTalentAvailability (RVA 0x2171390, bit set => availability 2) and
// CanResearchAtThisTier (RVA 0x2298DF0, which early-outs "already taken" only when the bit is set).
// Leaving it clear is why a fully researched sanctum/order-hall talent rendered as un-researched and
// why talent-gated unlocks (e.g. the covenant transport-network taxi nodes) never opened.
// TC's internal flags (only GARRISON_TALENT_FLAG_TEMPORARY=0x1 today, an unrelated meaning) are not
// wire-compatible, so derive the wire value purely from research state.
int32 wireFlags = 0; int32 wireFlags = 0;
if (talent.Rank >= 1) if (talent.Rank >= 1)
wireFlags |= 0x1; // client "researched / taken" bit; every client test pairs it with a rank check wireFlags |= 0x1;
if (talent.ResearchStartTime != 0) if (talent.ResearchStartTime != 0)
wireFlags |= 0x4; // client "being researched" bit (never 0x2 = duration override) wireFlags |= 0x4;
data << int32(talent.GarrTalentID); data << int32(talent.GarrTalentID);
data << int32(talent.Rank); data << int32(talent.Rank);
data << talent.ResearchStartTime; data << talent.ResearchStartTime;
data << int32(wireFlags); data << int32(wireFlags);
// socketData is the SAME reflection kind (0x80003) as the reward itemInstance, which is sniff-confirmed as
// an OptionalInit bit (NOT a uint32 count). The prior uint32 "fix" made each talent 3 bytes too long,
// shifting the mission-reward data written right after the Talents block (rewards showed value x 2^16).
data << OptionalInit(talent.Socket); data << OptionalInit(talent.Socket);
data.FlushBits(); data.FlushBits();
if (talent.Socket) if (talent.Socket)
@@ -420,10 +377,7 @@ namespace WorldPackets::Garrison
{ {
_worldPacket >> GarrTalentID; _worldPacket >> GarrTalentID;
uint32 count = _worldPacket.read<uint32>(); uint32 count = _worldPacket.read<uint32>();
// Sanity cap the client-supplied socket count before resize(): a soulbind/conduit tree has at most a few dozen
// sockets, so this is well above any legitimate value while stopping a crafted count (e.g. 0xFFFFFFFF) from
// requesting a multi-gigabyte allocation. resize() throws std::bad_alloc, which the opcode dispatcher does not
// catch (only ByteBufferException), so an unbounded count would crash the world thread instead of disconnecting.
count = std::min<uint32>(count, 64); count = std::min<uint32>(count, 64);
Sockets.resize(count); Sockets.resize(count);
for (GarrisonTalentSocketData& socket : Sockets) for (GarrisonTalentSocketData& socket : Sockets)
@@ -559,7 +513,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// Conservative shape: u32 Result, u32 GarrSpecID, u32 GarrPlotInstanceID.
WorldPacket const* GarrisonLearnSpecializationResult::Write() WorldPacket const* GarrisonLearnSpecializationResult::Write()
{ {
_worldPacket << uint32(Result); _worldPacket << uint32(Result);
@@ -568,7 +521,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// Conservative shape: u32 Result, u32 GarrPlotInstanceID, u32 GarrSpecID.
WorldPacket const* GarrisonBuildingSetActiveSpecializationResult::Write() WorldPacket const* GarrisonBuildingSetActiveSpecializationResult::Write()
{ {
_worldPacket << uint32(Result); _worldPacket << uint32(Result);
@@ -579,8 +531,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// u32 GarrPlotInstanceID, u64 TimeBuilt, u32 Result - see the header for the disassembly that inverts the
// order the old comment claimed. Result LAST is the field the client's `cmp [rcx+0x30], 0` gate reads.
WorldPacket const* GarrisonCompleteBuildingConstructionResult::Write() WorldPacket const* GarrisonCompleteBuildingConstructionResult::Write()
{ {
_worldPacket << uint32(GarrPlotInstanceID); _worldPacket << uint32(GarrPlotInstanceID);
@@ -590,7 +540,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// IDA case 4980791 (-8.45): PackedGuid + sub-call + varU32 size + varU32[size].
WorldPacket const* GarrisonOpenCrafter::Write() WorldPacket const* GarrisonOpenCrafter::Write()
{ {
_worldPacket << NpcGUID; _worldPacket << NpcGUID;
@@ -602,8 +551,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// IDA case 4980817 (-8.51): generic byte-block helper. Conservative: u32 NewMinLevel.
// 8 bytes, not 4 - the client's handler (0x22A0BA0) reads two u32s out of the opaque tail. See the header.
WorldPacket const* GarrisonAutoTroopMinLevelUpdateResult::Write() WorldPacket const* GarrisonAutoTroopMinLevelUpdateResult::Write()
{ {
_worldPacket << uint32(UnkLookupKey); _worldPacket << uint32(UnkLookupKey);
@@ -612,8 +559,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// IDA case 4980772 (-8.30): u8 GarrTypeID + GarrisonSmallStruct.
// Conservative inner shape: {u32 MissionRecID, u32 BonusAbilityID}.
WorldPacket const* GarrisonActivateMissionBonusAbility::Write() WorldPacket const* GarrisonActivateMissionBonusAbility::Write()
{ {
_worldPacket << uint8(GarrTypeID); _worldPacket << uint8(GarrTypeID);
@@ -667,7 +612,7 @@ namespace WorldPackets::Garrison
void GarrisonGetMissionReward::Read() void GarrisonGetMissionReward::Read()
{ {
_worldPacket >> DbID; // RAW uint64, not a PackedGuid ? see GarrisonPackets.h _worldPacket >> DbID;
_worldPacket >> MissionRecID; _worldPacket >> MissionRecID;
} }
@@ -747,8 +692,6 @@ namespace WorldPackets::Garrison
return &_worldPacket; return &_worldPacket;
} }
// IDA-confirmed (12.0.5.67186) layout ? see SNIFF_AUDIT -10.2. 3 CONFIRMED + 2 HIGH
// fields. FollowerInfo struct shape matches COMPLETE_MISSION_RESULT (-10.1.3).
WorldPacket const* GarrisonMissionBonusRollResult::Write() WorldPacket const* GarrisonMissionBonusRollResult::Write()
{ {
_worldPacket << Mission; _worldPacket << Mission;
@@ -52,12 +52,15 @@ public:
if (!player->GetGroup() || !player->GetGroup()->GetLeaderGUID()) if (!player->GetGroup() || !player->GetGroup()->GetLeaderGUID())
return; return;
player->GetGroup()->AddFollowerModeBots(player);
} }
void OnPlayerExit(Player* player) void OnPlayerExit(Player* player)
{ {
if (!player->GetGroup()) if (!player->GetGroup())
return; return;
player->GetGroup()->DespawnFollowerModeBots(player);
} }
}; };