From 75d86216c7b9aa61b102e24ba2094e11f2911ba8 Mon Sep 17 00:00:00 2001 From: MadMax Date: Wed, 15 Jul 2026 01:56:08 +0100 Subject: [PATCH] feat(ahworker): own bidder-list result assembly --- src/game/AuctionHouseBot/BrowsePending.h | 1 - .../WorldHandlers/AuctionHouseHandler.cpp | 17 --- src/game/WorldHandlers/World.cpp | 63 +---------- src/mangosd/MangosdTest.cpp | 70 +++++++----- src/modules/AhWorker/BrowseHandler.cpp | 107 ++++++++++++++++-- src/modules/AhWorker/BrowseHandler.h | 6 + src/modules/AhWorker/Main.cpp | 78 +++++++++++++ 7 files changed, 226 insertions(+), 116 deletions(-) diff --git a/src/game/AuctionHouseBot/BrowsePending.h b/src/game/AuctionHouseBot/BrowsePending.h index b77e9bc06..17958bd5c 100644 --- a/src/game/AuctionHouseBot/BrowsePending.h +++ b/src/game/AuctionHouseBot/BrowsePending.h @@ -53,7 +53,6 @@ struct PendingBrowse uint8 deferEluna; uint32 listfrom; std::wstring wname; ///< lower-cased UTF-16 (in-process Utf8FitTo) - std::vector clientOutbidIds; ///< BIDDER client outbid ids (client order) }; class BrowsePendingMap diff --git a/src/game/WorldHandlers/AuctionHouseHandler.cpp b/src/game/WorldHandlers/AuctionHouseHandler.cpp index 7a0ca8b23..e4bd94f4d 100644 --- a/src/game/WorldHandlers/AuctionHouseHandler.cpp +++ b/src/game/WorldHandlers/AuctionHouseHandler.cpp @@ -1612,22 +1612,6 @@ static void AhBuildKnownRecipeCastSpells(Player* player, std::vector& ou } } -// V8: re-resolve the AuctionHouseObject for a stored pending request, mirroring -// GetAuctionsMap by team. pb.house: 0=ALLIANCE,1=HORDE,2=NEUTRAL (== the -// AuctionHouseType values); allHouses => the neutral house. Non-static so the -// world thread's IPC_BROWSE_RESULT reply branch can use it for BIDDER prepends. -AuctionHouseObject* AhResolveHouse(const PendingBrowse& pb) -{ - AuctionHouseType t = AUCTION_HOUSE_NEUTRAL; - if (pb.allHouses == 0u) - { - t = (pb.house == 0u) ? AUCTION_HOUSE_ALLIANCE - : (pb.house == 1u) ? AUCTION_HOUSE_HORDE - : AUCTION_HOUSE_NEUTRAL; - } - return sAuctionMgr.GetAuctionsMap(t); -} - // Coordinator model: the worker is the AH read authority. When it cannot serve // (down, a transient register/encode/send failure, queue-full, oversize, or a // reply timeout) mangosd serves NOTHING in-process. It sends an empty list so @@ -1713,7 +1697,6 @@ void WorldSession::HandleAuctionListBidderItems(WorldPacket& recv_data) pb.deferEluna = 0u; pb.listfrom = listfrom; pb.wname.clear(); - pb.clientOutbidIds = clientOutbidIds; pb.seq = sWorld.GetBrowsePending().NextSeqFor(pb.playerGuidLow, pb.kind); BrowseQuery q; diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index aa0c757da..7e4ba566f 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -130,12 +130,10 @@ INSTANTIATE_SINGLETON_1(World); extern void LoadGameObjectModelList(); -// SP-1 coordinator: the "AH unavailable" responder + house re-resolution (for -// the BIDDER outbid-prepend), defined in AuctionHouseHandler.cpp (next to the -// three async-proxy handlers). The world thread calls these from the -// IPC_BROWSE_RESULT reply branch and the TTL sweep. +// SP-1 coordinator: the "AH unavailable" responder, defined in +// AuctionHouseHandler.cpp next to the three async-proxy handlers. The world +// thread calls it from the IPC_BROWSE_RESULT reply branch and the TTL sweep. void AhSendBrowseUnavailable(WorldSession* session, uint8 kind); -AuctionHouseObject* AhResolveHouse(const PendingBrowse& pb); volatile bool World::m_stopEvent = false; uint8 World::m_ExitCode = SHUTDOWN_EXIT_CODE; @@ -1665,61 +1663,6 @@ void World::HandleAhInbound(const IpcMessage& msg) opcode = SMSG_AUCTION_BIDDER_LIST_RESULT; } - if (pb.kind == uint8(BROWSE_BIDDER)) - { - // I1: BIDDER prepends the client-supplied outbid entries in CLIENT - // ORDER (mirroring HandleAuctionListBidderItems) before the worker - // bidder-sweep entries. The client outbid rows are resolved live - // from the player's house via GetAuction(id)->BuildAuctionInfo. - AuctionHouseObject* house = AhResolveHouse(pb); - ByteBuffer rows; - uint32 prependCount = 0; - if (house) - { - for (size_t i = 0; i < pb.clientOutbidIds.size(); ++i) - { - AuctionEntry* ae = house->GetAuction(pb.clientOutbidIds[i]); - if (!ae) - { - continue; - } - WorldPacket one(opcode, 60); - if (ae->BuildAuctionInfo(one)) - { - rows.append(one.contents(), one.size()); - ++prependCount; - } - } - } - // Assemble: count(prepend+sweep) | client rows | sweep rows | total - WorldPacket data(opcode, 4 + rows.size() + finalEntries.size() * 60 + 4); - data << uint32(prependCount + uint32(finalEntries.size())); - if (rows.size()) - { - data.append(rows.contents(), rows.size()); - } - ByteBuffer sweepRows; - for (size_t i = 0; i < finalEntries.size(); ++i) - { - const BrowseEntry& e = finalEntries[i]; - sweepRows << uint32(e.id) << uint32(e.itemEntry) << uint32(e.enchantId) - << uint32(e.randomPropId) << uint32(e.suffixFactor) - << uint32(e.count) << uint32(e.charges) - << ObjectGuid(HIGHGUID_PLAYER, e.ownerGuidLow) - << uint32(e.startbid) << uint32(e.outbid) << uint32(e.buyout) - << uint32(e.timeLeftMs) - << ObjectGuid(HIGHGUID_PLAYER, e.bidderGuidLow) - << uint32(e.curBid); - } - if (sweepRows.size()) - { - data.append(sweepRows.contents(), sweepRows.size()); - } - data << uint32(totalcount + prependCount); - session->SendPacket(&data); - break; - } - WorldPacket data(opcode, 4 + 4 + finalEntries.size() * 60); ByteBuffer assembled; AhAssembleBrowseListBody(finalEntries, totalcount, assembled); diff --git a/src/mangosd/MangosdTest.cpp b/src/mangosd/MangosdTest.cpp index 2fae5f18a..a3d4fc410 100644 --- a/src/mangosd/MangosdTest.cpp +++ b/src/mangosd/MangosdTest.cpp @@ -1193,40 +1193,54 @@ static int RunAhBrowsePendingTest() return 1; } - // SMSG assembly: count, first entry id, totalcount appear in order. + // SMSG assembly preserves the worker's complete bidder order, including a + // duplicate auction id, and writes the worker-provided totalcount. { std::vector entries; - BrowseEntry e; e.id=42u; e.itemEntry=19019u; e.enchantId=0u; e.randomPropId=0u; - e.suffixFactor=0u; e.count=1u; e.charges=0; e.ownerGuidLow=4u; e.startbid=100u; - e.outbid=0u; e.buyout=5000u; e.timeLeftMs=720000u; e.bidderGuidLow=0u; e.curBid=100u; - entries.push_back(e); + const uint32 orderedIds[] = { 9u, 3u, 9u, 7u }; + for (size_t i = 0; i < 4u; ++i) + { + BrowseEntry e; + e.id=orderedIds[i]; e.itemEntry=19019u; e.enchantId=0u; + e.randomPropId=0u; e.suffixFactor=0u; e.count=1u; + e.charges=0; e.ownerGuidLow=4u; e.startbid=100u; + e.outbid=0u; e.buyout=5000u; e.timeLeftMs=720000u; + e.bidderGuidLow=0u; e.curBid=100u; + entries.push_back(e); + } ByteBuffer body; - AhAssembleBrowseListBody(entries, 3u, body); + AhAssembleBrowseListBody(entries, 4u, body); uint32 c=0; body >> c; - if (c != 1u) { printf("ahbrowsepending FAIL: assembled count %u\n", unsigned(c)); return 1; } - uint32 firstId=0; body >> firstId; - if (firstId != 42u) { printf("ahbrowsepending FAIL: first id %u\n", unsigned(firstId)); return 1; } - // Verify all remaining BrowseEntry fields + trailing totalcount. - // AhAssembleBrowseListBody writes fields in BuildAuctionInfo wire order: - // itemEntry, enchantId, randomPropId, suffixFactor, count, charges (6 x uint32), - // ownerGuid (uint64), startbid, outbid, buyout, timeLeftMs (4 x uint32), - // bidderGuid (uint64), curBid (uint32), then trailing uint32 totalcount. + if (c != 4u) + { + printf("ahbrowsepending FAIL: assembled count %u\n", + unsigned(c)); + return 1; + } uint32 dummy32 = 0; uint64 dummy64 = 0; - body >> dummy32; // itemEntry - body >> dummy32; // enchantId - body >> dummy32; // randomPropId - body >> dummy32; // suffixFactor - body >> dummy32; // count - body >> dummy32; // charges - body >> dummy64; // ownerGuid (uint64) - body >> dummy32; // startbid - body >> dummy32; // outbid - body >> dummy32; // buyout - body >> dummy32; // timeLeftMs - body >> dummy64; // bidderGuid (uint64) - body >> dummy32; // curBid + for (size_t i = 0; i < 4u; ++i) + { + uint32 id = 0; body >> id; + if (id != orderedIds[i]) + { + printf("ahbrowsepending FAIL: ordered id[%u]=%u\n", + unsigned(i), unsigned(id)); + return 1; + } + body >> dummy32 >> dummy32 >> dummy32; + body >> dummy32 >> dummy32 >> dummy32; + body >> dummy64; + body >> dummy32 >> dummy32 >> dummy32 >> dummy32; + body >> dummy64; + body >> dummy32; + } uint32 totalcount = 0; body >> totalcount; - if (totalcount != 3u) { printf("ahbrowsepending FAIL: totalcount %u\n", unsigned(totalcount)); return 1; } + if (totalcount != 4u) + { + printf("ahbrowsepending FAIL: totalcount %u\n", + unsigned(totalcount)); + return 1; + } (void)dummy32; (void)dummy64; } diff --git a/src/modules/AhWorker/BrowseHandler.cpp b/src/modules/AhWorker/BrowseHandler.cpp index b6b0b1975..c04c6d1c2 100644 --- a/src/modules/AhWorker/BrowseHandler.cpp +++ b/src/modules/AhWorker/BrowseHandler.cpp @@ -28,6 +28,7 @@ #include "Log/Log.h" #include #include +#include namespace { @@ -121,6 +122,44 @@ namespace namespace BrowseHandler { +std::vector ComposeBidderRows(const std::vector& rows, + const BrowseQuery& q) +{ + std::vector composed; + composed.reserve(q.outbidIds.size() + rows.size()); + + std::unordered_map byAuctionId; + byAuctionId.reserve(rows.size()); + for (size_t i = 0; i < rows.size(); ++i) + { + byAuctionId[rows[i].entry.id] = &rows[i]; + } + + // Legacy parity: trust the validated client list, preserve its order and + // duplicates, and silently skip ids absent from this house-scoped result. + for (size_t i = 0; i < q.outbidIds.size(); ++i) + { + std::unordered_map::const_iterator it = + byAuctionId.find(q.outbidIds[i]); + if (it != byAuctionId.end()) + { + composed.push_back(*it->second); + } + } + + // Fetch orders rows by auction id, matching BuildListBidderItems' map + // traversal. An auction also present above intentionally appears twice. + for (size_t i = 0; i < rows.size(); ++i) + { + if (rows[i].entry.bidderGuidLow == q.requesterGuidLow) + { + composed.push_back(rows[i]); + } + } + + return composed; +} + BrowseResult Fetch(ServiceDatabase& db, const BrowseQuery& q, FetchStatus& status) { status = FETCH_OK; @@ -203,16 +242,36 @@ BrowseResult Fetch(ServiceDatabase& db, const BrowseQuery& q, FetchStatus& statu sql += buf; } } - else + else if (q.kind == static_cast(BROWSE_OWNER)) { - // OWNER or BIDDER: scope by the requester GUID. char buf[96]; - const char* col = (q.kind == static_cast(BROWSE_BIDDER)) - ? "a.buyguid" - : "a.itemowner"; - snprintf(buf, sizeof(buf), " AND %s = %u", col, q.requesterGuidLow); + snprintf(buf, sizeof(buf), " AND a.itemowner = %u", q.requesterGuidLow); sql += buf; } + else + { + // BIDDER needs both parts of the legacy reply: auctions currently bid + // by the requester plus client-supplied outbid ids. The decoded rows + // are composed in exact client-visible order after this single query. + char buf[64]; + snprintf(buf, sizeof(buf), " AND (a.buyguid = %u", q.requesterGuidLow); + sql += buf; + if (!q.outbidIds.empty()) + { + sql += " OR a.id IN ("; + for (size_t i = 0; i < q.outbidIds.size(); ++i) + { + if (i != 0u) + { + sql += ','; + } + snprintf(buf, sizeof(buf), "%u", q.outbidIds[i]); + sql += buf; + } + sql += ')'; + } + sql += ')'; + } sql += " ORDER BY a.id"; @@ -384,6 +443,11 @@ BrowseResult Fetch(ServiceDatabase& db, const BrowseQuery& q, FetchStatus& statu while (result->NextRow()); delete result; + if (q.kind == static_cast(BROWSE_BIDDER)) + { + rows = BrowseHandler::ComposeBidderRows(rows, q); + } + status = rows.empty() ? FETCH_EMPTY : FETCH_OK; return BrowseHandler::FilterAndPaginate(rows, q); } @@ -439,19 +503,32 @@ void BrowseThread::run() } IpcMessage rm; rm.op = IPC_BROWSE_RESULT; - res.Encode(rm.body); + if (res.entries.size() > BrowseResult::MAX_ENTRIES) + { + BrowseResult capped; + capped.queryId = res.queryId; + capped.kind = res.kind; + capped.elunaPending = 0u; + capped.tooMany = 1u; + capped.totalcount = 0u; + capped.Encode(rm.body); + } + else + { + res.Encode(rm.body); + } // I8: preflight outbound body size; never emit over the MAXLEN cap. if (rm.body.size() > BrowseResult::MAX_WIRE) { - // Should be impossible at cap 1000 entries; fail safe by + // Should be impossible within MAX_ENTRIES; fail safe by // replying tooMany so mangosd tells the player the AH is // unavailable (coordinator model: no in-process fallback). BrowseResult capped; capped.queryId = res.queryId; capped.kind = res.kind; - capped.elunaPending = res.elunaPending; + capped.elunaPending = 0u; capped.tooMany = 1u; - capped.totalcount = res.totalcount; + capped.totalcount = 0u; rm.body.clear(); capped.Encode(rm.body); } @@ -519,6 +596,16 @@ namespace BrowseHandler if (!isList) { // OWNER/BIDDER: every row is an entry. + if (res.entries.size() >= BrowseResult::MAX_ENTRIES) + { + // The IPC decoder rejects a larger count. Decline now so + // mangosd consumes the pending request and immediately + // reports the AH as unavailable instead of timing out. + res.tooMany = 1u; + res.entries.clear(); + res.totalcount = 0u; + return res; + } ++res.totalcount; res.entries.push_back(r.entry); continue; diff --git a/src/modules/AhWorker/BrowseHandler.h b/src/modules/AhWorker/BrowseHandler.h index 394f562ea..f5885f4f5 100644 --- a/src/modules/AhWorker/BrowseHandler.h +++ b/src/modules/AhWorker/BrowseHandler.h @@ -70,6 +70,12 @@ enum FetchStatus namespace BrowseHandler { + /// PURE: compose a BIDDER result exactly like the legacy client path. + /// Client-supplied outbid ids are resolved in client order (including + /// duplicates), then the requester's current bids follow in row order. + std::vector ComposeBidderRows(const std::vector& rows, + const BrowseQuery& q); + /// PURE: usable filter (Task 6) + name filter (LIST) + pagination OR /// defer-un-paginated. Cheap proto filters are assumed SQL-applied. BrowseResult FilterAndPaginate(const std::vector& rows, diff --git a/src/modules/AhWorker/Main.cpp b/src/modules/AhWorker/Main.cpp index 5f3b350e2..493dcb6a4 100644 --- a/src/modules/AhWorker/Main.cpp +++ b/src/modules/AhWorker/Main.cpp @@ -746,6 +746,84 @@ static int RunIntentCodecSelfTest() return 1; } + // BIDDER: client outbid ids come first in client order. Missing ids are + // skipped, duplicate ids remain duplicated, then the requester's live + // bids append in SQL row order. An overlap appears in both groups. + std::vector bidderRows; + const uint32 bidderIds[] = { 3u, 7u, 9u, 12u }; + const uint32 bidders[] = { 41u, 42u, 42u, 0u }; + for (size_t i = 0; i < 4u; ++i) + { + BrowseRow r; + r.entry = BrowseEntry(); + r.entry.id = bidderIds[i]; + r.entry.bidderGuidLow = bidders[i]; + bidderRows.push_back(r); + } + BrowseQuery qb = qo; + qb.kind = static_cast(BROWSE_BIDDER); + qb.requesterGuidLow = 42u; + qb.outbidIds.push_back(9u); + qb.outbidIds.push_back(99u); // missing + qb.outbidIds.push_back(3u); + qb.outbidIds.push_back(9u); // duplicate + std::vector composed = + BrowseHandler::ComposeBidderRows(bidderRows, qb); + const uint32 expectedBidderIds[] = { 9u, 3u, 9u, 7u, 9u }; + if (composed.size() != 5u) + { + fprintf(stderr, "browse selftest FAILED: bidder composed size=%u" + " (exp 5)\n", + unsigned(composed.size())); + return 1; + } + for (size_t i = 0; i < composed.size(); ++i) + { + if (composed[i].entry.id != expectedBidderIds[i]) + { + fprintf(stderr, "browse selftest FAILED: bidder id[%u]=%u" + " (exp %u)\n", + unsigned(i), unsigned(composed[i].entry.id), + unsigned(expectedBidderIds[i])); + return 1; + } + } + + // IPC entry-count boundary: 1024 duplicate outbid rows are valid; + // 1025 must become an immediate tooMany response rather than a frame + // mangosd rejects before consuming its pending request. + std::vector boundaryRows; + BrowseRow boundaryRow; + boundaryRow.entry = BrowseEntry(); + boundaryRow.entry.id = 9u; + boundaryRow.entry.bidderGuidLow = 0u; + boundaryRows.push_back(boundaryRow); + BrowseQuery qlimit = qb; + qlimit.outbidIds.assign(BrowseResult::MAX_ENTRIES, 9u); + std::vector atLimit = + BrowseHandler::ComposeBidderRows(boundaryRows, qlimit); + BrowseResult limitResult = + BrowseHandler::FilterAndPaginate(atLimit, qlimit); + if (limitResult.tooMany != 0u || + limitResult.entries.size() != BrowseResult::MAX_ENTRIES) + { + fprintf(stderr, "browse selftest FAILED: bidder limit rejected\n"); + return 1; + } + qlimit.outbidIds.push_back(9u); + std::vector overLimit = + BrowseHandler::ComposeBidderRows(boundaryRows, qlimit); + BrowseResult overLimitResult = + BrowseHandler::FilterAndPaginate(overLimit, qlimit); + if (overLimitResult.tooMany != 1u || + !overLimitResult.entries.empty() || + overLimitResult.totalcount != 0u) + { + fprintf(stderr, "browse selftest FAILED: bidder over-limit" + " not declined\n"); + return 1; + } + // Over-cap deferred-Eluna (decision #2): >cap survivors -> the worker // declines with tooMany (no entries) so mangosd sends "AH unavailable", // rather than serving an approximate short page.