Skip to content

fix: restore Sell() fallthrough semantics, guard price overrides, live reload#23

Merged
icemansparks merged 5 commits into
merkerhoodfrom
fix/sell-flow-and-override-guards
Jul 2, 2026
Merged

fix: restore Sell() fallthrough semantics, guard price overrides, live reload#23
icemansparks merged 5 commits into
merkerhoodfrom
fix/sell-flow-and-override-guards

Conversation

@icemansparks

Copy link
Copy Markdown
Collaborator

Fixes the must-fix findings from a review of the merkerhood branch, plus two follow-ups the review of this branch itself surfaced.

Sell() control flow (silent under-listing)

The Sell() refactor had turned upstream's continue into return in all four failure branches and chained the rarity-tier selection into if/else if. First failed pick (e.g. an item at its count-override cap) aborted the entire cycle, so the AH chronically undershot MinItems/MaxItems. Restored upstream skip-and-continue and per-tier fallthrough, keeping all merkerhood features (priority list, count overrides, percentage caps).

Count-override cap now applies to priority-list picks

Priority-list picks from the price-override set went straight to listing without ever consulting the count override — the cap only existed in the (unreachable) random-pick fallback. Items with curated liquidity caps were exactly the ones the bot could flood without limit. Priority picks now check the cap like getElement() does; capped picks fall through to the rarity-tier selection.

Price override validation (UB in world thread)

LoadPriceOverrides() accepted rows with minPrice > avgPrice; Buy() then computed avgPrice + (avgPrice - minPrice) in uint64, wrapping to ~UINT64_MAX and casting out-of-range to uint32 — undefined behavior reachable from a single malformed backfill row. Load now clamps minPrice = avgPrice with a warning naming the item; Buy() re-clamps as defense in depth.

Overrides reload with .reload config

The per-bot static bool load guards meant price/count override edits needed a server restart. Loading moved to AHBot_WorldScript::LoadSharedOverrides(), called on startup and in the reload branch of OnBeforeConfigLoad (after DeleteBots(), before PopulateBots()). Maps are cleared before loading so row deletions take effect too.

Compile-checked against the full core tree (RelWithDebInfo, gcc/ninja).

… Sell()

Reverts a return()/if-else-if regression in the per-item listing loop.
A failed pick (itemID==0, missing prototype/item, quality over the
configured max) now skips to the next item instead of aborting the
whole selling cycle for the bot, matching upstream continue semantics.
The rarity/category selection block is restored to independent if
blocks so a tier with no eligible pick correctly falls through to the
next tier instead of being short-circuited by the chained else-if.
avgPrice - minPrice underflows a uint64 when minPrice > avgPrice, which
Buy() then folds into maxPrice and eventually a uint32 cast, producing
undefined behavior on the world thread. Clamp at the source in
LoadPriceOverrides() with a warning naming the offending item, and add
a matching guard at the Buy() call site as defense in depth.
priceOverridesLoaded/countOverridesLoaded were function-local static
bools inside AuctionHouseBot::Initialize(), so once set they never
went false again and .reload config left the bot running with
whatever overrides were loaded at server start. Move the load-once-
and-share step into AHBot_WorldScript::LoadSharedOverrides(), called
once per startup and once per reload cycle before bots are
(re)constructed, so both tables actually refresh on reload.
Comment thread src/AuctionHouseBot.cpp
itemID = getElement(config->GreyItemsVec, urand(0, config->GreyItemsVec.size() - 1), _id, config->DuplicatesCount, botItemCounts, config);
}
else if (itemID == 0 && !config->GreyTradeGoodsVec.empty() && (currentGreyTG < maxGreyTG))
if (itemID == 0 && !config->GreyTradeGoodsVec.empty() && (currentGreyTG < maxGreyTG))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this change cause problems since the item is handled by all buckets?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No — at most one bucket can produce the item per iteration. Every bucket after the first is gated on itemID == 0, so as soon as any getElement() returns a real item, all later buckets short-circuit. Fall-through only continues while nothing has been picked yet (bucket empty, percentage cap reached, or the random pick sat at its count-override cap). An item also can't appear in two buckets: the bins are partitioned by quality and class (items vs trade goods), so each item id lives in exactly one vector.

itemTypeSelectedToSell does get set optimistically before getElement() resolves, but it's overwritten by whichever bucket actually delivers, and if the loop exits empty the iteration is discarded before the value is read. This matches upstream azerothcore/master exactly — the else if chain was the deviation this PR reverts.

@icemansparks
icemansparks merged commit 3ca39be into merkerhood Jul 2, 2026
1 check passed
@icemansparks
icemansparks deleted the fix/sell-flow-and-override-guards branch July 2, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant