From 1d23d9f623c3e6fec2149c289f6ae2e0d942cf67 Mon Sep 17 00:00:00 2001 From: Tecc Date: Wed, 1 Jul 2026 20:12:24 +0000 Subject: [PATCH] feat: reasonable AH stack sizes (category rules + realistic breakpoints) getStackCount now takes the ItemTemplate and applies player-like stacking instead of uniform random. Always-single categories (glyphs, recipes, quest/container/key, and cut gems via GemProperties!=0) return 1 regardless of template stackable; raw gems and commodities get weighted human breakpoints {1,5,10,20,full} clamped to the stackable ceiling, never a random 7/13. O(1), no DB/alloc; existing quality/config caps and the optional DivisibleStacks mode are preserved. --- ...026-07-01-reasonable-stack-sizes-design.md | 67 +++++++++++++++++++ src/AuctionHouseBot.cpp | 67 ++++++++++++++++--- src/AuctionHouseBot.h | 3 +- 3 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 docs/superpowers/specs/2026-07-01-reasonable-stack-sizes-design.md diff --git a/docs/superpowers/specs/2026-07-01-reasonable-stack-sizes-design.md b/docs/superpowers/specs/2026-07-01-reasonable-stack-sizes-design.md new file mode 100644 index 0000000..6ef4b58 --- /dev/null +++ b/docs/superpowers/specs/2026-07-01-reasonable-stack-sizes-design.md @@ -0,0 +1,67 @@ +# Reasonable AH stack sizes — design + +Date: 2026-07-01 +Status: Approved, implementing. + +## Problem + +`AuctionHouseBot::getStackCount` picks a stack size with `urand(1, min(stackable, maxStackSize))` +(uniform random, with a `// TODO: This is not good` comment). Result: commodities list at +unrealistic sizes (7, 13, ...) and any item whose template `stackable > 1` (e.g. glyphs on +this dataset) gets quantities a real player would never post. Real players sell glyphs, +recipes, cut gems, etc. one at a time, and post commodities in clean 1/5/10/20/full stacks. + +## Approach + +Rewrite `getStackCount` to be category-aware and use realistic breakpoints. It takes the +`ItemTemplate*` (already in scope at the two `Sell` call sites) to branch on class / +GemProperties. Pure arithmetic + a `switch` + one `urand` — O(1), no DB lookup, no +allocation (not a performance path). All existing ceilings are preserved: the caller still +passes `item->GetMaxStackCount()` (template `stackable`) and applies the config +`GetMaxStackSize` and per-quality `GetMaxStack` caps. + +## Rules (in order) + +1. **Always single (return 1)** regardless of template stackable: + - `ITEM_CLASS_GLYPH` (16), `ITEM_CLASS_RECIPE` (9), `ITEM_CLASS_QUEST` (12), + `ITEM_CLASS_CONTAINER` (1), `ITEM_CLASS_KEY` (13) + - `ITEM_CLASS_GEM` (3) **when `prototype->GemProperties != 0`** (cut/socketable gem). + Raw gems (class 3, `GemProperties == 0`) fall through and stack like materials. +2. **Non-stackable** (`max <= 1`) -> 1. +3. **Commodities** -> weighted human breakpoint, capped at `cap = maxStackSize > 0 ? + min(max, maxStackSize) : max`: + - roll `urand(1,100)`: `<=30` -> full stack (`cap`); `<=50` -> `1`; else a random pick of + the breakpoints `{5,10,20}` that are `<= cap` (if none valid, full stack). + - Weights are tunable; the intent is "biased toward what players actually post, never a + random 7/13". + +The existing `DivisibleStacks` config branch is kept as an alternate mode for admins who set +it; only the uniform-random default is replaced. Category rules (step 1) apply before either. + +## Signature change + +- `getStackCount(AHBConfig* config, uint32 max)` -> `getStackCount(AHBConfig* config, uint32 max, ItemTemplate const* prototype)`. +- Update the two call sites in `Sell` (they already have `prototype`) to pass it. +- Header declaration updated to match. + +## Safety / perf + +- No behaviour change to the `Sell` gating or the quality/config caps — only the number + chosen within the ceiling changes. +- No DB access, no heap allocation; a fixed 3-element local array for the mid breakpoints. +- Degrades sensibly for odd `cap` values (e.g. a `cap` with no valid mid breakpoint returns + the full stack). + +## Testing + +C++ can't be unit-tested locally (needs the full core). Gates: +- CI `-Werror` build (compile + no unused-param/var). +- TEST-realm validation: confirm glyphs / recipes / cut gems list as quantity 1, raw gems + and commodities (cloth/ore/herbs/potions) list as clean 1/5/10/20/full stacks, no random + odd sizes. + +## Out of scope + +- The pre-existing `DivisibleStacks` `ret == 0` edge (when `max` isn't divisible by 3/4/5) — + left untouched. +- Any per-item stack override table (rules cover the need; revisit only if exceptions surface). diff --git a/src/AuctionHouseBot.cpp b/src/AuctionHouseBot.cpp index bbf5070..93b5df8 100644 --- a/src/AuctionHouseBot.cpp +++ b/src/AuctionHouseBot.cpp @@ -101,19 +101,34 @@ uint32 AuctionHouseBot::getElement(const std::vector& vec, int index, ui return itemID; } -uint32 AuctionHouseBot::getStackCount(AHBConfig* config, uint32 max) +uint32 AuctionHouseBot::getStackCount(AHBConfig* config, uint32 max, ItemTemplate const* prototype) { uint32 maxStackSize = config->GetMaxStackSize(); - if (max == 1) + // Some categories are always sold one at a time, regardless of the template's + // stackable value -- a real player never posts a glyph, recipe or cut gem in a stack. + switch (prototype->Class) { + case ITEM_CLASS_GLYPH: + case ITEM_CLASS_RECIPE: + case ITEM_CLASS_QUEST: + case ITEM_CLASS_CONTAINER: + case ITEM_CLASS_KEY: return 1; + case ITEM_CLASS_GEM: + if (prototype->GemProperties != 0) // cut/socketable gem; raw gems stack like mats + return 1; + break; + default: + break; } - // - // Organize the stacks in a pseudo random way - // + if (max <= 1) + { + return 1; + } + // Optional legacy mode: organize stacks by divisibility. if (config->DivisibleStacks) { uint32 ret = 0; @@ -141,9 +156,41 @@ uint32 AuctionHouseBot::getStackCount(AHBConfig* config, uint32 max) return ret; } - // Totally random stack sizes... - // TODO: This is not good, we need to find a better way to organize the stacks - return urand(1, std::min(max, maxStackSize)); + // Realistic weighted breakpoints -- looks player-made, never a random 7 or 13. + uint32 cap = (maxStackSize > 0) ? std::min(max, maxStackSize) : max; + if (cap <= 1) + { + return 1; + } + + uint32 roll = urand(1, 100); + if (roll <= 30) + { + return cap; // full stack + } + if (roll <= 50) + { + return 1; // single + } + + // A common human breakpoint that fits under the cap. + uint32 const breakpoints[3] = { 5, 10, 20 }; + uint32 valid[3]; + uint32 nValid = 0; + for (uint32 i = 0; i < 3; ++i) + { + if (breakpoints[i] <= cap) + { + valid[nValid++] = breakpoints[i]; + } + } + + if (nValid == 0) + { + return cap; // cap is 2..4: no clean breakpoint, use the full stack + } + + return valid[urand(0, nValid - 1)]; } uint32 AuctionHouseBot::getElapsedTime(uint32 timeClass) @@ -985,11 +1032,11 @@ void AuctionHouseBot::Sell(Player* AHBplayer, AHBConfig* config) // Determine the stack size if (config->GetMaxStack(prototype->Quality) > 1 && item->GetMaxStackCount() > 1) { - stackCount = minValue(getStackCount(config, item->GetMaxStackCount()), config->GetMaxStack(prototype->Quality)); + stackCount = minValue(getStackCount(config, item->GetMaxStackCount(), prototype), config->GetMaxStack(prototype->Quality)); } else if (config->GetMaxStack(prototype->Quality) == 0 && item->GetMaxStackCount() > 1) { - stackCount = getStackCount(config, item->GetMaxStackCount()); + stackCount = getStackCount(config, item->GetMaxStackCount(), prototype); } else { diff --git a/src/AuctionHouseBot.h b/src/AuctionHouseBot.h index 9f63937..cdf4ea1 100644 --- a/src/AuctionHouseBot.h +++ b/src/AuctionHouseBot.h @@ -39,6 +39,7 @@ class AHBConfig; class AuctionHouseObject; struct AuctionEntry; +struct ItemTemplate; class Player; class WorldSession; @@ -72,7 +73,7 @@ class AuctionHouseBot inline uint32 minValue(uint32 a, uint32 b) { return a <= b ? a : b; }; uint32 getNofAuctions(AHBConfig* config, AuctionHouseObject* auctionHouse, ObjectGuid guid); - uint32 getStackCount(AHBConfig* config, uint32 max); + uint32 getStackCount(AHBConfig* config, uint32 max, ItemTemplate const* prototype); uint32 getElapsedTime(uint32 timeClass); uint32 getElement(const std::vector& vec, int index, uint32 botId, uint32 maxDup, std::unordered_map& botItemCounts);