Skip to content

lity to force a level in Creature::SelectLevel()#2

Merged
billy1arm merged 1 commit into
mangoszero:masterfrom
Chuck5ta:master
Jan 21, 2017
Merged

lity to force a level in Creature::SelectLevel()#2
billy1arm merged 1 commit into
mangoszero:masterfrom
Chuck5ta:master

Conversation

@Chuck5ta

Copy link
Copy Markdown
Contributor

Cmangos commit bb91d5e

@billy1arm billy1arm merged commit c0eea3f into mangoszero:master Jan 21, 2017
billy1arm pushed a commit that referenced this pull request Feb 15, 2017
billy1arm added a commit that referenced this pull request Jun 5, 2026
billy1arm pushed a commit that referenced this pull request Jun 18, 2026
* feat(livingworld): add per-cell loaded bitset to NGrid for B-Cell

* feat(livingworld): add static_assert-tested cell-envelope math header

* feat(livingworld): add LOG_FILTER_CELL_ENVELOPE debug filter

* refactor(livingworld): make grid load cell-granular via idempotent LoadCell

* feat(livingworld): add Map::EnsureCellEnvelopeLoaded (3x3 envelope load)

* feat(livingworld): add LivingWorld.CellEnvelopeLoad config (default off) + version bump

* feat(livingworld): load 3x3 envelope for player-less anchors via EnsureGridLoadedAtEnter

* feat(livingworld): accrete envelope on same-grid anchor cell moves

* feat(livingworld): add tier-2 anomaly capture + live .grid info state + counters

* fix(livingworld): remove corpse re-add from envelope path; drop dead anomalyTouchUnloaded counter

* feat(livingworld): add ObjectGridUnloader::MoveToRespawnCell for per-cell teardown

* feat(livingworld): add Map::UnloadCell per-cell teardown primitive

* feat(livingworld): add LivingWorld.CellEnvelopeDowngradeDelay config + version bump

* feat(livingworld): track per-grid player count + downgrade timer

* feat(livingworld): add Map::IsCellAnchorProtected envelope-coverage check

* feat(livingworld): downgrade FULL anchor grids back to ENVELOPE after players leave

* feat(livingworld): trailing unload as moving anchors advance (sliding envelope)

* feat(livingworld): show B-Cell unload-side counters in .grid lwstats

* fix(livingworld): downgrade timer must accrue real time; cheap grid-reject in IsCellAnchorProtected

Review fixes on PR2:
- GridStates ActiveState::Update: the FULL->ENVELOPE downgrade timer was Update()'d
  with one tick's t_diff but only inside the periodic info-timer gate, so it accrued
  ~grid_expiry/10 too slowly and the downgrade fired hours late. Move it to the outer
  per-tick scope (cheap FULL + no-player gate; ActiveObjectsInGrid()+downgrade only on
  elapse) so it honours CellEnvelopeDowngradeDelay.
- Map::IsCellAnchorProtected: add a grid-distance early-reject (a 3x3 envelope reaches
  at most one grid away) before the per-cell test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): don't downgrade a grid a neighbouring player can see (thrash fix)

A player's visibility FULL-loads adjacent grids, but the FULL->ENVELOPE downgrade
gated only on the grid's own in-grid playerCount. A player standing in grid 14,32
thus FULL-loaded neighbour 14,31 (playerCount 0 there), the downgrade fired, the
player's visibility instantly reloaded it -> 86 downgrades at ~2s, ignoring the
30s delay, NPCs popping for the player.

Add Map::HasPlayerInOrAroundGrid (3x3 grid-block player check) and gate the
downgrade on it: reset the timer while any player is in or adjacent, only accrue
toward the delay once the whole 3x3 is player-free.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(livingworld): add cell-loaded to .npc watch (per-cell envelope state)

grid-loaded reports the whole-grid FULL flag, which reads 'no' for an ENVELOPE
grid even when the watched NPC's own cell is resident. Add Map::IsCellLoaded
(read-only per-cell bitset query) and surface cell-loaded= at both .npc watch
and .npc watch last print sites, so an anchor in an envelope grid shows
grid-loaded=no cell-loaded=yes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): tick resident envelope cells in Map::Visit without forcing FULL load

* fix(livingworld): count/log accretion only on actual cell loads, not every crossing

The accretion counter + trace fired on every anchor cell-boundary crossing, so a
world of patrolling anchors produced ~16k 'accretion' log lines / 16 min while only
~5k cells were truly loaded -- flooding the log and making .grid lwstats 'accretions'
read 'steps taken' rather than 'cells accreted'. Gate both on EnsureCellEnvelopeLoaded
returning true (it already logs each loaded cell), so the metric is meaningful and the
log is quiet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): restore upstream grid-load re-entrancy invariant

Decouple the grid-bool guard from the cell-bitset: add markGridObjectDataLoading() to NGrid

so the FULL flag is set before LoadN (matching upstream re-entrancy guard) without

prematurely setting all 256 cell bits. setGridObjectDataLoaded(true) remains after

LoadN to finalise the full bitset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): Map::Remove must not skip ENVELOPE-resident objects

Gate removal on the object's cell residency (isCellObjectDataLoaded)

instead of the grid FULL flag, so objects in envelope cells are fully removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): cell-aware respawn relocation in UnloadCell

Add i_cellGranular to ObjectGridRespawnMover. When true, relocate creatures

whose current cell differs from their respawn cell (not only DiffGrid).

MoveToRespawnCell uses cellGranular=true; MoveToRespawnN keeps grid semantics.

Prevents same-grid wanderers from being deleted during per-cell teardown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): ForceLoadGrid must FULL-load, never envelope-load

Replace EnsureGridLoadedAtEnter with direct EnsureGridLoaded + active-state

setup so ForceLoadGrid always yields a FULL grid with explicit unload lock,

identical to flag-off behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): don't bounce non-active creatures off ENVELOPE-resident destination cells

Gate cross-grid rejection on destination cell residency, not the FULL flag.

A non-active creature may path into an adjacent envelope-resident cell;

only genuinely-unloaded destinations still send it to respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): make pool/direct spawn checks cell-aware

Replace map->IsLoaded with map->IsCellLoaded at spawn sites so a pool

or direct spawn into an envelope-resident cell is instantiated, not deferred.

Files: PoolManager.cpp, Creature.cpp, GameObject.cpp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): defer cell teardown out of the update/iteration path

Queue pending cell unloads from trailing-unload and DowngradeGridToEnvelope,

then drain them at the end of Map::Update after all iterations complete.

Re-checks residency and anchor protection at drain time. Prevents iterator

invalidation from object deletion mid-iteration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): verify corpse safety on cell teardown

Document that UnloadCell only visits GridTypeMapContainer; resurrectable

player corpses are world objects (WorldTypeMapContainer) and are already

preserved during cell teardown. No behavioural change required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): scope envelope loading to continent world maps

Add IsContinent() guard so only continent world maps use the envelope path;

instances and battlegrounds fall through to normal FULL load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): promote ENVELOPE neighbour grids to FULL for nearby players

When a player is added or changes grid, iterate the 8 neighbour grids and

FULL-load any that are resident but ENVELOPE. Restores visual parity with

the pre-B-Cell baseline at grid boundaries without forcing empty neighbours.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): document FULL-grid bitset semantics

Clarify that isCellObjectDataLoaded means 'cell is covered/scanned'

(FULL grid or envelope-loaded), not 'cell contains DB objects'.

No behavioural change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): Allman braces in GridCommands.cpp lwState assignment

Expand compact single-line if assignments to Allman-braced multiline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(livingworld): pin SD3 submodule at master-reachable 10e471f (defer scan-guard bump)

Commits a6ae561 (re-entrancy fix) and 9f27f65 (bitset doc) inadvertently
staged the SD3 gitlink to the unmerged scan-guard branch tip (54d2025) as a
side effect of `git add`. A server PR must reference a commit reachable from
ScriptDev3 master, so reset the recorded pin back to 10e471f.

The scan-guard + anomaly-key changes live on branch livingworld/bcell-sd3
(tip 54d2025) and land via their own PR into mangos/ScriptDev3; the server
submodule pin is bumped to the merged SHA as the final landing step. The
working tree stays checked out at 54d2025 so local builds keep the scan-guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): harden deferred cell-unload against freed grids; tighten neighbour promotion

PR2.1 review follow-up (issues found while reviewing the fix stack).

1. Deferred cell-unload use-after-free (Task 7):
   m_pendingCellUnloads stored raw NGridType* pointers. Within one Map::Update a
   cell can be enqueued during the active-object loop, then its grid fully
   unloaded by UnloadGrid() in the grid-state loop -- which runs before the drain
   and does `delete getNGrid(...)` -- leaving a dangling pointer that
   ProcessPendingCellUnloads() then dereferences. Store grid coords instead and
   re-resolve via getNGrid() at drain time (null -> skip).

2. Neighbour promotion over-broad (Task 10):
   PromoteEnvelopeNeighboursToFull() promoted any non-FULL existing grid,
   including a created-but-empty one. Require loadedCellCount() > 0 so only
   genuine ENVELOPE grids are promoted.

Builds clean (RelWithDebInfo, exit 0, no new warnings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): reject non-active creature relocation into a non-resident cell

PR2.1 review follow-up (Codex 2nd-pass High #1).

CreatureCellRelocation only validated/loaded the destination cell on cross-grid
moves. A same-grid relocation went straight to RemoveFromGrid/AddToGrid without
checking residency. The cell-aware respawn relocation (UnloadCell -> MoveToRespawnCell)
can send a wandered creature home into a same-grid cell that was already unloaded
earlier in the same teardown drain; the creature then sits in a non-resident cell,
which Map::Visit skips (frozen) and Map::Remove skips (unremovable).

After the cross-grid load attempt, reject any non-active creature whose destination
cell is still not resident (same-grid or cross-grid). The caller
(CreatureRespawnRelocation -> Unload) then deletes + reschedules it via SaveRespawnTime
instead of stranding it. Active anchors are exempt: their accretion path loads the
destination envelope before AddToGrid.

Builds clean (RelWithDebInfo, exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): promote an ENVELOPE grid to FULL when it appears next to a stationary player

PR2.1 review follow-up (Codex 2nd-pass High #2).

PromoteEnvelopeNeighboursToFull only fires on player add/relocation, so an ENVELOPE
grid created or extended NEXT TO a stationary player -- an anchor wandering in
(accretion) or being registered active nearby (AddToActive) -- was not promoted,
re-introducing the boundary visibility pop-in for that case.

Add MaybePromoteEnvelopeGridForPlayer(gridX, gridY): if the grid is a genuine ENVELOPE
(loadedCellCount > 0, not FULL) and HasPlayerInOrAroundGrid is true, promote it to FULL.
Call it from the accretion path in CreatureCellRelocation and from AddToActive. Gated on
CellEnvelopeLoad; flag-off is unchanged. Composes with the existing downgrade guard
(HasPlayerInOrAroundGrid blocks teardown while a player is near), so no thrash.

Builds clean (RelWithDebInfo, exit 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(livingworld): require lwstats DB structure update

* chore(livingworld): use standard header for cell envelope

* fix(livingworld): mark respawn mover constructor explicit

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
MadMaxMangos added a commit that referenced this pull request Jul 1, 2026
… externalized AH reads (coordinator) (#414)

* feat(mangosd): in-process -t test-mode scaffold (custody groundwork)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mangosd): _exit after -t test to skip Eluna teardown crash (reliable exit code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(db): add CommitTransactionChecked synchronous durable commit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(mail): SendMailToInTransaction co-commit variant + ordered deferred queue

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): CustodyDeferred online-item rollback lifetime + self-contained header

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(db): harden CommitTransactionChecked (begin-failure, stopped-thread, exception safety)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(character): require Character DB 22_05_001 for custody_ledger

Bumps the required Character DB version to match the Rel22_05_001
custody_ledger migration (mangoszero/database). Separated from the
CommitTransactionChecked infra commit so the shared-DB change carries
no feature/migration coupling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): custody_ledger persistent model + CRUD selftest

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): CustodyService primitives + ordered deferred context

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* build(custody): add AH.Service.Custody + CustodyCrashAt + bump MANGOS_WORLD_VER

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): route S1 auction-create through the custody ledger (gated)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): route S2/S3 bid+buyout through the custody ledger (gated)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): route custody buyouts to legacy path (avoid nested-txn abort until Task 10)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): S2 restore-on-failure + fail-closed bid lookup + scalar notify closure (Codex NO-SHIP)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(mail): re-resolve deferred mail-push receiver by GUID (I2 scalar-only closure)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): S1 restore-on-failure (refund deposit, drop phantom auction, return item)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): scalar-only command-result closure + active-rows gate + auction-id reuse guard (Codex re-review)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): NextBidSeq uses MAX(id) not COUNT to avoid key collision after TTL prune (GLM review)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): route S4 win/resolve + buyout through the custody ledger (gated)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): S4 deferred closures scalar-only (drop AuctionEntry* capture) + remove dead GetLiveBidKey

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(custody): mail items survive rollback (dispose only on success) - fixes win-resolution use-after-free (GLM)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(custody): route S5 auction-cancel through the custody ledger (gated)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): terminalize item ledger row on the custody win path (S4)

AuctionBidWinningCustody flipped the dep: and bid rows but never the
item:<Id> row; SendAuctionWonMailInTransaction moves the item (owner UPDATE
or item_instance DELETE) without a ledger flip. So every custody
sale/buyout/bot-win left the item:<Id> row CST_RESERVED after the auction
row was deleted -> an orphaned non-terminal row, which breaks the
reconciliation invariant ("every non-terminal row maps to a live auction")
that Tasks 13/14 depend on.

Add one in-txn CommitGoldLedgerOnly("item:"+Id) immediately after
SendAuctionWonMailInTransaction and before DeleteFromDB. On a win the item
always resolves (delivered or destroyed), so the flip is unconditional and
safe; it is in-txn so on rollback it rolls back with everything else (no
orphan, no false terminal).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-custody): route unsold-expiry through checked custody txn (S6)

The no-bidder expiry branch of AuctionHouseObject::Update() now routes
custody-row auctions through a single checked co-committed transaction
(per-auction HasRows gate, X3), mirroring the win-branch gate already at
:890-908. S6 is the simplest seam: no bid, no winner, no cut, and NO
synchronous in-memory mutation -> no X6 restore (every effect is deferred
and does not run on rollback; the auction + item survive intact and the
next tick re-resolves cleanly).

AuctionHouseMgr.cpp:
- SendAuctionExpiredMailInTransaction: co-commit mirror of
  SendAuctionExpiredMail. Owner-exists branch: defers the online owner's
  SMSG_AUCTION_OWNER_NOTIFICATION (expired form, sold=false,
  bid/outbid/bidder all 0) BEFORE the mail push (notify-then-mail, legacy
  :307), defers RemoveAItem FIRST then zeroes itemGuidLow, and returns the
  item via CustodyService::DeliverItem (flips "item:<Id>" -> TERMINAL_OK
  AND co-commits the return mail). Destroy branch: in-txn item_instance
  DELETE + CommitGoldLedgerOnly("item:"+Id) (the §0 lesson -- no mail means
  DeliverItem is not called, so the escrow row must be terminalized
  explicitly), defers RemoveAItem + live delete pItem (X5: destroy only on
  commit success).
- AuctionEntry::ExpireUnsoldCustody: orchestrator mirroring
  AuctionBidWinningCustody. Returns the item (or destroys it), forfeits the
  deposit to the house (CommitGoldLedgerOnly("dep:"+Id), house sink, no
  money/mail), deletes the auction row IN-TXN, and defers the AH-map erase
  + object delete LAST so `this` stays valid throughout def.run().
- Update() no-bid branch: gated custody path (symmetric with the win
  branch); legacy path unchanged behind the else.

AuctionHouseMgr.h: declare both new methods beside their win-path siblings.

Gate-OFF (no custody rows) is byte-identical to legacy. Eluna: legacy
expiry fires no AH hook, so S6 fires none either (parity audit in the
follow-up Eluna batch). Build green; -t custody green. The SQL differential
(online/offline/deleted-account seller) needs the human's quiesced realm
(Task 15).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): fire Eluna AH hooks success-only in legacy order across custody seams

Audit every custody seam for Eluna AH-hook parity (success-only + legacy
sequence position + live entry):

S5 Cancel (AuctionHouseHandler.cpp): OnRemove was fired inside
if (CommitTransactionChecked()) -- success-only, good -- but BEFORE
def.run(), i.e. not at its legacy sequence position among the cancel
effects. Move it into a def.effects closure appended AFTER the
refund/item-return effects and BEFORE the trailing RemoveAuction/delete
closure, so it runs in legacy order during def.run() and still sees a live
entry. Captures auctionHouse + auction by value (both live until the final
delete effect runs later in the same def.run() pass). Wrapped in
#ifdef ENABLE_ELUNA as before.

S1 Create: CONFIRMED already correct -- the X6 rollback path returns at
:641 before the OnAdd call at :658, so OnAdd fires only on a successful
checked commit. No change.

S4 Win: legacy AuctionBidWinning (AuctionHouseMgr.cpp:1274-1291) fires NO
Eluna AH hook, so the custody win path (AuctionBidWinningCustody) fires
none either. No change.

S6 Expire: legacy expiry (SendAuctionExpiredMail + the no-bid Update()
branch) fires NO Eluna AH hook, so ExpireUnsoldCustody fires none either.
No change.

Build green; -t custody green. Eluna behavior itself is validated only
with an Eluna build + a Lua hook script (manual follow-up); noted here as
the brief's stated test caveat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): keep S6 expire rollback-safe (drop synchronous itemGuidLow=0)

Adversarial-review fix. SendAuctionExpiredMailInTransaction zeroed
auction->itemGuidLow synchronously (before the checked commit) in BOTH the
owner-exists and destroy branches. That is an in-memory mutation that
SURVIVES a CommitTransactionChecked() rollback -- contradicting the seam's
own invariant ("S6 makes NO synchronous in-memory mutation; on rollback the
auction + item survive intact and the next tick re-resolves cleanly").

On rollback: def.run() does not fire (the auction stays in the map) but
itemGuidLow is now 0; the custody rows revert so HasRows() stays true. The
next Update() tick re-enters the seam, GetAItem(0) returns NULL -> early
"item not found, and lost" return, yet ExpireUnsoldCustody continues to
forfeit the deposit + DeleteFromDB + delete the auction. Net: item never
returned to the player, "item:" row orphaned CST_RESERVED, live Item* leaked.

Fix: remove both synchronous `auction->itemGuidLow = 0;` writes. Nothing
reads itemGuidLow after that point on the success path (the deferred
RemoveAItem uses the savedItemGuidLow snapshot; the auction is deleted by
the trailing deferred closure), so the field never needs zeroing; on
rollback it must stay valid for the next-tick re-resolution. This matches
S4's SendAuctionWonMailInTransaction, which never zeroes it either. Also
correct the now-stale savedItemGuidLow snapshot comment in both senders.

Build green; -t custody green (custody OK / EXIT=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): fire S5 Eluna OnRemove in exact legacy order (RemoveAuction -> OnRemove -> delete)

Adversarial-review refinement (Eluna parity, Minor). The moved S5 OnRemove
hook fired BEFORE the trailing RemoveAuction closure, i.e. while the auction
was still in the AH map -- whereas the legacy non-custody branch fires
OnRemove AFTER RemoveAuction (out of map) and before delete. A Lua OnRemove
handler that queried the auction house during the callback would see the
auction present on the custody path but absent on legacy.

Fold OnRemove into the trailing deferred closure between RemoveAuction and
delete self, reproducing the legacy order exactly (RemoveAuction out-of-map
-> OnRemove -> delete). Still success-only (the closure runs only in
def.run() after a successful checked commit); `self`/auctionHouse stay valid
until the delete at the end of the closure.

Build green; -t custody green (custody OK / EXIT=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): terminalize item row on the S6 missing-item path + correct stale itemGuidLow contract docs

Independent-review (GLM/Codex) NO-SHIP fixes.

IMPORTANT: SendAuctionExpiredMailInTransaction returns early when
GetAItem(itemGuidLow) is NULL (item already absent from the live AH cache),
WITHOUT terminalizing the "item:<Id>" escrow row -- but ExpireUnsoldCustody
still forfeits the deposit and deletes the auction row unconditionally. A
custody auction could thus be removed while item:<Id> stays CST_RESERVED,
orphaning a non-terminal custody row with no live auction (breaks the
reconciliation invariant Tasks 13/14 depend on). Requires a degraded state
(cache drift / partial prior corruption) to trigger, but the seam should not
introduce a ledger orphan. Fix: flip "item:<Id>" -> TERMINAL_OK ledger-only
in the early-return branch (legacy player-visible behavior unchanged -- the
item is already lost; item_instance is intentionally NOT touched, matching
legacy, since its DB state is unknown when the cache has drifted).

MINOR: the function doc comment (AuctionHouseMgr.cpp) and the header
declaration comment (AuctionHouseMgr.h) still claimed "itemGuidLow=0 ... are
deferred", stale after 4ced3e7 removed the synchronous zero entirely. Both
now state that itemGuidLow is intentionally NOT zeroed on the custody path
(success deletes the AuctionEntry; rollback needs the original GUID for the
next-tick re-resolution), so a future maintainer does not reintroduce either
a synchronous or a deferred zero.

Build green; -t custody green (custody OK / EXIT=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-custody): add reconcile sweep and terminal prune

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-custody): add console-only 'ah repair' custody-drift command

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): make repair terminalize drift without minting value

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-custody): wire crash-injection hooks (pre-commit/pre-deferred) into the custody seams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ah-custody): add custody differential SQL, crash-injection procedure, operator docs (Task 15)

- src/ah-service/tools/custody_diff.sql: the spec Section 8 player-visible
  projection diff (characters/mail/mail_items/item_instance/auction),
  count-aware + NULL-safe, parameterized clone schemas, custody_ledger
  excluded, mail-time normalization toggle.
- src/ah-service/tools/custody_crash_test.md: the crash-injection procedure
  against the now-wired pre-commit/pre-deferred hooks, the forced-rollback
  (CommitTransactionChecked()==false) case, the full Section 8 matrix, and the
  promotion gate.
- doc/AuctionHouseBot.md: custody operator section (AH.Service.Custody +
  AH.Service.CustodyCrashAt keys, per-auction HasRows gate, the console-only
  'ah repair' terminalize-without-disbursing + force-forfeit escape hatch,
  promotion checklist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah-custody): address GLM Task-15 review (auction-time normalization + crash-log flush + doc clarifications)

I1 (Important): custody_diff.sql had no normalization toggle for auction.time
(= expireTime, a wall-clock timestamp set at creation), so an unpinned-clock A/B
run would false-positive every created auction in AUCTION_DIFF with no escape.
Add @compare_absolute_auction_times (default 0) mirroring the mail toggle: the
auction temp tables now NULL out `time` by default, AND the AUCTION_DIFF join
compares `time` with the NULL-safe <=> (a plain `=` makes NULL=NULL fail and
would report every row -- the gap in the review's suggested minimal fix).
Precondition note updated.

M1 (Minor): MaybeCrash flushed only stdout before _exit(3); the sLog crash line
(written to the log FILE*) could be lost. Use fflush(NULL) to flush all stdio
streams so the crash-test log assertion is reliable.

M2 (Minor): custody_crash_test.md pre-commit recovery assertion said "the
auction is intact", misleading for S1 (create) where no row was ever inserted.
Reworded to "pre-seam DB state unchanged -- for S1 no auction row exists; for
S2-S6 the existing row is unchanged".

M3 (Minor): custody_diff.sql notes that a clean MAIL_DIFF with a dirty
MAIL_ITEMS_DIFF means mail.id drifted between clones (re-verify the
idle-snapshot precondition).

Build green; -t custody green (custody OK / EXIT=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ah-custody): add in-game promotion-gate runbook

Ties the Section 8 promotion gate together for an operator: prerequisites
(build c08a6b7, the two pending mangos0 World migrations, the character0 A/B
clones, the custody conf keys), the seeded quiesced fixture, the A/B
differential procedure (single-server two-pass + custody_diff.sql invocation +
packet-order check), per-seam crash-injection, concurrent-observer, an ah repair
drift smoke, and the final promotion checklist + instant rollback note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): forged system owner -- name intercept + constants + default

Create AhBotSystemOwner.h (AHBOT_SYSTEM_OWNER_NAME/GUID + helpers),
intercept GetPlayerGuidByName so "AuctionHouse" (case-insensitive) resolves
to ObjectGuid(HIGHGUID_PLAYER, 0xFFFFFFFE) without a DB round-trip, default
AuctionHouseBot.CharacterName to AHBOT_SYSTEM_OWNER_NAME, and add the
-t ahowner self-test (TDD: RED before intercept, GREEN after).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): exclude the forged system GUID from the player allocator

Wire SkipAhBotSystemOwnerGuid into SetHighestGuids so the char-GUID
allocator's next value can never land on the reserved 0xFFFFFFFE
sentinel; add belt-and-suspenders contract assertions to -t ahowner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): self-heal the reserved system name on every load

LoadReservedPlayersNames() now seeds AHBOT_SYSTEM_OWNER_NAME into
m_ReservedNames in-memory on every call (both the early-return/empty-table
branch and the main path after the DB loop), so IsReservedName(AuctionHouse)
returns true even if the reserved_name table has been wiped. Assertion added
to the -t ahowner test verifies this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): reject player mail to the forged system owner

Gate added in HandleSendMail after the receiver GUID resolves: if
IsAhBotSystemOwnerGuid(rc) is true, send MAIL_ERR_RECIPIENT_NOT_FOUND
and return. The bot's own auction proceeds/returns use the internal
MailDraft::SendMailTo path which bypasses this handler, so the bot
still receives them. Kills COD scams and accidental gifts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahbot): don't loop-spawn the worker on an unresolved bot GUID + operator doc

Guard at the top of WorkerSupervisor::Start() returns false immediately when
m_botGuid == 0 (AuctionHouseBot.CharacterName unresolved), preventing the
supervisor restart loop from running with a useless GUID. The forged system
owner always resolves to a non-zero sentinel; 0 means misconfiguration.
Adds -t ahowner assertion that Start() refuses with botGuid=0, and adds
operator-doc note to doc/AuctionHouseBot.md explaining the reserved system
character.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ahworker): relocate ah-service to src/modules/AhWorker (no behavior change)

Move the out-of-process worker under the SD3/Eluna module convention so it is
submodule-ready. modules/ is added when BUILD_MANGOSD OR BUILD_AH_SERVICE; SD3
and Eluna self-gate on BUILD_MANGOSD AND SCRIPT_LIB_* (they link `game`), while
AhWorker self-gates on BUILD_AH_SERVICE and links only ah_ipc + shared. Binary
name, install path, and conf template are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ipc): browse wire contract (length-prefixed, deferEluna, strict decode)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ipc): validate inbound frame size on the worker client handler (I8)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahworker): item_instance.data blob decoder for enchant/suffix/charges (I9)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahworker): pure FilterAndPaginate (name filter, page slice, deferEluna set)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahworker): full-parity CanUseItem port as ah_usability lib (rep/honor/proficiency/mount/recipe-set)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(game): AhUsabilityRef evaluator (AhUseResult, D1/D2) + CanUseItem delegates + CanUseItemEluna (D5)

Caller audit: CanUseItem(proto) callers that forward specific EQUIP_ERR codes
all go through CanUseItem(Item*) which delegates here — codes preserved 1:1
by MapAhUseResult (exhaustive switch + static_assert AHUSE_OK==EQUIP_ERR_OK).
Direct proto-overload callers (ViableEquipSlots, NeedBeforeGreed, playerbot,
LookupCommands, AuctionHouseMgr) only check == EQUIP_ERR_OK; unaffected.

TDD: ahusabilityref RED (LNK2019 before cmake reconfigure), GREEN after;
ahowner GREEN (regression). Both exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ahworker): separate ah_usability_difftest exe (worker port vs production ref) (C5/I10)

Adds an EXCLUDE_FROM_ALL differential drift-test executable that runs
AhUsability::IsUsable (worker) and AhUsabilityRef::Evaluate (reference)
over a 15-item x 3-profile battery and asserts 0 drift. AhUsabilityRef.cpp
is compiled directly (not via linking game) to surface any boundary
violation at build time. Fix: ACE_DOESNT_DEFINE_MAIN must be defined before
any include on Windows to prevent ACE's OS_main.h from hijacking `main`.
Run: 45/45 agree, 0 drift, exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(ah): extract BuildListForKind in-process browse helper (fallback core, C1/I1)

Adds AuctionHouseObject::BuildListForKind, a thin dispatcher that routes
browse requests to the existing BuildListAuctionItems/BuildListOwnerItems/
BuildListBidderItems (kind 0/1/2) so Task-11 timeout fallback has a single
re-entrant call site. For BIDDER the client outbid ids are prepended in
CLIENT ORDER before the sweep, matching HandleAuctionListBidderItems exactly.
The three inner builders are completely unchanged (behavior-preserving).

Also adds AhAppendClientOutbidsForTest free helper (test seam) and
-t ahbrowsehelper smoke test locking the order-preserving contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahworker): on-demand Fetch (full parity + blob decode + I3 status) + per-thread browse thread + defer

- BrowseHandler::Fetch: cross-DB JOIN (auction x item_instance x world.item_template),
  AhItemBlob decode, locale overlay (single LEFT JOIN, LocaleConstant suffix V3/D7),
  COUNT(*) probe to distinguish FETCH_EMPTY vs FETCH_DB_ERROR (I3), D6 proficiency
  from GetItemProficiencySkill, outbid/curBid/timeLeftMs at full parity.
- BrowseThread (ACE_Based::Runnable): explicit BoundedQueue<BrowseQuery>(256,8MB),
  per-thread MySQL ThreadStart/ThreadEnd (C4), atomic stop, D4 tooMany reply on
  queue-full via SendFrame (confirmed thread-safe), FETCH_DB_ERROR sends no reply
  (mangosd TTL fallback), I8 outbound size preflight.
- Main.cpp: browse dispatch selftest (RED->GREEN), BrowseThread start after
  handshake, IPC_BROWSE_QUERY case, guaranteed join before DB/client shutdown (C4).
- --selftest: exit 0 confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ahworker): make BrowseThread diagnostic counters atomic (review)

m_processed/m_rejected/m_dbErrors were plain uint64 but written from two
threads (Submit on IPC-dispatch; run() on browse thread) — latent data race
and torn 64-bit read on non-x86. Changed to std::atomic<uint64> with
fetch_add(relaxed) at increment sites and load(relaxed) in getters, matching
the existing m_stop precedent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah): async browse proxy + pending-map (cap+GUID re-resolve+seq) + bounded Eluna pass + fallback + sub-budget

Task 11: mangosd's three AH list handlers become async proxies to the ah-service
worker. BrowsePendingMap (MAX_PENDING cap=D3; per-(char,kind) seq + playerGuidLow
re-resolve=I4; one GetGameTime clock=M5). World IPC_BROWSE_RESULT branch: tooMany
in-process fallback FIRST, then the deferred-Eluna pass (CanUseItemEluna over the
full surviving set, no per-tick budget=D5) + re-pagination; TTL sweep. C1 send-fail
/timeout in-process fallback via BuildListForKind. AhEnsureRecipeCastMap built at
the TOP of LoadAuctionItems before its empty-AH early return (V6/D9). V3 locale
LocaleConstant on the wire. I1 bidder client-outbid-first. I5 cross-faction. I6
WS_DRAIN_BROWSE_PER_CALL sub-budget. -t ahbrowsepending + regressions green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ah): add MaNGOS GPL header to new SP-1 source files

ItemInstanceFields, Usability, AhUsabilityRef, and the difftest were created
without the standard license header (the briefs only mandated it for
BrowseMessages.h). Prepend the verbatim 20-line MaNGOS GPL block; comment-only,
BOM-free, no behavior change. ah-service + ah_usability_difftest build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah): Task 11 review -- Allman braces, totalcount assert, BIDDER eluna guard, M5 comment, sweep test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ah): SP-1 conf floor (2 char DB conns), operator doc, full smoke procedure

Raise CharacterDatabaseConnections default 1->2 with reworded M4 comment
(pool floor, not a dedicated browse-thread connection). Extend
doc/AuctionHouseBot.md with the SP-1 read-model operator note (worker-up
forwarding, deferred-Eluna, fallback contract, proficiency parity).
Smoke procedure written to Memory tree (untracked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ahworker): coordinator flip pt1 -- prePaginated wire flag + over-cap pagination

SP-1 coordinator flip (worker-mandatory reads). The worker's deferred-Eluna
path no longer signals an in-process fallback on overflow; instead it
pre-paginates server-side so mangosd never has to serve the browse itself.

- BrowseResult gains uint8 prePaginated. tooMany is repurposed to mean ONLY
  "worker could not serve (queue full / oversize failsafe)" -> mangosd will
  send "AH unavailable"; prePaginated means ">cap deferred-Eluna: entries ARE
  the listfrom page, run the Lua veto on the page, do not re-paginate". The two
  are mutually exclusive by construction. (IPC_BROWSE_RESULT is a MAXLEN rule,
  so the +1 byte needs no rule change; Decode min-size guard +1.)
- FilterAndPaginate collects the listfrom..+50 survivor window alongside the
  un-paginated set; >cap -> prePaginated=1 + ship just that page (was tooMany=1
  + clear entries). The common <=cap path is unchanged (exact in-process
  parity: mangosd runs Eluna over the full set, then paginates).
- D4 (queue saturated) and I8 (oversize failsafe) keep tooMany=1 (now
  unambiguously "unavailable"); comments updated off "in-process fallback".
- selftest: codec round-trip carries prePaginated; the two strict-decode
  buffers gain the byte; new >cap test asserts prePaginated=1, 50-entry page at
  listfrom, totalcount=1200. Build clean; ah-service --selftest exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp1): coordinator flip pt2 -- mangosd serves "AH unavailable", no in-process read fallback

mangosd becomes a pure coordinator for the AH read paths. When the worker is
the authority and cannot serve, the player gets "AH temporarily unavailable"
instead of an in-process browse. Mutations stay in-process in SP-1 (unaffected).

- AuctionHouseHandler.cpp: delete AhServeBrowseInProcess; add
  AhSendBrowseUnavailable (empty SMSG_AUCTION_*_LIST_RESULT so the client does
  not hang + a red system line). The 3 read handlers: every worker-active
  transient failure (register cap / encode oversize / SendFrame fail) now sends
  unavailable; a NEW `if (sv)` branch sends unavailable when a worker is
  configured but DOWN. sv==NULL (no worker configured = legacy single-process)
  still falls through to the in-process path. AhResolveHouse is KEPT (the BIDDER
  outbid-prepend uses it -- it is not a fallback helper).
- World.cpp: forward-decl AhSendBrowseUnavailable (drop AhServeBrowseInProcess);
  TTL sweep + reply-branch tooMany -> unavailable; the deferred-Eluna pass now
  splits on prePaginated -- common case runs Eluna over the full set then
  paginates (exact parity), the >cap edge runs Eluna on the worker's pre-
  paginated page only and does not re-paginate (decision #2).
- BuildListForKind + its -t ahbrowsehelper test are kept (deleted wholesale at
  SP-6). Build clean; -t ahbrowsepending/ahbrowsehelper/ahusabilityref all OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ah-sp1): operator doc for the coordinator flip (worker-mandatory reads)

The SP-1 section described the removed in-process read fallback. Replace it with
the coordinator model: a configured-but-unavailable worker yields "AH
temporarily unavailable" (empty list + red chat), nothing served in-process; a
worker fault degrades only the AH, never the realm; no-worker builds keep the
legacy in-process AH. Add a smoke note for the accepted decision-#2 cosmetic
edge (Eluna realm + >1000 usable hits -> a few extra page buttons / sparse deep
pages) so it is not mis-flagged during testing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(ah-sp1): AH unavailable -> center-screen flash + gate the window-open when worker down

Player-facing polish for the coordinator flip:

- AhSendUnavailableMessage: one shared "AH temporarily unavailable" responder --
  a transient center-screen SMSG_NOTIFICATION flash PLUS the persistent red
  system chat line. AhSendBrowseUnavailable now routes its message through it
  (browse/owner/bidder empty-list replies + the World.cpp reply/sweep paths).
- SendAuctionHello gate: when a worker is the AH authority and it is down, do
  NOT open the auction window at all -- send the unavailable message and return.
  This is the single chokepoint for every open path (auctioneer click, gossip
  option, .auction GM commands), so the AH stays fully dark while the worker is
  down. (sv == NULL = no worker configured = legacy single-process -> open
  normally.) An already-open window cannot be force-closed in 1.12 (no opcode);
  a window opened while the worker was up then sees the browse-unavailable reply.

Build clean; -t ahbrowsepending/ahbrowsehelper/ahusabilityref all OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ah-sp1): document the center-flash + window-open gate in the unavailable behavior

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ahworker): empty filtered browse must not false-positive as cross-DB failure

The FETCH_DB_ERROR detection compared a FILTERED query's legitimate 0-row result
against an UNFILTERED `SELECT COUNT(*) FROM auction`, so any empty filtered
browse -- an OWNER list with no auctions, a BIDDER list with no bids (e.g. right
after a buyout deletes the won auction), a LIST whose item filters match
nothing, or an empty house -- was misread as a broken cross-DB JOIN. Pre-flip
that fell back to in-process (masking it); post-flip it surfaced as a 10s TTL
hang then "AH unavailable" for legitimately-empty browses.

Fix: when the filtered query returns 0 rows, probe the BARE cross-DB JOIN (no
WHERE filters). If it returns a row, the cross-DB wiring is fine and the query
simply matched nothing -> FETCH_EMPTY (serve an empty list immediately, no TTL
hang). Only if the bare JOIN is also empty while `auction` is non-empty is it a
genuine config failure -> FETCH_DB_ERROR. Verified live: bare JOIN returns 2714
(= full auction count) while buyguid/itemowner filters legitimately return 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(ah-custody): point promotion runbook at renumbered AH Repair migration

The World AH Repair migration was renumbered Rel22_05_003 -> _005 in the
database repo (collision with the merged #152 ItemAndSkinGroupFix). Update
the promotion runbook to apply the full chained World sequence (_002.._005)
so _005's version gate is satisfied, and confirm the DB tip is 22/5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ah): move custody_diff.sql to the database repo; repoint docs

The AH custody differential is SQL and does not belong in the server
repo. Relocated to the mangoszero/database repo at Tools/custody_diff.sql
(alongside the other diagnostic SQL there). Repoint the three references
(AuctionHouseBot.md, custody_crash_test.md, custody_promotion_runbook.md)
to the new location, and fix a sibling stale path (custody_crash_test.md
still pointed at the pre-relocation src/ah-service/tools/ path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ah): remediate Codex review of the coordinator flip (F1-F5, F8-doc)

Adversarially-verified findings from the SP-1 coordinator-flip code review:

- F1 (critical): a configured-but-failed worker Start() nulls the supervisor
  pointer, and the read handlers keyed unavailable-vs-legacy off that pointer,
  so a configured worker that failed to start silently reverted to in-process
  reads and opened the window -- defeating the coordinator invariant. Add a
  persistent World::IsAhServiceConfigured() authority flag (set before Start(),
  survives the failed-start teardown) and re-key all four chokepoints
  (SendAuctionHello + browse/owner/bidder down-branches) off it.
- F2 (critical): over-cap deferred-Eluna shipped a worker-paginated page that
  mangosd veto-filtered on the page only -> short page + pre-veto totalcount.
  Per the coordinator's correct-or-unavailable contract, the worker now declines
  the over-cap case (tooMany -> "AH unavailable"). prePaginated is retired
  entirely: wire field, encode/decode, the mangosd consumer branch, the
  page-window collection, and both worker selftests.
- F4: BrowseResult::Decode now fails closed on semantically-invalid flag
  combinations (booleans 0/1; tooMany mutually exclusive with elunaPending and
  carries no entries).
- F5: an invalid-UTF-8 search name now sends a terminal SMSG on both browse
  paths (proxy -> unavailable, legacy -> empty list) instead of a bare return
  that left the client's browse panel hung.
- F3: the cross-DB health probe now mirrors the real query's locales_item join
  and localized column, so a locale-schema fault reports unavailable instead of
  slipping through as a false empty result.
- F8 (documented, per maintainer decision): Eluna Player:SendAuctionMenu builds
  MSG_AUCTION_HELLO directly and bypasses the window gate -> recorded as a known
  limitation in doc/AuctionHouseBot.md rather than patching the third-party
  Eluna module.

Builds clean (RelWithDebInfo); ah-service --selftest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ahbot): remediate Codex review of the forged system owner (F6, F7)

- F6 (critical, hardening): at GUID overflow the generator set the deferred
  World::StopNow flag but still returned m_nextGuid++, which at the boundary
  equals the reserved AH-bot system-owner GUID (0xFFFFFFFE for players) -- a real
  character could be branded with the forged owner's GUID before the shutdown
  takes effect. Return the top-of-range GUID instead (never the reserved one) and
  correct the false "cannot collide" comment in AhBotSystemOwner.h. (Reaching the
  boundary needs ~4.29 billion player GUIDs, so it is effectively unreachable --
  minimal safe fix, per maintainer decision.)
- F7: an upgraded ahbot.conf carrying an explicit empty
  AuctionHouseBot.CharacterName bypassed the forged-owner default and left the
  bot GUID unresolved (0). Coalesce an empty name to AHBOT_SYSTEM_OWNER_NAME so
  the name-intercept resolves it to the reserved system GUID.

Builds clean (RelWithDebInfo).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ahworker): init BrowseQuery profile/mount fields in selftest (GCC/Clang UB)

The FilterAndPaginate usable=1 selftest built a BrowseQuery but never
initialized q.profile (PlayerProfile scalars) or q.minMountLevel /
q.minEpicMountLevel, all of which RowUsable -> AhUsability::IsUsable reads.
MSVC happened to read them as usable; GCC/Clang read uninitialized garbage and
filtered every row -> "browse selftest FAILED: defer set size=0 (exp 60)" on the
branch's first-ever Linux CI run. Pre-existing since SP-1 (Windows-only tested);
not introduced by the Codex remediation (RowUsable / the profile / that test were
untouched, and the F2 defer-branch change is behavior-identical below the cap).

Fix root + symptom:
- default-init the wire-struct scalars (PlayerProfile classId/raceId/level/
  honorRank; BrowseQuery minMountLevel/minEpicMountLevel) so a default-constructed
  query can never feed uninitialized memory to the usability check;
- give the selftest a capable profile (class 1, level 60) so its level-1 rows are
  usable deterministically on every platform.

Builds clean (RelWithDebInfo); ah-service --selftest green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MadMaxMangos added a commit that referenced this pull request Jul 6, 2026
The companion SD3 and Eluna PRs were squash-merged to their masters, so
re-point from the (now-redundant) feature-branch tips to the canonical
master commits:
  SD3   7d62a5b (enhance/dbc) -> 052bd57 (mangos/ScriptDev3 master, PR #97)
  Eluna cc67c39 (enhance/dbc) -> f7a6e59 (MangosServer/Eluna master, PR #2)
Both squash commits were verified tree-identical to the feature-branch tips.
MadMaxMangos added a commit that referenced this pull request Jul 6, 2026
#428)

* CharStartOutfit: faithful DBC layout (4x uint8 identity, bbbb fmt, correct docs)

Model race/class/sex/outfit as four uint8 fields (was one packed uint32),
fmt 'dbbbb...' so file offsets match the 152-byte record, drop the phantom
Unknown1-3, and update the single Player.cpp consumer to match on the three
identity bytes. sizeof == GetFormatRecordSize == 52 (startup assert).

* SpellRadius: document server-unused DBC fields (comment-but-correct)

* Emotes: document server-unused DBC fields (comment-but-correct)

* Lock: document server-unused DBC fields (comment-but-correct)

* AuctionHouse: document server-unused DBC fields (comment-but-correct)

* ItemBagFamily: document server-unused DBC fields (comment-but-correct)

* MailTemplate: document server-unused DBC fields (comment-but-correct)

* SpellFocusObject: document server-unused DBC fields (comment-but-correct)

* WorldSafeLocs: document server-unused DBC fields (comment-but-correct)

* CreatureType: document server-unused DBC fields (comment-but-correct)

* CreatureDisplayInfoExtra: document server-unused DBC fields (comment-but-correct)

* SoundEntries: document server-unused DBC fields (comment-but-correct)

* DBC align Batch 2: 11 tables (renames + doc-unused) + consumers

Renames (struct field -> client-verified provenance name), consumers updated:
BankBagSlotPrices price->Price; TaxiNodes name->Name_lang;
SpellCastTimes CastTime->Base; ItemClass name->ClassName_lang;
CreatureSpellData spellId->SpellId; CinematicSequences Id->ID;
QuestSort id->ID; Talent DependsOnSpell->RequiredSpellID;
ItemRandomProperties enchant_id->Enchantment; CharStartOutfit ItemId->ItemID;
DurabilityCosts Itemlvl->ID, multiplier->WeaponSubClassCost.
Builds clean (mangosd links); server-unused fields documented (comment-but-correct).

* DBC align Batch 3: 11 tables (28 renames + doc-unused) + consumers

Renames (struct field -> client-verified provenance name; ~131 consumers updated):
LiquidType Id->ID SpellId->SpellID; CreatureDisplayInfo Displayid->ID scale->CreatureModelScale;
GameObjectDisplayInfo Displayid->ID filename->ModelName; SpellShapeshiftForm flags1->Flags creatureType->CreatureType;
TalentTab TalentTabID->ID tabpage->OrderIndex; EmotesText Id->ID textid->EmoteID;
SpellRange minRange->RangeMin maxRange->RangeMax; ChatChannels ChannelID->ID flags->Flags pattern->Name_lang;
SkillLine id->ID categoryId->CategoryID name->DisplayName_lang;
ChrClasses ClassID->ID powerType->DisplayPower name->Name_lang spellfamily->SpellClassSet;
Map map_type->InstanceType (incl. inline helpers) name->MapName_lang linked_zone->AreaTableID multimap_id->LoadingScreenID.
LiquidType.Name deferred (type-contested). Builds clean (mangosd links); server-unused fields documented.

* DBC align Batch 4: 11 tables (66 renames + doc-unused) + consumers

Aligns DBCStructure.h field names + docs to the client-verified schema
(reconciliation.json worklist) for 11 tables:
  AreaTrigger, SkillRaceClassInfo, WMOAreaTable, ItemSet, WorldMapArea,
  SpellItemEnchantment, Faction, ChrRaces, FactionTemplate, CreatureFamily,
  TaxiPathNode.

66 struct-field renames + comment-but-correct docs on server-unused fields.
Renames are compile-time only: same offset/bytes, sizeof unchanged, so the
startup sizeof==GetFormatRecordSize assert still holds; DBCfmt.h untouched.
287 consumer sites across 37 game/playerbot files updated (compiler-verified
complete: mangosd links clean). Submodule pointers advanced for the SD3
(AreaTrigger id->ID) and Eluna consumer fixes, each committed on its own
enhance/dbc branch in its own repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* DBC align Batch 5: AreaTable + SkillLineAbility (19 renames + doc-unused) + consumers

Final batch of the DBC field-name alignment campaign.
- AreaTable (8 renames): mapid/zone/exploreFlag/flags/area_level/area_name/team/LiquidTypeOverride
  -> ContinentID/ParentAreaID/AreaBit/Flags/ExplorationLevel/AreaName_lang/FactionGroupMask/LiquidTypeID_3
  + 9 document-unused fields (SoundProviderPref/AmbienceID/ZoneMusic/... /LiquidTypeID_0..2).
- SkillLineAbility (11 renames): id/skillId/spellId/racemask/classmask/req_skill_value/
  forward_spellid/learnOnGetSkill/max_value/min_value/reqtrainpoints
  -> ID/SkillLine/Spell/RaceMask/ClassMask/MinSkillLineRank/SupercededBySpell/AcquireMethod/
     TrivialSkillLineRankHigh/TrivialSkillLineRankLow/ReqTrainPoints + 4 document-unused.
- 129 consumer sites fixed across 30 game/playerbot files + 1 Eluna (submodule pointer bumped).
Renames + comment-but-correct docs only; no type/width/DBCfmt change. sizeof unchanged
(AreaTable 96B, SkillLineAbility 44B) so the boot size-assert still holds. Builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* LiquidType: field 1 is the Name string, not an int (fmt i->s, char* Name)

* Spell: rename SpellEntry fields to build-exact 1.12 .dbd names + doc unused (struct only)

* Spell: fix SpellEntry consumers for .dbd rename (align)

Compiler-driven consumer fan-out for the SpellEntry struct rename (Task 2):
- ~1100 member accesses across game lib + Bots + ChatCommands renamed to the
  build-exact 1.12 .dbd names (Id->ID, EffectApplyAuraName->EffectAura,
  SpellFamilyName->SpellClassSet, SpellFamilyFlags->SpellClassMask,
  EffectImplicitTargetA/B->ImplicitTargetA/B, SpellName->Name_lang, etc.).
- Header PCH-unblock: SpellAuras.h/Spell.h/GridNotifiersImpl.h/SpellMgr.h +
  Bots/ItemVisitors.h inline accessors.
- Explicit EffectAmplitude/EffectMultipleValue collision remap (name-swap by
  column the compiler cannot catch): original EffectAmplitude sites ->
  EffectAuraPeriod, original EffectMultipleValue sites -> EffectAmplitude.
- Submodule consumer companions committed on their own enhance/dbc branches
  (SD3, Eluna); parent tracks the pointers.

Builds clean (mangosd links). No runtime change (pure member renames).

* Spell: bump SD3/Eluna to expansion-guarded accessors (cross-fork safe)

Submodule consumer renames now route through expansion guards so merging the
SD3/Eluna enhance/dbc branches upstream keeps One/Two/Three/Four building
unchanged. SD3 4e6b444, Eluna 2d9a9a3.

* AreaTrigger + SpellRange: bump SD3 pointer to guarded accessors (cross-fork safe)

SD3 8f5b6ff wraps the campaign's earlier AreaTrigger id->ID and SpellRange
min/max->RangeMin/RangeMax renames in expansion-guarded accessors
(SD3_AreaTriggerId / SD3_SpellRangeMin / SD3_SpellRangeMax), matching the
Spell/Eluna guard pattern so the SD3 enhance/dbc branch stays upstream-safe
for One/Two/Three/Four. Verified by a real One/TBC mangosscript compile: all
DBC-rename cross-fork breaks resolved (only a pre-existing powerType base-drift
in a Karazhan script remains, unrelated to DBC alignment). SD3 8f5b6ff,
Eluna 2d9a9a3 (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Cross-fork: bump Eluna pointer to guarded Batch 3-5 consumer renames

Eluna cc67c39 wraps the previously-unguarded AreaTrigger/AreaTable/
TaxiPathNode/SkillLine/ChrClasses/ChrRaces consumer renames in
ELUNA_EXPANSION==EXP_CLASSIC guards (legacy token restored verbatim in
#else), so merging the Eluna enhance/dbc branch upstream keeps One/Two/
Three/Four building. Verified by real game-target compiles on One (TBC) and
Two (WotLK): both recompiled CreatureHooks/ServerHooks/Methods and linked
game.lib with 0 errors. SD3 8f5b6ff (unchanged), Eluna 2d9a9a3 -> cc67c39.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sd3): bump pointer to cross-core GetPowerType() fix (7d62a5b)

SD3 7d62a5b drops the `#if !defined(MISTS) m_creature->powerType #else
GetPowerType()` guard (a master 005a340f regression that breaks TBC/WotLK/
Cata, which lack the Creature powerType data member) for plain GetPowerType()
at boss_shade_of_aran + boss_deathbringer_saurfang. Verified by real
mangosscript compiles: One/TBC + Two/WotLK + Three/Cata all now link with 0
errors (previously 1-2 powerType errors, no link). Surfaced by this branch's
cross-fork sweep; unrelated to the DBC alignment. SD3 8f5b6ff -> 7d62a5b,
Eluna cc67c39 (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Rename SpellEntry consumers in merged-in SpellCooldownMgr (Id->ID)

master's [Decomp][Player] (bfaf638) extracted the spell-cooldown code out of
Player.cpp into src/game/Object/SpellCooldownMgr.cpp AFTER this branch had
renamed the SpellEntry consumers - so the extracted file still read the
pre-rename spellInfo->Id at 6 sites. Renamed them to spellInfo->ID
(SpellEntry::Id -> ID). PetMgr uses no renamed SpellEntry fields.

Post-merge validation: full mangosd build links clean (game.lib + mangosd.exe,
0 errors); SpellCooldownMgr.cpp and PetMgr.cpp now compiled into the build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Point SD3/Eluna submodules at squash-merged master commits

The companion SD3 and Eluna PRs were squash-merged to their masters, so
re-point from the (now-redundant) feature-branch tips to the canonical
master commits:
  SD3   7d62a5b (enhance/dbc) -> 052bd57 (mangos/ScriptDev3 master, PR #97)
  Eluna cc67c39 (enhance/dbc) -> f7a6e59 (MangosServer/Eluna master, PR #2)
Both squash commits were verified tree-identical to the feature-branch tips.

* docs(dbc): reference structs for the 103 unmodelled client DBCs (complete 154-table map)

Adds src/game/Server/DBCStructure_reference.h — inert, build-exact reference
structs for the 103 client 1.12 DBC tables the core does not model, plus a
catalogue of all 154 (ACTIVE in DBCStructure.h vs DOC here). Generated from the
client-verified schemas by CoreAlign/tools/gen_reference.py: every field carries
its .dbd name/type and a [dbd-only] flag where the schema is dbd-inferred rather
than decomp-confirmed. The header is NOT #included by any TU (zero build cost);
activating a table = move its struct into DBCStructure.h + add its fmt + a loader.
So the core now carries a complete map of every 1.12 client DBC.

Verified: valid C++ (MSVC cl /Zs, all 103 structs, 0 errors) and adversarially
checked struct-vs-schema across all 103 (coverage/types/names/confidence flags).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

2 participants