Lfg list update to 12.1
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+1032
-117
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,14 @@
|
||||
#include "Define.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include "LFGListPackets.h"
|
||||
#include "SharedDefines.h"
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
class Group;
|
||||
class Player;
|
||||
|
||||
// Premade Group Finder (the "Premade Groups" tab). Server-side registry of player-published group listings + the
|
||||
@@ -46,7 +51,12 @@ namespace LFGList
|
||||
{
|
||||
uint32 Id = 0;
|
||||
ObjectGuid ApplicantGuid; // the applying player (or group leader)
|
||||
uint8 RoleMask = 0;
|
||||
uint8 RoleMask = 0; // the role the applicant asked for
|
||||
// The role the LEADER assigned when inviting. CMSG_LFG_LIST_INVITE_APPLICANT carries an Invitees[]
|
||||
// list of { PackedGuid, u8 RoleMask } (client writer RVA 0x6A4A30) precisely so the leader can grant
|
||||
// a role other than the one applied for - a healer applicant slotted as damage, say. Zero until an
|
||||
// invite names it; SMSG_LFG_LIST_APPLICATION_STATUS_UPDATE.RoleGranted then carries this, not RoleMask.
|
||||
uint8 GrantedRoleMask = 0;
|
||||
uint32 SpecID = 0;
|
||||
uint32 ItemLevel = 0;
|
||||
std::string Comment;
|
||||
@@ -54,18 +64,130 @@ namespace LFGList
|
||||
uint32 AppliedTime = 0; // drives the retail 300s application timeout (sniff-verified)
|
||||
};
|
||||
|
||||
// How the player has dealt with a flagged (censored) listing. Mirrors the tri-state the client keeps
|
||||
// at LFG-list manager offset +0x13E0: 1 = flagged and undecided, 2 = the player confirmed it as is,
|
||||
// 0 = not flagged / cleared. C_LFGList.IsCensoredActiveEntryUnresolved tests for exactly the 1.
|
||||
enum class CensorState : uint8
|
||||
{
|
||||
None = 0,
|
||||
Unresolved = 1,
|
||||
Confirmed = 2,
|
||||
};
|
||||
|
||||
// AdvancedFilterOptions as the client packs it into CMSG_LFG_LIST_SEARCH.AdvancedFilterMask (client RVA 0x24E1030,
|
||||
// LSB first in the order Blizzard_APIDocumentationGenerated/LFGListInfoDocumentation.lua declares the fields).
|
||||
enum AdvancedFilterFlag : uint32
|
||||
{
|
||||
ADVANCED_FILTER_NEEDS_TANK = 0x0001,
|
||||
ADVANCED_FILTER_NEEDS_HEALER = 0x0002,
|
||||
ADVANCED_FILTER_NEEDS_DAMAGE = 0x0004,
|
||||
ADVANCED_FILTER_NEEDS_MY_CLASS = 0x0008,
|
||||
ADVANCED_FILTER_HAS_TANK = 0x0010,
|
||||
ADVANCED_FILTER_HAS_HEALER = 0x0020,
|
||||
ADVANCED_FILTER_DIFFICULTY_NORMAL = 0x0040,
|
||||
ADVANCED_FILTER_DIFFICULTY_HEROIC = 0x0080,
|
||||
ADVANCED_FILTER_DIFFICULTY_MYTHIC = 0x0100,
|
||||
ADVANCED_FILTER_DIFFICULTY_MYTHIC_PLUS = 0x0200,
|
||||
ADVANCED_FILTER_GENERAL_PLAYSTYLE_1 = 0x0400, // Learning
|
||||
ADVANCED_FILTER_GENERAL_PLAYSTYLE_2 = 0x0800, // FunRelaxed
|
||||
ADVANCED_FILTER_GENERAL_PLAYSTYLE_3 = 0x1000, // FunSerious
|
||||
ADVANCED_FILTER_GENERAL_PLAYSTYLE_4 = 0x2000, // Expert
|
||||
|
||||
ADVANCED_FILTER_DIFFICULTY_ANY = ADVANCED_FILTER_DIFFICULTY_NORMAL | ADVANCED_FILTER_DIFFICULTY_HEROIC
|
||||
| ADVANCED_FILTER_DIFFICULTY_MYTHIC | ADVANCED_FILTER_DIFFICULTY_MYTHIC_PLUS,
|
||||
ADVANCED_FILTER_GENERAL_PLAYSTYLE_ANY = ADVANCED_FILTER_GENERAL_PLAYSTYLE_1 | ADVANCED_FILTER_GENERAL_PLAYSTYLE_2
|
||||
| ADVANCED_FILTER_GENERAL_PLAYSTYLE_3 | ADVANCED_FILTER_GENERAL_PLAYSTYLE_4,
|
||||
};
|
||||
|
||||
// What a listing's party looks like to a browser: C_LFGList.GetSearchResultMemberCounts' TANK / HEALER / DAMAGER
|
||||
// and per-class counts, taken from the very role bytes the search row carries so the two cannot disagree.
|
||||
struct MemberComposition
|
||||
{
|
||||
uint8 Tanks = 0;
|
||||
uint8 Healers = 0;
|
||||
uint8 Damagers = 0;
|
||||
std::array<uint8, MAX_CLASSES> Classes = { };
|
||||
};
|
||||
|
||||
// The search terms of one CMSG_LFG_LIST_SEARCH, as the packet hands them over: one inner vector per
|
||||
// term block, holding that block's non-empty values. Blocks are ANDed, values inside a block ORed -
|
||||
// see LFGListSearch::GetKeywords, which owns that decision and marks it.
|
||||
using SearchKeywords = std::vector<std::vector<std::string>>;
|
||||
|
||||
// Everything one CMSG_LFG_LIST_SEARCH asks for, in one object, so the search reply and the live push
|
||||
// cannot drift apart: both run it through LFGListMgr::Matches and there is no second copy of the rule.
|
||||
// Field meanings and their sources are documented on WorldPackets::LFGList::LFGListSearch; in short,
|
||||
// ResolvedActivityIds is the set the CLIENT derived from category + filter + search box, while
|
||||
// ActivityIds and ActivityGroupIds are the explicit narrowings from the advanced filter. Every member
|
||||
// left empty / 0 is a wildcard.
|
||||
struct SearchFilter
|
||||
{
|
||||
uint32 CategoryId = 0;
|
||||
std::vector<uint32> ResolvedActivityIds; // GroupFinderActivity ids, client-resolved
|
||||
std::vector<uint32> ActivityIds; // GroupFinderActivity ids, C_LFGList.Search arg 7
|
||||
std::vector<uint32> ActivityGroupIds; // GroupFinderActivityGrp ids, advancedFilter.activities
|
||||
SearchKeywords Keywords;
|
||||
uint32 AdvancedFilterMask = 0; // AdvancedFilterFlag
|
||||
uint32 MinimumRating = 0; // advancedFilter.minimumRating, against the leader's dungeon score
|
||||
uint32 LanguageMask = 0; // one bit per LocaleConstant; 0 = no restriction
|
||||
uint8 SearcherClass = 0; // needsMyClass is relative to the player who searches
|
||||
};
|
||||
|
||||
// One published group listing.
|
||||
struct Listing
|
||||
{
|
||||
uint32 Id = 0; // server-issued listing id (RideTicket.Id)
|
||||
ObjectGuid LeaderGuid;
|
||||
ObjectGuid GroupGuid; // the leader's group (empty = solo listing)
|
||||
ObjectGuid GroupGuid; // the leader's group (empty = solo listing), LIVE - see TicketGuid
|
||||
// The listing's identity on the wire, frozen at CreateListing and never written again. It is the
|
||||
// group guid if the leader already had a group when publishing, otherwise the leader's own guid.
|
||||
// It exists because GroupGuid is LIVE: a solo leader who publishes and then accepts an applicant
|
||||
// gets a group created for him (LFGListHandler, HandleLFGListInviteResponse), and deriving the
|
||||
// ticket from GroupGuid made the ticket of an already-published listing change underneath the
|
||||
// client. The consumer of SMSG_LFG_LIST_UPDATE_STATUS (RVA 0x24DE410) compares the incoming ticket
|
||||
// against its stored active entry field by field and drops anything that differs, so after that
|
||||
// flip the leader's own delist, the expiry and every application status update were discarded in
|
||||
// silence and the entry stayed on his screen. The row header of a search result is the same ticket
|
||||
// in disguise, so an open browser saw a NEW row instead of an update of the one it had.
|
||||
// GroupGuid stays live on purpose - it is what enumerates the current members.
|
||||
ObjectGuid TicketGuid;
|
||||
WorldPackets::LFGList::ListingDescriptor Descriptor;
|
||||
// SMSG_LFG_LIST_SEARCH_RESULTS(+_UPDATE) row revision. The client applies an update record only when its revision
|
||||
// is not older than the row it holds (0x7FF7CF2AF730), so it has to grow with every change. Every fresh 12.1
|
||||
// retail row carries 3.
|
||||
uint32 Revision = 3;
|
||||
// Who last changed the listing, and its name / comment / voice-chat text (row guids +1840..+1888).
|
||||
ObjectGuid LastEditorGuid;
|
||||
ObjectGuid NameEditorGuid;
|
||||
ObjectGuid CommentEditorGuid;
|
||||
ObjectGuid VoiceChatEditorGuid;
|
||||
uint32 CreatedTime = 0;
|
||||
uint32 ExpireTime = 0;
|
||||
std::vector<Application> Applications;
|
||||
// Every client that has been handed this listing with Listed = true, and therefore holds an active
|
||||
// entry for it. The delist has to reach ALL of them, not just the leader: the consumer of
|
||||
// SMSG_LFG_LIST_UPDATE_STATUS (RVA 0x24DE410) creates the entry from any Listed payload whose ticket
|
||||
// it does not know yet, and an applicant who accepts an invite is sent exactly that (status 0x19).
|
||||
// Notifying only the leader left the joined member's group finder showing the entry until relog -
|
||||
// the very damage DelistAndNotify exists to prevent, one flank short. Maintained by
|
||||
// WorldSession::SendLFGListUpdateStatus, which is the only producer of a Listed payload.
|
||||
std::unordered_set<ObjectGuid> StatusRecipients;
|
||||
CensorState Censor = CensorState::None;
|
||||
uint8 CensorCode = 0;
|
||||
uint8 CensorFieldFlags = 0; // search row +2161: 1 = the name is flagged, 2 = the comment // non-zero is what makes the client treat the entry as flagged
|
||||
|
||||
uint32 GetCategoryID() const { return Descriptor.CategoryID; }
|
||||
// Flagged at all - this is what the wire carries, and what decides whether
|
||||
// SMSG_LFG_LIST_CENSORED_ACTIVE_ENTRY_UPDATE goes out with a code.
|
||||
bool IsCensored() const { return Censor != CensorState::None && CensorCode != 0; }
|
||||
// Flagged AND still undecided - this, and only this, is what withholds the listing's text.
|
||||
// Once the player has confirmed the listing (CensorState::Confirmed, set by
|
||||
// CMSG_LFG_LIST_CONFIRM_CENSORED_ACTIVE_ENTRY) the text goes out again. It has to: this wire can
|
||||
// only ever push the client's censor state to 0 or 1, so a confirmed listing is never re-announced,
|
||||
// and a client that lost its local 2 (any UI reload) would otherwise render an EMPTY title with no
|
||||
// dialog and no explanation left to account for it. Withholding past the confirmation buys nothing
|
||||
// and costs the owner their listing's name. See GetPublicDescriptor.
|
||||
bool IsTextWithheld() const { return Censor == CensorState::Unresolved && CensorCode != 0; }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,31 +202,183 @@ public:
|
||||
uint32 CreateListing(Player* leader, WorldPackets::LFGList::ListingDescriptor const& descriptor);
|
||||
bool UpdateListing(uint32 listingId, ObjectGuid leader, WorldPackets::LFGList::ListingDescriptor const& descriptor);
|
||||
void RemoveListing(uint32 listingId, ObjectGuid leader);
|
||||
void RemoveListingsBy(ObjectGuid leader); // logout cleanup
|
||||
// Delist whatever this leader has listed - logout cleanup, and the replace step of a re-publish.
|
||||
// Goes through DelistAndNotify like every other server-initiated delist; see its definition for why
|
||||
// erasing the maps in silence was wrong on both paths.
|
||||
void RemoveListingsBy(ObjectGuid leader);
|
||||
// Delist AND tell everyone holding an active entry for the listing, in that order, from one place. The
|
||||
// notification has to be built while the listing still exists: the client only accepts a delist whose
|
||||
// ticket matches its stored active entry field for field (see FillListingTicket below), and a ticket
|
||||
// cannot be reconstructed once the listing is gone. Every server-initiated delist goes through here; a
|
||||
// hand-built "not listed" message is silently dropped by the client and leaves the entry standing in the
|
||||
// recipient's group finder.
|
||||
// "Everyone" is the leader plus Listing::StatusRecipients - an applicant who accepted an invite was sent
|
||||
// the listing with Listed = true (status 0x19) and holds an entry of its own. Addressing the leader alone
|
||||
// fixed the create/edit flank and left that one standing until relog.
|
||||
// `status` selects the client's popup (consumer RVA 0x24DE410): 0x2C too many players, 0x3B timeout,
|
||||
// 0x4B GameError 0x291, anything else no popup at all.
|
||||
void DelistAndNotify(uint32 listingId, ObjectGuid leader, uint8 status);
|
||||
|
||||
LFGList::Listing* GetListing(uint32 listingId);
|
||||
LFGList::Listing const* GetListing(uint32 listingId) const;
|
||||
LFGList::Listing* GetListingByLeader(ObjectGuid leader);
|
||||
// The listing published for THIS group, if any. There is no index for it because there cannot be more
|
||||
// than a handful of listings at a time and because an index keyed on Listing::GroupGuid would have to be
|
||||
// maintained across the one place that writes it after CreateListing (a solo leader whose first
|
||||
// applicant forms the party, WorldSession::HandleLFGListInviteResponse). The three group hooks below are
|
||||
// its only callers.
|
||||
LFGList::Listing* GetListingByGroup(ObjectGuid groupGuid);
|
||||
|
||||
// Search the registry. Any argument left 0 acts as a wildcard. Results are capped by config.
|
||||
std::vector<LFGList::Listing const*> Search(uint32 category, uint32 activityGroup, uint32 activityId, std::string const& keyword = std::string()) const;
|
||||
// ---- the group's own lifecycle, as it reaches the listing (LFGGroupScript) ----
|
||||
// Without these three the listing outlives the group it advertises. Logout of the leader and the expiry
|
||||
// sweep were the only two events that could ever end a listing, so a disbanded group kept a phantom row
|
||||
// in the browser for the rest of the 30-minute window, a promoted leader could not touch the listing of
|
||||
// his own party, and a member who left kept an active entry until relog.
|
||||
|
||||
// The party has a new leader. The listing follows him: Listing::LeaderGuid is what UpdateListing,
|
||||
// RemoveListing and DelistAndNotify all check the sender against, so leaving it on the previous leader
|
||||
// locks the listing - the new leader can neither edit nor delist it while it goes on being advertised.
|
||||
// He is also sent the active entry, because he needs one for C_LFGList.HasActiveEntryInfo to be true.
|
||||
//
|
||||
// Retail additionally WARNS him and delists on a timer, and that half does not live here: the warning
|
||||
// is SMSG_PARTY_NOTIFY_LFG_LEADER_CHANGE (0x45030A, family 0x45, still STATUS_UNHANDLED and NOT part of
|
||||
// this unit), which feeds LFG_GROUP_DELISTED_LEADERSHIP_CHANGE(listingName, automaticDelistTimeRemaining)
|
||||
// (LFGListInfoDocumentation.lua:684-691) and through it the popup PREMADE_GROUP_LEADER_CHANGE_DELIST_WARNING
|
||||
// (LFGList.lua:346-348). The delist itself is then the CLIENT's: GameDialogDefs.lua:3286-3317 shows the
|
||||
// popup calling C_LFGList.RemoveListing() from its OnHide unless the new leader picks "List My Group" -
|
||||
// i.e. the server must NOT delist here, it must keep the listing alive and owned so that the client's
|
||||
// choice has something to act on. See dod_luecken/D2 in the status file for the handover.
|
||||
void TransferListingLeadership(ObjectGuid groupGuid, ObjectGuid newLeader);
|
||||
// The party is gone. So is anything it advertised.
|
||||
void RemoveListingsByGroup(ObjectGuid groupGuid);
|
||||
// One member joined a listed party. Nothing to decide, only something to say: the row a browser holds
|
||||
// enumerates the LIVE group, so it is out of date until this pushes it.
|
||||
void NotifyGroupMemberJoined(ObjectGuid groupGuid);
|
||||
// One member left a listed party - the listing stays, that member's active entry must not. Also
|
||||
// refreshes the row in every open browser, without the departing member.
|
||||
void NotifyGroupMemberLeft(ObjectGuid groupGuid, ObjectGuid member);
|
||||
|
||||
// Search the registry. Every empty / zero member of the filter is a wildcard. Results are capped by
|
||||
// config. The per-listing test is Matches(), which the live push uses too.
|
||||
std::vector<LFGList::Listing const*> Search(LFGList::SearchFilter const& filter) const;
|
||||
|
||||
// Does this listing satisfy a search filter? THE one place that answers it - the search reply and the
|
||||
// live SMSG_LFG_LIST_SEARCH_RESULTS_UPDATE push both call it, so a pushed row can never reach a browser
|
||||
// whose filters would have excluded it from the reply. That agreement used to be a promise kept by two
|
||||
// hand-maintained copies of the same conditions.
|
||||
static bool Matches(LFGList::Listing const& listing, LFGList::SearchFilter const& filter);
|
||||
|
||||
// Does this listing satisfy the keyword part of a search? The ONE place that answers it, because the
|
||||
// search reply and the live push have to agree - and because it has to agree with what
|
||||
// GetPublicDescriptor actually delivers. A listing whose text is withheld is matched against an EMPTY
|
||||
// name, i.e. it is never a keyword hit: matching the STORED name would have let a searcher confirm a
|
||||
// word inside the withheld text from a result row that renders as CENSORED_LFG_GROUP_NAME, which is the
|
||||
// one thing withholding the text is supposed to prevent. Such a listing still shows up in an unfiltered
|
||||
// browse, exactly as in retail.
|
||||
static bool MatchesKeywords(LFGList::Listing const& listing, LFGList::SearchKeywords const& keywords);
|
||||
|
||||
// The role a member advertises in a search row (0 tank, 1 healer, 2 damage - ChrSpecialization.Role): the role the
|
||||
// party assigned (Group member slot, set when an applicant is invited with a granted role), else the member's
|
||||
// specialization role. Used for the row's role byte and for the advanced filter, so both see the same party.
|
||||
static uint8 GetMemberRole(Player const* player, Group const* group);
|
||||
static LFGList::MemberComposition GetMemberComposition(LFGList::Listing const& listing, ObjectGuid excludeMember = ObjectGuid::Empty);
|
||||
// The leader's overall Mythic+ rating (PlayerData.DungeonScore), what advancedFilter.minimumRating is compared to.
|
||||
static float GetLeaderDungeonScore(LFGList::Listing const& listing);
|
||||
// advancedFilter's difficulty bands for the listing's first activity (LFGList.lua uses activityIDs[1]). True when the
|
||||
// activity is in no band the filter leaves out.
|
||||
static bool MatchesDifficultyBand(LFGList::Listing const& listing, uint32 advancedFilterMask);
|
||||
|
||||
// Fills one search-result row for a listing. Shared by the search reply, the apply-result snapshot and the
|
||||
// live update push so all three serialize a listing identically.
|
||||
void FillSearchRow(WorldPackets::LFGList::SearchResultListing& row, LFGList::Listing const& listing) const;
|
||||
// `excludeMember` leaves one guid out of the roster. It exists for exactly one caller and for a reason
|
||||
// that is not negotiable: LFGGroupScript::OnRemoveMember fires from the FIRST statement of
|
||||
// Group::RemoveMember (Group.cpp:551), long before the member slot is erased, so a row built from the
|
||||
// live group at that moment still advertises the player who is leaving.
|
||||
// `viewer` is the player the row is written for (the friend / guild-mate lists and hasSelf are relative to them);
|
||||
// null for an update record, which carries none of those.
|
||||
void FillSearchRow(WorldPackets::LFGList::SearchResultListing& row, LFGList::Listing const& listing,
|
||||
Player const* viewer, ObjectGuid excludeMember = ObjectGuid::Empty) const;
|
||||
|
||||
// The two RideTickets of the listing system. These MUST be built in exactly one place: the client keys
|
||||
// its stored active entry on the whole 32-byte ticket and compares it field by field before it accepts a
|
||||
// delist (SMSG_LFG_LIST_UPDATE_STATUS consumer @ RVA 0x24DE410, six comparisons against LFG-list manager
|
||||
// +0..+24). A ticket assembled differently on one path than on another is silently ignored by the client.
|
||||
static void FillListingTicket(WorldPackets::LFG::RideTicket& ticket, LFGList::Listing const& listing);
|
||||
static void FillApplicationTicket(WorldPackets::LFG::RideTicket& ticket, LFGList::Application const& app);
|
||||
|
||||
// The absolute deadline of an application, as BOTH messages of the application strand carry it:
|
||||
// SMSG_LFG_LIST_APPLY_TO_GROUP_RESULT.ApplicationExpiration and
|
||||
// SMSG_LFG_LIST_APPLICATION_STATUS_UPDATE.ApplicationExpiration. The two are not two fields that happen
|
||||
// to look alike - they land in the SAME client slot. Both messages are decoded into the state setter
|
||||
// @ RVA 0x24DD190 as (record, state, time, code, byte), and the consumer subtracts the client's time
|
||||
// base qword_7FF7877C9640 before storing it, i.e. the wire value is an absolute server timestamp.
|
||||
// Shared because it MUST be: the apply reply used to compute it inline while every status update sent a
|
||||
// hard 0, so the reply set the applicant's deadline and the status update one line later overwrote it
|
||||
// with 0 - timebase. Same defect class, same fix, as the drifted ticket builders above.
|
||||
static uint64 GetApplicationExpiration(LFGList::Application const& app);
|
||||
|
||||
// The wire state bits of an application, and one whole applicant record of SMSG_LFG_LIST_APPLICANT_LIST_UPDATE.
|
||||
// Shared for the same reason as the tickets: the message has two producers (the handler's push and the
|
||||
// application-timeout sweep) and every field one of them forgets is a field the leader's applicant list loses.
|
||||
static uint8 ApplicationStateToBits(LFGList::ApplicationState state);
|
||||
static void FillApplicantInfo(WorldPackets::LFGList::ApplicantInfo& info, LFGList::Application const& app);
|
||||
|
||||
// Push the full applicant list of a listing to every connected member of the listed group (sniff: the
|
||||
// packet goes to all members, not only the leader; solo listings notify just the leader). THE one
|
||||
// producer of SMSG_LFG_LIST_APPLICANT_LIST_UPDATE - it lived in the handler's anonymous namespace while
|
||||
// the timeout sweep in Update() had a hand-built copy that addressed the LEADER ALONE, so after every
|
||||
// application timeout the non-leader members of a listed group kept the expired applicant in their list
|
||||
// until relog. Same defect class as the four drifted copies of the delist message, and the same fix.
|
||||
static void SendApplicantList(LFGList::Listing const& listing);
|
||||
|
||||
// SMSG_LFG_LIST_APPLICATION_STATUS_UPDATE with the wire state given DIRECTLY instead of derived from
|
||||
// Application::State. It exists because an application can end in states the server-side enum has no
|
||||
// member for - "declined because the party filled up", "declined because the listing is gone" - which
|
||||
// the CLIENT does have (nibbles 6 and 7 of the state->string mapper @ RVA 0x24DADA0). Without it those
|
||||
// endings were bare returns and the applicant was told nothing at all.
|
||||
//
|
||||
// failureReason has NO default on purpose. It is the message's reason field, and the client reads it on
|
||||
// exactly one state: the setter @ RVA 0x24DD190 forwards it to the error presenter @ RVA 0x24E25E0 only
|
||||
// when the state resolves to 3 = failed. There it must be one of the presenter table's values or the
|
||||
// lookup falls off the end and the applicant sees nothing - the state changes, no text appears. On every
|
||||
// other state pass ApplicationFailureReason::NotAFailure, which is the value the capture carries and
|
||||
// says in its name that no reason is being claimed. A default would let a Failed caller forget it and
|
||||
// reinstate exactly the silence this parameter exists to end.
|
||||
static void SendApplicationStatusBits(LFGList::Listing const& listing, LFGList::Application const& app,
|
||||
uint8 stateBits, uint8 failureReason);
|
||||
|
||||
// The descriptor as it must go out to a client. Identical to the stored one, except that a listing
|
||||
// whose text was flagged and not yet resolved goes out WITHOUT its name and comment: retail withholds
|
||||
// them (that is why the edit dialog has to ask the server via C_LFGList.DoesCensoredTextMatch instead
|
||||
// of comparing locally), and the client renders CENSORED_LFG_GROUP_NAME in their place.
|
||||
WorldPackets::LFGList::ListingDescriptor GetPublicDescriptor(LFGList::Listing const& listing) const;
|
||||
|
||||
// Runs the listing's text through the server-side check and updates its censor state. Returns true if
|
||||
// the state changed, i.e. if SMSG_LFG_LIST_CENSORED_ACTIVE_ENTRY_UPDATE has to go out.
|
||||
bool EvaluateCensorship(LFGList::Listing& listing);
|
||||
// CMSG_LFG_LIST_CONFIRM_CENSORED_ACTIVE_ENTRY: the player keeps the flagged listing as it is.
|
||||
// Returns true when the state actually moved to Confirmed, and a true return OBLIGES the caller to push
|
||||
// the descriptor: this is the third mutation of the publicly visible descriptor (IsTextWithheld goes
|
||||
// false, so GetPublicDescriptor stops clearing Name/Comment and MatchesKeywords stops rejecting), and
|
||||
// unlike CreateListing and UpdateListing it does not notify by itself - the handler owns the two sends
|
||||
// because only it has the owner's session. See WorldSession::HandleLFGListConfirmCensoredActiveEntry.
|
||||
bool ConfirmCensoredListing(uint32 listingId, ObjectGuid leader);
|
||||
|
||||
// Live search updates. While a player has the Premade Groups browser open, retail keeps pushing
|
||||
// SMSG_LFG_LIST_SEARCH_RESULTS_UPDATE as listings appear/change. A search registers the player's filters
|
||||
// here; listing mutations then push the affected row to every subscriber whose filters still match.
|
||||
void RegisterSearch(ObjectGuid player, uint32 category, uint32 activityGroup, std::string const& keyword);
|
||||
void RegisterSearch(ObjectGuid player, LFGList::SearchFilter filter);
|
||||
void UnregisterSearch(ObjectGuid player);
|
||||
void NotifyListingChanged(uint32 listingId);
|
||||
// `changes` (WorldPackets::LFGList::SearchResultChange) names the listing fields the update record carries.
|
||||
void NotifyListingChanged(uint32 listingId, ObjectGuid excludeMember = ObjectGuid::Empty, uint32 changes = 0);
|
||||
|
||||
// Applications. An application gets a globally-unique id the client keys on via a RideTicket.
|
||||
LFGList::Application* AddApplication(uint32 listingId, ObjectGuid applicant, uint8 roleMask, uint32 specId, uint32 itemLevel, std::string const& comment);
|
||||
LFGList::Listing* GetListingByApplication(uint32 applicationId);
|
||||
LFGList::Application* GetApplication(uint32 applicationId);
|
||||
bool SetApplicationState(uint32 applicationId, LFGList::ApplicationState state);
|
||||
// Records the role the leader granted in CMSG_LFG_LIST_INVITE_APPLICANT.Invitees[].
|
||||
bool SetApplicationGrantedRole(uint32 applicationId, uint8 roleMask);
|
||||
void RemoveApplication(uint32 applicationId);
|
||||
// Drops every application this player has outstanding (logout cleanup).
|
||||
void RemoveApplicationsBy(ObjectGuid applicant);
|
||||
@@ -114,17 +388,52 @@ public:
|
||||
private:
|
||||
LFGListMgr() = default;
|
||||
|
||||
// Push ONE row of one listing to every open browser whose filters still match it, and reap the
|
||||
// subscriptions that have lapsed while doing so. The one place that serializes
|
||||
// SMSG_LFG_LIST_SEARCH_RESULTS_UPDATE, which is what keeps its two occasions in step: a listing that
|
||||
// changed (delisted = false) and a listing that is about to cease to exist (delisted = true, the sticky
|
||||
// isDelisted bit - see LFGListSearchResultsUpdate in LFGListPackets.h). The second occasion is why this
|
||||
// takes the listing by reference rather than an id: it is called from inside DelistAndNotify, where the
|
||||
// row has to be built while the listing is still there.
|
||||
// Every push is a change of the listing, so it advances the listing's revision.
|
||||
void PushSearchRow(LFGList::Listing& listing, bool delisted, ObjectGuid excludeMember, uint32 changes);
|
||||
|
||||
// The delist form of SMSG_LFG_LIST_UPDATE_STATUS - Listed = 0, no expiry, no descriptor - built from the
|
||||
// LIVE listing. THE one builder, for the same reason the two ticket builders are: the client compares
|
||||
// the incoming ticket against its stored active entry field by field and drops a mismatch in silence
|
||||
// (consumer RVA 0x24DE410), and four hand-built copies of this message have already drifted apart once.
|
||||
// Its two callers differ only in who they send it to - DelistAndNotify to everyone holding an entry,
|
||||
// NotifyGroupMemberLeft to the single player who left.
|
||||
static void BuildDelistPacket(WorldPackets::LFGList::LFGListUpdateStatus& packet,
|
||||
LFGList::Listing const& listing, uint8 status);
|
||||
|
||||
// An open Premade Groups browser: the filters the player last searched with. 0 / empty = wildcard, matching
|
||||
// Search(). Refreshed by every search; expires so a client that closed the browser (there is no "stopped
|
||||
// searching" opcode) stops receiving pushes.
|
||||
struct SearchSubscription
|
||||
{
|
||||
uint32 CategoryId = 0;
|
||||
uint32 ActivityGroupId = 0;
|
||||
std::string Keyword;
|
||||
LFGList::SearchFilter Filter;
|
||||
uint32 ExpireTime = 0;
|
||||
};
|
||||
|
||||
// Parsed LFGList.CensorWords, lower-cased. Re-parsed whenever the config string changes, which is what
|
||||
// makes `reload config` take effect without a hook of its own.
|
||||
std::vector<std::string> _censorWords;
|
||||
std::string _censorWordsConfig;
|
||||
bool _censorWordsLoaded = false;
|
||||
std::vector<std::string> const& GetCensorWords();
|
||||
|
||||
// Does this listing text hit the configured word list? The list is LFGList.CensorWords and nothing
|
||||
// else. It used to be ObjectMgr::IsReservedName, i.e. the `reserved_name` table, and that was wrong in
|
||||
// both directions: the table ships EMPTY (sql/base/characters_database.sql has not one INSERT for it),
|
||||
// so on any normal realm the check could never fire and the whole censor pair was dead wire; and where
|
||||
// an admin does fill it, he fills it with CHARACTER names he wants kept off the realm - class names,
|
||||
// "gm", "admin" - which would then flag ordinary listing titles, withhold their name and comment from
|
||||
// every searcher and drop them out of keyword search entirely (MatchesKeywords returns false for a
|
||||
// withheld listing). Two unrelated policies on one table. Empty list = censorship off, which is the
|
||||
// shipped default and is an honest off, not an accident.
|
||||
bool ContainsCensoredWord(std::string const& text);
|
||||
|
||||
uint32 _nextListingId = 1;
|
||||
uint32 _nextApplicationId = 1;
|
||||
uint32 _expireTimer = 0;
|
||||
|
||||
@@ -20,94 +20,153 @@
|
||||
|
||||
namespace WorldPackets::LFGList
|
||||
{
|
||||
// The published-listing parameters. RESOLVED from the 12.0.7.68275 premade-groups sniff + the client JOIN
|
||||
// serializer (sub_7FF72914ABE0) + the generated Lua API doc (LfgListingCreateData). The descriptor is BIT-PACKED:
|
||||
// a bit-packed header (5-bit trailing-vector count; three bit-packed string lengths of 10/11/8 bits; four boolean
|
||||
// flags; and presence bits for the nilable numeric fields), then FlushBits, then the member-requirement block, the
|
||||
// fixed activity fields, the trailing uint32 vector, the three strings, and the present optional fields. Only
|
||||
// CategoryID + the activity vector + item-level drive server filtering; the rest are pass-through echo. Reads are guarded against
|
||||
// over-run (the descriptor is variable-length and pass-through, so a malformed tail is tolerated, never fatal).
|
||||
// Full layout + bit-widths: c:\dumps\LFG_LIST_WIRE_68275.md.
|
||||
namespace
|
||||
{
|
||||
// Client buffer capacities of the three descriptor strings; they decide the bit width of each length
|
||||
// prefix (N = ceil(log2(capacity))) and, because the client appends its own NUL at buf[len], the
|
||||
// largest string that still fits.
|
||||
constexpr std::size_t DESCRIPTOR_NAME_CAPACITY = 513; // bits<10>
|
||||
constexpr std::size_t DESCRIPTOR_COMMENT_CAPACITY = 1025; // bits<11>
|
||||
constexpr std::size_t DESCRIPTOR_VOICECHAT_CAPACITY = 129; // bits<8>
|
||||
|
||||
// The trailing uint32 vector's count is a 5-bit field, so it can never legitimately exceed 31.
|
||||
constexpr uint32 DESCRIPTOR_MAX_ACTIVITY_IDS = 31;
|
||||
}
|
||||
|
||||
// The embedded DungeonScoreSummary. MythicPlusPacketsCommon already provides operator<< for both this and
|
||||
// its element type and both are byte-identical to the client's reader 0x6EC830 / writer 0x6EC980
|
||||
// ({float, float, u32 count} then count x {i32, float, i32, i32, u8, one bit + flush}); only the read direction
|
||||
// is missing there, so it lives here.
|
||||
static ByteBuffer& operator>>(ByteBuffer& data, MythicPlus::DungeonScoreMapSummary& run)
|
||||
{
|
||||
data >> run.ChallengeModeID;
|
||||
data >> run.MapScore;
|
||||
data >> run.BestRunLevel;
|
||||
data >> run.BestRunDurationMS;
|
||||
data >> run.Unknown1110;
|
||||
data >> Bits<1>(run.FinishedSuccess);
|
||||
data.ResetBitPos();
|
||||
return data;
|
||||
}
|
||||
|
||||
static ByteBuffer& operator>>(ByteBuffer& data, MythicPlus::DungeonScoreSummary& summary)
|
||||
{
|
||||
data >> summary.OverallScoreCurrentSeason;
|
||||
data >> summary.LadderScoreCurrentSeason;
|
||||
|
||||
uint32 runCount = 0;
|
||||
data >> runCount;
|
||||
// 18 wire bytes per run; refuse a count the packet cannot possibly contain rather than trusting it.
|
||||
if (runCount <= (data.size() - data.rpos()) / 18)
|
||||
{
|
||||
summary.Runs.resize(runCount);
|
||||
for (MythicPlus::DungeonScoreMapSummary& run : summary.Runs)
|
||||
data >> run;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
// ListingDescriptor. See the header for the full layout and for what is measured vs. inferred.
|
||||
// Reads are size-guarded throughout: the descriptor is variable-length and largely pass-through, so a
|
||||
// malformed tail must be tolerated, never fatal.
|
||||
static ByteBuffer& operator>>(ByteBuffer& data, ListingDescriptor& d)
|
||||
{
|
||||
auto remaining = [&]() -> std::size_t { return data.size() - data.rpos(); };
|
||||
|
||||
// --- bit-packed header (client bit-writer, MSB-first) ---
|
||||
uint32 vectorCount = data.ReadBits(5); // sub_7FF729064C20: count of the trailing uint32 vector
|
||||
uint32 str0Len = data.ReadBits(10); // string @0x40 length
|
||||
uint32 str1Len = data.ReadBits(11); // string @0x241 length
|
||||
uint32 str2Len = data.ReadBits(8); // string @0x642 length ("crate" in the sniff)
|
||||
d.IsAutoAccept = data.ReadBits(1) != 0; // presence/flag bits (client offsets 0x6c3..0x703)
|
||||
d.IsCrossFactionListing = data.ReadBits(1) != 0;
|
||||
d.IsPrivateGroup = data.ReadBits(1) != 0;
|
||||
d.NewPlayerFriendly = data.ReadBits(1) != 0;
|
||||
bool hasQuestId = data.ReadBits(1) != 0; // 0x6cc -> uint32 @0x6c8
|
||||
bool hasOpt1 = data.ReadBits(1) != 0; // 0x6f4 -> uint32 @0x6f0
|
||||
bool hasOpt2 = data.ReadBits(1) != 0; // 0x6fc -> uint32 @0x6f8
|
||||
bool hasOpt3 = data.ReadBits(1) != 0; // 0x701 -> uint8 @0x700
|
||||
data.ReadBits(1); // 0x703 standalone flag (unused server-side)
|
||||
data.ResetBitPos(); // FlushBits (sub_7FF729064E60)
|
||||
// --- bit header, MSB-first, 43 bits (client reader 0x7572C0 / writer 0x757660) ---
|
||||
uint32 const activityCount = data.ReadBits(5);
|
||||
uint32 const nameLength = data.ReadBits(10);
|
||||
uint32 const commentLength = data.ReadBits(11);
|
||||
uint32 const voiceChatLength = data.ReadBits(8);
|
||||
d.IsAutoAccept = data.ReadBits(1) != 0; // +1699
|
||||
d.IsPrivateGroup = data.ReadBits(1) != 0; // +1700
|
||||
d.IsWarMode = data.ReadBits(1) != 0; // +1701
|
||||
d.IsCrossFactionListing = data.ReadBits(1) != 0; // +1702
|
||||
bool const hasQuestId = data.ReadBits(1) != 0;
|
||||
bool const hasRequiredDungeonScore = data.ReadBits(1) != 0;
|
||||
bool const hasRequiredPvpRating = data.ReadBits(1) != 0;
|
||||
bool const hasPlaystyle = data.ReadBits(1) != 0;
|
||||
d.NewPlayerFriendly = data.ReadBits(1) != 0; // +1763
|
||||
data.ResetBitPos(); // 5 spare bits; the client never reads them
|
||||
|
||||
// --- member-requirement block (nested sub_7FF729167840) ---
|
||||
data >> d.HeaderFloat0 >> d.HeaderFloat1;
|
||||
uint32 memberCount = 0;
|
||||
data >> memberCount;
|
||||
if (memberCount <= remaining() / 0x11) // 0x11 = min bytes per entry; guard against a bad count
|
||||
// --- byte-aligned body ---
|
||||
data >> d.CategoryID;
|
||||
data >> d.RequiredItemLevel;
|
||||
// The client zeroes this block on every CreateListing / UpdateListing, so an inbound descriptor always
|
||||
// carries an empty summary. Read it anyway - it is 12 bytes of wire either way.
|
||||
data >> d.LeaderScore;
|
||||
data >> d.GeneralPlaystyle;
|
||||
|
||||
// --- deferred: the uint32 vector, then the three strings ---
|
||||
if (activityCount <= DESCRIPTOR_MAX_ACTIVITY_IDS && activityCount <= remaining() / 4)
|
||||
{
|
||||
d.MemberRequirements.resize(memberCount);
|
||||
for (ListingMemberRequirement& m : d.MemberRequirements)
|
||||
{
|
||||
data >> m.Field0 >> m.Field1 >> m.Field2 >> m.Field3 >> m.Field4;
|
||||
m.Flag = data.ReadBits(1) != 0;
|
||||
data.ResetBitPos();
|
||||
}
|
||||
d.ActivityIDs.resize(activityCount);
|
||||
for (uint32& activityId : d.ActivityIDs)
|
||||
data >> activityId;
|
||||
}
|
||||
|
||||
// --- fixed activity fields ---
|
||||
data >> d.CategoryID; // uint32 @0x38: GroupFinderCategory id (68974: 1; the activities ride in the vector below)
|
||||
data >> d.RequiredDungeonScore; // float @0x3c
|
||||
data >> d.TrailingByte; // uint8 @0x702
|
||||
if (nameLength <= remaining() && nameLength < DESCRIPTOR_NAME_CAPACITY)
|
||||
d.Name.assign(data.ReadString(nameLength));
|
||||
if (commentLength <= remaining() && commentLength < DESCRIPTOR_COMMENT_CAPACITY)
|
||||
d.Comment.assign(data.ReadString(commentLength));
|
||||
if (voiceChatLength <= remaining() && voiceChatLength < DESCRIPTOR_VOICECHAT_CAPACITY)
|
||||
d.VoiceChat.assign(data.ReadString(voiceChatLength));
|
||||
|
||||
// --- trailing uint32 vector ---
|
||||
if (vectorCount <= remaining() / 4)
|
||||
{
|
||||
d.ActivityIDs.resize(vectorCount);
|
||||
for (uint32& v : d.ActivityIDs)
|
||||
data >> v;
|
||||
}
|
||||
|
||||
// --- string data (order matches the serializer: @0x40, @0x241, @0x642) ---
|
||||
if (str0Len <= remaining()) d.Name.assign(data.ReadString(str0Len));
|
||||
if (str1Len <= remaining()) d.VoiceChat.assign(data.ReadString(str1Len));
|
||||
if (str2Len <= remaining()) d.Comment.assign(data.ReadString(str2Len));
|
||||
|
||||
// --- present optional (nilable) numeric fields ---
|
||||
// --- deferred: the present optionals ---
|
||||
if (hasQuestId && remaining() >= 4) { uint32 v; data >> v; d.QuestID = v; }
|
||||
if (hasOpt1 && remaining() >= 4) { uint32 v; data >> v; d.OptionalValue1 = v; }
|
||||
if (hasOpt2 && remaining() >= 4) { uint32 v; data >> v; d.OptionalValue2 = v; }
|
||||
if (hasOpt3 && remaining() >= 1) { uint8 v; data >> v; d.OptionalValue3 = v; }
|
||||
if (hasRequiredDungeonScore && remaining() >= 4) { uint32 v; data >> v; d.RequiredDungeonScore = v; }
|
||||
if (hasRequiredPvpRating && remaining() >= 4) { uint32 v; data >> v; d.RequiredPvpRating = v; }
|
||||
if (hasPlaystyle && remaining() >= 1) { uint8 v; data >> v; d.Playstyle = v; }
|
||||
return data;
|
||||
}
|
||||
|
||||
ByteBuffer& operator<<(ByteBuffer& data, ListingInfo const& listing)
|
||||
static ByteBuffer& operator<<(ByteBuffer& data, ListingDescriptor const& d)
|
||||
{
|
||||
for (uint8 param : listing.Params)
|
||||
data << param;
|
||||
data << uint32(listing.ActivityID);
|
||||
data << uint32(listing.Field1);
|
||||
data << uint8(listing.Field2);
|
||||
data << uint32(listing.RequiredItemLevel);
|
||||
data << SizedString::BitsSize<10>(listing.Comment);
|
||||
data << SizedString::BitsSize<7>(listing.LeaderName);
|
||||
data << SizedString::BitsSize<7>(listing.VoiceChat);
|
||||
// The client copies each string into a fixed-size buffer and writes its own NUL at buf[len], so
|
||||
// capacity-1 is the largest length it can survive. ReadBytes on that side bounds-checks against the
|
||||
// remaining packet only, never against the target buffer - an over-long string is a client-side
|
||||
// memory overrun, so clamp here.
|
||||
std::size_t const nameLength = std::min<std::size_t>(d.Name.length(), DESCRIPTOR_NAME_CAPACITY - 1);
|
||||
std::size_t const commentLength = std::min<std::size_t>(d.Comment.length(), DESCRIPTOR_COMMENT_CAPACITY - 1);
|
||||
std::size_t const voiceChatLength = std::min<std::size_t>(d.VoiceChat.length(), DESCRIPTOR_VOICECHAT_CAPACITY - 1);
|
||||
std::size_t const activityCount = std::min<std::size_t>(d.ActivityIDs.size(), DESCRIPTOR_MAX_ACTIVITY_IDS);
|
||||
|
||||
data.WriteBits(activityCount, 5);
|
||||
data.WriteBits(nameLength, 10);
|
||||
data.WriteBits(commentLength, 11);
|
||||
data.WriteBits(voiceChatLength, 8);
|
||||
data << Bits<1>(d.IsAutoAccept); // +1699
|
||||
data << Bits<1>(d.IsPrivateGroup); // +1700
|
||||
data << Bits<1>(d.IsWarMode); // +1701
|
||||
data << Bits<1>(d.IsCrossFactionListing); // +1702
|
||||
data << OptionalInit(d.QuestID);
|
||||
data << OptionalInit(d.RequiredDungeonScore);
|
||||
data << OptionalInit(d.RequiredPvpRating);
|
||||
data << OptionalInit(d.Playstyle);
|
||||
data << Bits<1>(d.NewPlayerFriendly); // +1763
|
||||
data.FlushBits();
|
||||
data << SizedString::Data(listing.Comment);
|
||||
data << SizedString::Data(listing.LeaderName);
|
||||
data << SizedString::Data(listing.VoiceChat);
|
||||
data << uint32(listing.Field3);
|
||||
data << uint32(listing.Field4);
|
||||
data << uint32(listing.Field5);
|
||||
data << uint8(listing.Field6);
|
||||
|
||||
data << uint32(d.CategoryID);
|
||||
data << uint32(d.RequiredItemLevel);
|
||||
data << d.LeaderScore;
|
||||
data << uint8(d.GeneralPlaystyle);
|
||||
|
||||
for (std::size_t i = 0; i < activityCount; ++i)
|
||||
data << uint32(d.ActivityIDs[i]);
|
||||
|
||||
data.WriteString(d.Name.c_str(), nameLength);
|
||||
data.WriteString(d.Comment.c_str(), commentLength);
|
||||
data.WriteString(d.VoiceChat.c_str(), voiceChatLength);
|
||||
|
||||
if (d.QuestID)
|
||||
data << uint32(*d.QuestID);
|
||||
if (d.RequiredDungeonScore)
|
||||
data << uint32(*d.RequiredDungeonScore);
|
||||
if (d.RequiredPvpRating)
|
||||
data << uint32(*d.RequiredPvpRating);
|
||||
if (d.Playstyle)
|
||||
data << uint8(*d.Playstyle);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -115,17 +174,13 @@ namespace WorldPackets::LFGList
|
||||
|
||||
void LFGListJoin::Read()
|
||||
{
|
||||
std::size_t const start = _worldPacket.rpos();
|
||||
_worldPacket >> Listing;
|
||||
Listing.RawBytes.assign(_worldPacket.data() + start, _worldPacket.data() + _worldPacket.rpos());
|
||||
}
|
||||
|
||||
void LFGListUpdateRequest::Read()
|
||||
{
|
||||
_worldPacket >> Ticket;
|
||||
std::size_t const start = _worldPacket.rpos();
|
||||
_worldPacket >> Listing;
|
||||
Listing.RawBytes.assign(_worldPacket.data() + start, _worldPacket.data() + _worldPacket.rpos());
|
||||
}
|
||||
|
||||
void LFGListLeave::Read()
|
||||
@@ -133,35 +188,88 @@ namespace WorldPackets::LFGList
|
||||
_worldPacket >> Ticket;
|
||||
}
|
||||
|
||||
void LFGListSearch::Read()
|
||||
std::vector<std::vector<std::string>> LFGListSearch::GetKeywords() const
|
||||
{
|
||||
// Sniff-exact (43B no keyword / 56B with one): see header comment. All reads size-guarded.
|
||||
uint32 const termCount = _worldPacket.ReadBits(5);
|
||||
_worldPacket.ReadBit(); // presence/flag bit (semantics approximate)
|
||||
_worldPacket.ResetBitPos();
|
||||
|
||||
if (termCount)
|
||||
std::vector<std::vector<std::string>> keywords;
|
||||
keywords.reserve(Terms.size());
|
||||
for (LFGListSearchTerm const& term : Terms)
|
||||
{
|
||||
std::array<uint32, 10> lengths = { };
|
||||
for (uint32& len : lengths)
|
||||
len = _worldPacket.ReadBits(5); // ten bits(5) lengths packed into the 8-byte block
|
||||
_worldPacket.ReadBits(64 - 10 * 5); // padding to the full 8 bytes
|
||||
_worldPacket.ResetBitPos();
|
||||
std::vector<std::string> alternatives;
|
||||
for (std::string const& value : term.Values)
|
||||
if (!value.empty())
|
||||
alternatives.push_back(value);
|
||||
|
||||
SearchTerms.resize(std::min<uint32>(termCount, 10));
|
||||
for (std::size_t i = 0; i < SearchTerms.size(); ++i)
|
||||
if (lengths[i] && _worldPacket.rpos() + lengths[i] <= _worldPacket.size())
|
||||
SearchTerms[i] = _worldPacket.ReadString(lengths[i]);
|
||||
// A block with nothing in it constrains nothing. Keeping it would turn every search that carries a
|
||||
// trailing empty block into a search no listing can satisfy.
|
||||
if (!alternatives.empty())
|
||||
keywords.push_back(std::move(alternatives));
|
||||
}
|
||||
return keywords;
|
||||
}
|
||||
|
||||
for (uint32& f : Filters)
|
||||
_worldPacket >> f;
|
||||
_worldPacket >> FilterByte1; // observed 0xFF
|
||||
_worldPacket >> FilterByte2; // observed 0x05
|
||||
void LFGListSearch::Read()
|
||||
{
|
||||
// Client writer 0x757EC0; term blocks 0x757D00. See the header for the three defects this replaces.
|
||||
uint32 const termCount = _worldPacket.ReadBits(5);
|
||||
_worldPacket >> Bits<1>(CrossFaction);
|
||||
_worldPacket.ResetBitPos();
|
||||
|
||||
_worldPacket >> CategoryID;
|
||||
_worldPacket >> Filter;
|
||||
_worldPacket >> PreferredFilters;
|
||||
_worldPacket >> LanguageMask;
|
||||
uint32 resolvedActivityCount = 0;
|
||||
_worldPacket >> resolvedActivityCount;
|
||||
_worldPacket >> AdvancedFilterMask;
|
||||
uint32 activityGroupCount = 0;
|
||||
_worldPacket >> activityGroupCount;
|
||||
uint32 activityCount = 0;
|
||||
_worldPacket >> activityCount;
|
||||
_worldPacket >> MinimumRating;
|
||||
_worldPacket >> FilterByte1;
|
||||
_worldPacket >> FilterByte2;
|
||||
uint32 guidCount = 0;
|
||||
_worldPacket >> guidCount;
|
||||
if (guidCount <= 50)
|
||||
|
||||
// A term block is at least 8 bytes (the interleaved 60-bit header) - refuse counts the packet cannot
|
||||
// hold before allocating.
|
||||
auto remaining = [&]() -> std::size_t { return _worldPacket.size() - _worldPacket.rpos(); };
|
||||
if (termCount && termCount <= remaining() / 8)
|
||||
{
|
||||
Terms.resize(termCount);
|
||||
for (LFGListSearchTerm& term : Terms)
|
||||
{
|
||||
std::array<uint32, LFGListSearchTerm::MAX_VALUES> lengths = { };
|
||||
for (std::size_t i = 0; i < LFGListSearchTerm::MAX_VALUES; ++i)
|
||||
{
|
||||
lengths[i] = _worldPacket.ReadBits(5); // client buffer 32 -> ceil(log2(32)) = 5
|
||||
term.Flags[i] = _worldPacket.ReadBit(); // one presence bit per slot, interleaved
|
||||
}
|
||||
_worldPacket.ResetBitPos(); // 60 bits used, 4 padding -> 8 bytes
|
||||
|
||||
for (std::size_t i = 0; i < LFGListSearchTerm::MAX_VALUES; ++i)
|
||||
if (lengths[i] && lengths[i] < LFGListSearchTerm::MAX_VALUE_LENGTH && lengths[i] <= remaining())
|
||||
term.Values[i] = _worldPacket.ReadString(lengths[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto readValues = [&](std::vector<uint32>& out, uint32 count)
|
||||
{
|
||||
if (!count || count > remaining() / 4)
|
||||
return;
|
||||
|
||||
out.resize(count);
|
||||
for (uint32& value : out)
|
||||
_worldPacket >> value;
|
||||
};
|
||||
// Order on the wire is fixed by the client writer and is NOT the order the counts appear in: the
|
||||
// count of the first list sits at struct +48, i.e. between LanguageMask and AdvancedFilterMask,
|
||||
// while the list itself follows the term blocks. See the header for what each one carries.
|
||||
readValues(ResolvedActivityIDs, resolvedActivityCount);
|
||||
readValues(ActivityGroupIDs, activityGroupCount);
|
||||
readValues(ActivityIDs, activityCount);
|
||||
|
||||
if (guidCount && guidCount <= remaining() / 2) // a PackedGuid is at least its 2-byte mask
|
||||
{
|
||||
Guids.resize(guidCount);
|
||||
for (ObjectGuid& guid : Guids)
|
||||
@@ -174,7 +282,9 @@ namespace WorldPackets::LFGList
|
||||
_worldPacket >> Ticket;
|
||||
_worldPacket >> ActivityID;
|
||||
_worldPacket >> RoleMask;
|
||||
_worldPacket >> Field2;
|
||||
_worldPacket >> SizedString::BitsSize<8>(Comment);
|
||||
_worldPacket.ResetBitPos();
|
||||
_worldPacket >> SizedString::Data(Comment);
|
||||
}
|
||||
|
||||
void LFGListCancelApplication::Read()
|
||||
@@ -191,10 +301,21 @@ namespace WorldPackets::LFGList
|
||||
void LFGListInviteApplicant::Read()
|
||||
{
|
||||
_worldPacket >> Ticket;
|
||||
_worldPacket >> ListingId;
|
||||
_worldPacket >> ApplicantGuid;
|
||||
_worldPacket >> RoleMask;
|
||||
_worldPacket >> ApplicantTicket;
|
||||
|
||||
uint32 inviteeCount = 0;
|
||||
_worldPacket >> inviteeCount;
|
||||
// Each invitee is at least 3 bytes (2-byte guid mask + role); a group cannot exceed MAX_RAID_SIZE
|
||||
// anyway, so a large count is malformed either way.
|
||||
if (inviteeCount && inviteeCount <= (_worldPacket.size() - _worldPacket.rpos()) / 3)
|
||||
{
|
||||
Invitees.resize(inviteeCount);
|
||||
for (LFGListInvitee& invitee : Invitees)
|
||||
{
|
||||
_worldPacket >> invitee.Guid;
|
||||
_worldPacket >> invitee.RoleMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LFGListInviteResponse::Read()
|
||||
@@ -204,262 +325,326 @@ namespace WorldPackets::LFGList
|
||||
_worldPacket.ResetBitPos();
|
||||
}
|
||||
|
||||
void LFGListConfirmCensoredActiveEntry::Read()
|
||||
{
|
||||
_worldPacket >> Ticket;
|
||||
}
|
||||
|
||||
// ---- SMSG Write ----
|
||||
|
||||
WorldPacket const* LFGListJoinResult::Write()
|
||||
{
|
||||
_worldPacket << Ticket;
|
||||
_worldPacket << uint32(Status);
|
||||
_worldPacket << uint8(Result);
|
||||
_worldPacket << uint8(ResultDetail);
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListUpdateStatus::Write()
|
||||
{
|
||||
// Byte order re-derived from the 12.0.7.69587 create sniff (feed 0x5A000A, idx 44791/44792/44912).
|
||||
// The previous writer (Ticket, ExpirationTime, Status, Descriptor, one Listed bit) is WRONG for this
|
||||
// build: the descriptor comes straight after the ticket, and the tail is a per-status flag byte, the
|
||||
// owner's PackedGuid and an optional trailing 0x00. Byte-exact for the statuses we emit:
|
||||
// 0x06 listed (create) : ticket 27B | desc 39B | u64 expiry | 06 | C0 | owner guid (85B)
|
||||
// 0x38 listed (steady) : ticket 27B | desc 39B | u64 expiry | 38 | E0 | owner guid | 00 (86B)
|
||||
// 0x08 delisted : ticket 27B | desc 27x00 | u64 0 | 08 | 60 | owner guid | 00 (74B)
|
||||
static constexpr std::size_t EMPTY_DESCRIPTOR_SIZE = 27;
|
||||
|
||||
_worldPacket << Ticket;
|
||||
if (Listed && !RawDescriptor.empty())
|
||||
_worldPacket.append(RawDescriptor.data(), RawDescriptor.size());
|
||||
else
|
||||
_worldPacket.append(std::vector<uint8>(EMPTY_DESCRIPTOR_SIZE, 0).data(), EMPTY_DESCRIPTOR_SIZE);
|
||||
_worldPacket << Listing; // 12.1: directly behind the ticket, not at the end
|
||||
_worldPacket << uint64(Listed ? ExpirationTime : 0);
|
||||
_worldPacket << uint8(Status);
|
||||
_worldPacket << Bits<1>(Listed);
|
||||
_worldPacket << OptionalInit(LeaderGuid);
|
||||
_worldPacket << OptionalInit(UnkByte);
|
||||
_worldPacket.FlushBits();
|
||||
if (LeaderGuid)
|
||||
_worldPacket << *LeaderGuid;
|
||||
if (UnkByte)
|
||||
_worldPacket << uint8(*UnkByte);
|
||||
|
||||
// Tail flag byte + trailing 0x00 are status-dependent (sniff MSB-first bit-flush remnants, reproduced
|
||||
// verbatim: 0x06 -> 0xC0 no trailing, 0x38 -> 0xE0 + 0x00, 0x08 -> 0x60 + 0x00).
|
||||
uint8 tailFlags = 0xE0;
|
||||
bool tail00 = true;
|
||||
switch (Status)
|
||||
{
|
||||
case 0x06: tailFlags = 0xC0; tail00 = false; break;
|
||||
case 0x08: tailFlags = 0x60; break;
|
||||
default: tailFlags = 0xE0; break; // 0x38 and unknown statuses
|
||||
}
|
||||
|
||||
_worldPacket << uint8(tailFlags);
|
||||
_worldPacket << PlayerGuid; // PackedGuid, the listing owner
|
||||
if (tail00)
|
||||
_worldPacket << uint8(0);
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListUpdateExpiration::Write()
|
||||
{
|
||||
_worldPacket << Ticket;
|
||||
_worldPacket << uint64(ExpirationTime);
|
||||
_worldPacket << uint8(Reason);
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListCensoredActiveEntryUpdate::Write()
|
||||
{
|
||||
// 69587 create sniff (idx 44793): 41B = the 39-byte listing descriptor echoed verbatim + 0x80 + 0x00.
|
||||
// The 0x80 byte mirrors the "listed" marker; semantics otherwise unknown, bytes cited verbatim.
|
||||
if (!RawDescriptor.empty())
|
||||
_worldPacket.append(RawDescriptor.data(), RawDescriptor.size());
|
||||
_worldPacket << uint8(0x80);
|
||||
_worldPacket << uint8(0x00);
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListSearchStatus::Write()
|
||||
{
|
||||
_worldPacket << Ticket;
|
||||
_worldPacket << uint8(Status);
|
||||
_worldPacket << Bits<1>(Complete);
|
||||
_worldPacket.FlushBits();
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
// One MemberDetail record (head sub_7FF7291DBF80 + tail sub_7FF729162CC0), shared between the full
|
||||
// search-result row and the compact SEARCH_RESULTS_UPDATE row (68974: byte-identical in both).
|
||||
// Head decoded from the 68974 capture: guid, level, class, role (0 tank/1 healer/2 dps), spec; the head
|
||||
// flag bit was set on both retail members (each was the listing's leader).
|
||||
// One member record, reader 0x7FF7CD528140 (see SearchResultMember).
|
||||
static void WriteSearchResultMember(ByteBuffer& data, SearchResultMember const& member)
|
||||
{
|
||||
data << member.Guid; // PackedGuid MemberGuid
|
||||
data << member.Guid;
|
||||
data << uint8(member.Level);
|
||||
data << uint8(member.ClassID);
|
||||
data << uint8(member.Role);
|
||||
data << uint32(member.SpecID);
|
||||
data << uint8(0); // Unk24
|
||||
data << uint8(member.IsLeader ? 0x80 : 0x00); // head flag (bit-as-byte; 68974: 1 for the leader)
|
||||
// tail (sub_7FF729162CC0)
|
||||
data << member.Guid; // PackedGuid MemberGuid2
|
||||
data << uint32(0); // T20
|
||||
data << uint32(0); // T24 (68974 live values 17; semantics unknown, zero-filled)
|
||||
data << uint32(0); // T28
|
||||
data << uint32(0); // T32 (68974 live values 18)
|
||||
data << uint32(0); // T36 (68974 live values 18)
|
||||
data << uint64(0); // T40
|
||||
data << uint64(0); // T48
|
||||
data << uint32(0); // T56
|
||||
data << uint8(0); // T_flag (bit-as-byte)
|
||||
}
|
||||
data << uint8(member.LfgRoles);
|
||||
|
||||
// Emit one SMSG_LFG_LIST_SEARCH_RESULTS row per c:\dumps\lfg_search_results_layout.md, re-verified byte-exact
|
||||
// against the 12.0.7.68974 capture (both bodies consume to exact end with the same layout ? no structural
|
||||
// drift 68275 -> 68974). Row-level "bit" fields are full wire bytes with the boolean in bit 7 (client reads
|
||||
// x >> 7); PackedGuid uses the standard TrinityCore ObjectGuid operator<< (u16 mask + data bytes). Unknown
|
||||
// scalars are zero-filled (the client parses them fine); observed retail constants are mirrored
|
||||
// (Unk_b=4, Unk1816/Unk2160=3 at 68974 ? they were 5 at 68275).
|
||||
// The row body from the age counter onward ? shared verbatim between SEARCH_RESULTS rows and the row
|
||||
// snapshot embedded in SMSG_LFG_LIST_APPLY_TO_GROUP_RESULT (sniff: identical bytes in both containers).
|
||||
static void WriteSearchResultRowBody(ByteBuffer& data, SearchResultListing const& row)
|
||||
{
|
||||
data << uint32(row.Age); // Unk40 (age counter; 68974 rows: 3)
|
||||
data << uint8(3); // Unk1816 (68974: 3; was 5 at 68275)
|
||||
data << row.LeaderGuid; // Guid_A
|
||||
data << row.LeaderGuid; // Guid_B
|
||||
data << row.LeaderGuid; // Guid_C
|
||||
data << row.LeaderGuid; // Guid_D
|
||||
data << row.LeaderGuid; // Guid_E
|
||||
data << uint32(0); // Unk1904
|
||||
data << uint32(0); // Unk1908
|
||||
data << uint32(0); // Unk1912
|
||||
data << uint32(0); // Count1 (GuidList1 length)
|
||||
data << uint32(0); // Count2 (GuidList2 length)
|
||||
data << uint32(0); // Count3 (GuidList3 length)
|
||||
data << uint32(uint32(row.Members.size())); // MemberCount
|
||||
data << uint32(0); // Unk2016
|
||||
data << uint64(row.PostTime); // PostTime2 (== PostTime)
|
||||
data << uint8(0); // Unk2032
|
||||
data << row.GroupGuid; // LeaderGuidEcho (== GroupGuid)
|
||||
for (uint32 i = 0; i < 9; ++i) // fixed 9-entry {u32,u8} table (sub_7FF729195220)
|
||||
{
|
||||
// 68974 capture: every entry is {u32 0, u8 index} ? the previous writer emitted {u32 index, u8 0},
|
||||
// which put the index into the wrong client field (same byte count, wrong values).
|
||||
// tail block (RVA 0x6E7EA0): leaver bookkeeping keyed by the Battle.net account
|
||||
data << member.BnetAccountGuid;
|
||||
data << uint32(0);
|
||||
data << uint8(i);
|
||||
}
|
||||
data << uint8(3); // Unk2160 (68974: 3; was 5 at 68275)
|
||||
data << uint8(0); // Unk2161
|
||||
data << uint32(0);
|
||||
data << uint32(0);
|
||||
data << uint32(0);
|
||||
data << uint32(0);
|
||||
data << uint64(0);
|
||||
data << uint64(0);
|
||||
data << uint32(0);
|
||||
data << Bits<1>(member.IsLeaver);
|
||||
data.FlushBits();
|
||||
|
||||
// === three PackedGuid lists (all empty, Count1/2/3 == 0) -> emit nothing ===
|
||||
|
||||
// === embedded ListingDescriptor (echo verbatim) ===
|
||||
if (!row.RawDescriptor.empty())
|
||||
data.append(row.RawDescriptor.data(), row.RawDescriptor.size());
|
||||
else
|
||||
data.append(std::vector<uint8>(27, 0).data(), 27); // minimal all-zero descriptor fallback
|
||||
|
||||
// === trailing bit ===
|
||||
data << uint8(0); // Unk1916 (bit-as-byte, observed 0)
|
||||
|
||||
// === block sub_7FF7291676F0 @2056 (empty) ===
|
||||
data << uint32(0); // Blk_f0
|
||||
data << uint32(0); // Blk_f1
|
||||
data << uint32(0); // Blk_count == 0
|
||||
|
||||
// === member detail list x MemberCount (sub_7FF7291DBF80 + tail sub_7FF729162CC0) ===
|
||||
for (SearchResultMember const& member : row.Members)
|
||||
WriteSearchResultMember(data, member);
|
||||
data << Bits<1>(member.IsLeader);
|
||||
data.FlushBits();
|
||||
}
|
||||
|
||||
// Emit one full row: header block (sub_7FF7291CCDB0) + body.
|
||||
// The descriptor as a search row carries it: the leader's score rides in the row (+2056), the embedded copy is empty in
|
||||
// every retail row.
|
||||
static ListingDescriptor RowDescriptor(SearchResultListing const& row)
|
||||
{
|
||||
ListingDescriptor descriptor = row.Listing;
|
||||
descriptor.LeaderScore = { };
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
// One SMSG_LFG_LIST_SEARCH_RESULTS row, reader 0x7FF7CD528370 (see SearchResultListing).
|
||||
static ByteBuffer& operator<<(ByteBuffer& data, SearchResultListing const& row)
|
||||
{
|
||||
data << row.GroupGuid; // PackedGuid GroupGuid
|
||||
data << uint32(row.ListingId); // ListingId (APPLY_TO_GROUP key)
|
||||
data << uint32(4); // Unk_b (observed constant 4)
|
||||
data << uint64(row.PostTime); // PostTime
|
||||
data << uint8(0); // Unk_hdrbit (bit-as-byte, observed 0)
|
||||
WriteSearchResultRowBody(data, row);
|
||||
data << row.GroupGuid;
|
||||
data << uint32(row.ListingId);
|
||||
data << uint32(4); // RideType::LfgListListing
|
||||
data << uint64(row.PostTime);
|
||||
data << Bits<1>(false); // IsCrossFaction
|
||||
data.FlushBits();
|
||||
|
||||
data << uint32(row.Revision);
|
||||
data << RowDescriptor(row);
|
||||
// Not read by any 12.1 consumer of the row; 7 in all 105 retail 12.1 rows.
|
||||
data << uint8(7);
|
||||
data << row.LeaderGuid;
|
||||
data << row.LastEditorGuid;
|
||||
data << row.NameEditorGuid;
|
||||
data << row.CommentEditorGuid;
|
||||
data << row.VoiceChatEditorGuid;
|
||||
data << uint32(row.LeaderVirtualRealmAddress);
|
||||
data << uint32(row.LeaderAreaID);
|
||||
data << uint32(0); // +1912: only ever set by an update record's flag bit
|
||||
data << uint32(row.BNetFriendGuids.size());
|
||||
data << uint32(row.CharacterFriendGuids.size());
|
||||
data << uint32(row.GuildMateGuids.size());
|
||||
data << uint32(row.Members.size());
|
||||
data << uint32(0); // +2016: only ever set by an update record
|
||||
data << uint64(row.PostTime);
|
||||
data << uint8(0); // +2032: no 12.1 consumer; 0 in 93 of 105 retail rows
|
||||
data << row.GroupGuid;
|
||||
data << row.LeaderScore;
|
||||
for (uint32 bracket = 0; bracket < row.LeaderPvpRatings.size(); ++bracket)
|
||||
{
|
||||
data << uint32(row.LeaderPvpRatings[bracket]);
|
||||
data << uint8(bracket);
|
||||
}
|
||||
data << uint8(row.LeaderFactionMask);
|
||||
data << uint8(row.CensorFlags);
|
||||
|
||||
for (ObjectGuid const& guid : row.BNetFriendGuids)
|
||||
data << guid;
|
||||
for (ObjectGuid const& guid : row.CharacterFriendGuids)
|
||||
data << guid;
|
||||
for (ObjectGuid const& guid : row.GuildMateGuids)
|
||||
data << guid;
|
||||
|
||||
for (SearchResultMember const& member : row.Members)
|
||||
WriteSearchResultMember(data, member);
|
||||
|
||||
data << Bits<1>(row.HasSelf);
|
||||
data.FlushBits();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListSearchResults::Write()
|
||||
{
|
||||
_worldPacket << uint16(Listings.size()); // Unk32 (duplicate row-count hint; == RowCount in sniffs)
|
||||
_worldPacket << uint32(Listings.size()); // RowCount (array length)
|
||||
_worldPacket << uint16(Listings.size()); // duplicate row-count hint (== RowCount in every sniff)
|
||||
_worldPacket << uint32(Listings.size());
|
||||
for (SearchResultListing const& row : Listings)
|
||||
_worldPacket << row;
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
// One update record, reader 0x7FF7CD528690 (see LFGListSearchResultsUpdate).
|
||||
WorldPacket const* LFGListSearchResultsUpdate::Write()
|
||||
{
|
||||
// 68974 capture (bodies idx 16193 len=69 / idx 18313 len=136): the UPDATE row is NOT the full
|
||||
// search-result row (the previous writer emitted the full ~285B row ? wrong wire). Observed compact row:
|
||||
// PackedGuid GroupGuid, u32 ListingId, u32 4, u64 PostTime, bit(0),
|
||||
// u32 Age (3 / 4), u32 MemberCount (0 / 1),
|
||||
// u8 0, u32 8 (constant in both bodies), u8[26] zero,
|
||||
// MemberDetail x MemberCount (identical 66B record as SEARCH_RESULTS).
|
||||
_worldPacket << uint32(Listings.size());
|
||||
for (SearchResultListing const& row : Listings)
|
||||
{
|
||||
_worldPacket << row.GroupGuid;
|
||||
_worldPacket << uint32(row.ListingId);
|
||||
_worldPacket << uint32(4); // header constant (== full-row Unk_b)
|
||||
_worldPacket << uint32(4); // RideType::LfgListListing
|
||||
_worldPacket << uint64(row.PostTime);
|
||||
_worldPacket << uint8(0); // header bit (bit-as-byte, observed 0)
|
||||
_worldPacket << uint32(row.Age); // refresh/age counter (matches the row's Unk40)
|
||||
_worldPacket << Bits<1>(false);
|
||||
_worldPacket.FlushBits();
|
||||
|
||||
_worldPacket << uint32(row.Revision);
|
||||
_worldPacket << uint32(row.Members.size());
|
||||
_worldPacket << RowDescriptor(row);
|
||||
_worldPacket << uint8(0);
|
||||
_worldPacket << uint32(8); // observed constant 8 in both 68974 bodies
|
||||
for (uint32 i = 0; i < 26; ++i)
|
||||
_worldPacket << uint8(0); // zero block (semantics unknown, all-zero in both bodies)
|
||||
|
||||
for (SearchResultMember const& member : row.Members)
|
||||
WriteSearchResultMember(_worldPacket, member);
|
||||
|
||||
uint32 const changes = row.Changes;
|
||||
bool const leader = (changes & SEARCH_RESULT_CHANGE_LEADER) != 0;
|
||||
bool const name = (changes & SEARCH_RESULT_CHANGE_NAME) != 0;
|
||||
bool const comment = (changes & SEARCH_RESULT_CHANGE_COMMENT) != 0;
|
||||
bool const voiceChat = (changes & SEARCH_RESULT_CHANGE_VOICE_CHAT) != 0;
|
||||
|
||||
_worldPacket << Bits<1>(leader);
|
||||
_worldPacket << Bits<1>(leader);
|
||||
_worldPacket << Bits<1>(false); // flag +1912 present
|
||||
_worldPacket << Bits<1>(false); // u32 +2016 present
|
||||
_worldPacket << Bits<1>(row.Delisted);
|
||||
_worldPacket << Bits<1>(row.Delisted);
|
||||
_worldPacket << Bits<1>(false); // guid the applier does not read
|
||||
_worldPacket << Bits<1>(name);
|
||||
_worldPacket << Bits<1>(comment);
|
||||
_worldPacket << Bits<1>(voiceChat);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_REQUIRED_ITEM_LEVEL) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_AUTO_ACCEPT) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_PRIVATE) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_REQUIRED_DUNGEON_SCORE) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_REQUIRED_PVP_RATING) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_PLAYSTYLE) != 0);
|
||||
_worldPacket << Bits<1>(false); // not read by the applier
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_CROSS_FACTION) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_ACTIVITIES) != 0);
|
||||
_worldPacket << Bits<1>((changes & SEARCH_RESULT_CHANGE_NEW_PLAYER_FRIENDLY) != 0);
|
||||
_worldPacket << Bits<1>(false); // flag +1912 value
|
||||
_worldPacket.FlushBits();
|
||||
|
||||
if (leader)
|
||||
{
|
||||
_worldPacket << row.LeaderGuid;
|
||||
_worldPacket << uint32(row.LeaderVirtualRealmAddress);
|
||||
}
|
||||
if (name)
|
||||
_worldPacket << row.NameEditorGuid;
|
||||
if (comment)
|
||||
_worldPacket << row.CommentEditorGuid;
|
||||
if (voiceChat)
|
||||
_worldPacket << row.VoiceChatEditorGuid;
|
||||
}
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListCensoredActiveEntryUpdate::Write()
|
||||
{
|
||||
_worldPacket << Listing;
|
||||
_worldPacket << OptionalInit(CensorCode);
|
||||
_worldPacket.FlushBits();
|
||||
if (CensorCode)
|
||||
_worldPacket << uint8(*CensorCode);
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListApplicantListUpdate::Write()
|
||||
{
|
||||
// Sniff-exact: Ticket(listing) + u32 count + u32 unk + entries. Entry (status-only form, HasInfo=0):
|
||||
// Ticket(application) + PackedGuid player + u32 HasInfo(0) + u8 StateBits + u8 pad.
|
||||
_worldPacket << ListingTicket;
|
||||
_worldPacket << uint32(Applicants.size());
|
||||
_worldPacket << uint32(Unknown);
|
||||
for (ApplicantInfo const& a : Applicants)
|
||||
_worldPacket << Size<uint32>(Applicants);
|
||||
_worldPacket << uint32(Unknown); // UNVERIFIED: see the field's note in LFGListPackets.h
|
||||
for (ApplicantInfo const& applicant : Applicants)
|
||||
{
|
||||
_worldPacket << a.Ticket;
|
||||
_worldPacket << a.PlayerGuid;
|
||||
_worldPacket << uint32(0); // HasInfo: 0 = status-only entry (full snapshot form documented, unresolved scalars)
|
||||
_worldPacket << uint8(a.StateBits);
|
||||
_worldPacket << uint8(0); // pad
|
||||
_worldPacket << applicant.Ticket;
|
||||
_worldPacket << applicant.PlayerGuid;
|
||||
_worldPacket << uint32(applicant.Members.size());
|
||||
for (ApplicantMember const& member : applicant.Members)
|
||||
{
|
||||
_worldPacket << member.Guid;
|
||||
_worldPacket << uint32(member.VirtualRealmAddress);
|
||||
_worldPacket << uint32(member.Level);
|
||||
_worldPacket << uint32(member.HonorLevel);
|
||||
_worldPacket << uint8(member.RoleMask);
|
||||
_worldPacket << uint8(member.AssignedRole);
|
||||
_worldPacket << uint32(0); // {u32, u32} pairs: no consumer among the applicant getters
|
||||
_worldPacket << member.DungeonScore;
|
||||
for (uint32 bracket = 0; bracket < member.PvpRatings.size(); ++bracket)
|
||||
{
|
||||
_worldPacket << uint32(member.PvpRatings[bracket]);
|
||||
_worldPacket << uint8(bracket);
|
||||
}
|
||||
_worldPacket << uint8(member.RaceID);
|
||||
_worldPacket << uint8(member.FactionMask);
|
||||
_worldPacket << member.BnetAccountGuid;
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << uint64(0);
|
||||
_worldPacket << uint64(0);
|
||||
_worldPacket << uint32(0);
|
||||
_worldPacket << Bits<1>(member.IsLeaver);
|
||||
_worldPacket.FlushBits();
|
||||
_worldPacket << float(member.ItemLevel);
|
||||
_worldPacket << float(member.PvpItemLevel);
|
||||
_worldPacket << uint32(member.SpecID);
|
||||
}
|
||||
// 12.1: the 13-bit block sits behind the member array. Written out bit by bit rather than as two
|
||||
// hand-packed bytes so it stays correct if the member list is ever filled.
|
||||
_worldPacket.WriteBits(applicant.StateBits >> 4, 4);
|
||||
_worldPacket << Bits<1>(applicant.CommentUpdated);
|
||||
_worldPacket << SizedString::BitsSize<8>(applicant.Comment);
|
||||
_worldPacket.FlushBits();
|
||||
_worldPacket << SizedString::Data(applicant.Comment);
|
||||
}
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListApplicationStatusUpdate::Write()
|
||||
{
|
||||
_worldPacket << Ticket;
|
||||
_worldPacket << uint64(0);
|
||||
_worldPacket << ListingTicket; // 12.1: pulled forward, adjacent to the first ticket
|
||||
_worldPacket << uint64(ApplicationExpiration);
|
||||
_worldPacket << uint32(UnkResult);
|
||||
_worldPacket << uint8(RoleGranted);
|
||||
_worldPacket << ListingTicket;
|
||||
_worldPacket << uint8(StateBits);
|
||||
_worldPacket << uint8(StateBits); // client keeps bits 7..4 only
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListApplyToGroupResult::Write()
|
||||
{
|
||||
_worldPacket << Ticket;
|
||||
_worldPacket << ListingTicket;
|
||||
_worldPacket << Row; // 12.1: the row moved in front of the scalar tail
|
||||
_worldPacket << uint64(ApplicationExpiration);
|
||||
_worldPacket << uint8(Status);
|
||||
_worldPacket << uint8(0);
|
||||
_worldPacket << ListingTicket;
|
||||
_worldPacket << uint8(0x10); // observed constant
|
||||
_worldPacket << ListingTicket;
|
||||
WriteSearchResultRowBody(_worldPacket, Row);
|
||||
_worldPacket << uint8(RoleGranted);
|
||||
_worldPacket << uint8(StateBits); // client keeps bits 7..4 only
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* LFGListUpdateBlacklist::Write()
|
||||
{
|
||||
_worldPacket << uint32(Entries.size());
|
||||
_worldPacket << Size<uint32>(Entries);
|
||||
for (LFGListBlacklistEntry const& entry : Entries)
|
||||
{
|
||||
_worldPacket << uint32(entry.ActivityID);
|
||||
_worldPacket << uint32(entry.Reason);
|
||||
}
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -649,7 +649,7 @@ void OpcodeTable::InitializeClientOpcodes()
|
||||
DEFINE_HANDLER(CMSG_LEAVE_PET_BATTLE_QUEUE, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLeavePetBattleQueue);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_APPLY_TO_GROUP, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListApplyToGroup);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_CANCEL_APPLICATION, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListCancelApplication);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_CONFIRM_CENSORED_ACTIVE_ENTRY, STATUS_UNHANDLED, PROCESS_THREADUNSAFE, &WorldSession::Handle_NULL);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_CONFIRM_CENSORED_ACTIVE_ENTRY, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListConfirmCensoredActiveEntry);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_DECLINE_APPLICANT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListDeclineApplicant);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_GET_STATUS, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListGetStatus);
|
||||
DEFINE_HANDLER(CMSG_LFG_LIST_INVITE_APPLICANT, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleLFGListInviteApplicant);
|
||||
|
||||
@@ -774,6 +774,7 @@ namespace WorldPackets
|
||||
class LFGListDeclineApplicant;
|
||||
class LFGListInviteApplicant;
|
||||
class LFGListInviteResponse;
|
||||
class LFGListConfirmCensoredActiveEntry;
|
||||
class RequestLFGListBlacklist;
|
||||
}
|
||||
|
||||
@@ -2322,8 +2323,10 @@ public:
|
||||
void HandleLFGListDeclineApplicant(WorldPackets::LFGList::LFGListDeclineApplicant& packet);
|
||||
void HandleLFGListInviteApplicant(WorldPackets::LFGList::LFGListInviteApplicant& packet);
|
||||
void HandleLFGListInviteResponse(WorldPackets::LFGList::LFGListInviteResponse& packet);
|
||||
void HandleLFGListConfirmCensoredActiveEntry(WorldPackets::LFGList::LFGListConfirmCensoredActiveEntry& packet);
|
||||
void HandleRequestLFGListBlacklist(WorldPackets::LFGList::RequestLFGListBlacklist& packet);
|
||||
void SendLFGListUpdateStatus(uint32 listingId, uint8 status = 0x38);
|
||||
void SendLFGListCensoredActiveEntryUpdate(uint32 listingId);
|
||||
|
||||
void SendLfgUpdateStatus(lfg::LfgUpdateData const& updateData, bool party);
|
||||
void SendLfgRoleChosen(ObjectGuid guid, uint8 roles);
|
||||
|
||||
Reference in New Issue
Block a user