## Summary
Migrates the Quest system gumps from legacy `Gump` to `DynamicGump` / `StaticGump<T>`.
**Renames** `Engines/ML Quests/Gumps/BaseQuestGump` to **`BaseMLQuestGump`** to
disambiguate from the Core quest abstract (`Engines/Quests/Core/QuestSystem.cs`).
Both abstracts now extend `DynamicGump`.
**Abstract bases**:
- `Server.Engines.Quests.BaseQuestGump` (Core) - now `abstract DynamicGump`. Holds constants and a static `AddHtmlObject(ref DynamicGumpBuilder, ...)` helper. Concrete subclasses each provide their own `BuildLayout`.
- `Server.Engines.MLQuests.Gumps.BaseMLQuestGump` (ML, renamed) - now `abstract DynamicGump` with a `protected abstract BuildContent(ref DynamicGumpBuilder)` hook. The base's `BuildLayout` draws shared chrome (background art, frame, label header) and then invokes `BuildContent`; subclasses use `BuildPage`, `SetTitle`, `RegisterButton`, `SetPageCount`, and content helpers (`AddDescription`, `AddObjectives`, `AddObjectivesProgress`, `AddRewardsPage`, `AddRewards`, `AddConversation`).
**Concrete Core gumps** migrated to `DynamicGump`:
- `QuestCancelGump`, `QuestOfferGump` (Core), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump`, `SheetMusicOfferGump` (Impresario), `PaintedImageGump` (renamed from `PaintedImage.InternalGump`).
**Concrete ML gumps** migrated to `DynamicGump` (extending `BaseMLQuestGump` or directly):
- `InfoNPCGump`, `QuestConversationGump`, `QuestLogDetailedGump`, `QuestLogGump`, `QuestOfferGump` (ML), `QuestReportBackGump`, `QuestRewardGump`, `QuestCancelConfirmGump`, `RaceChangeConfirmGump`.
**`StaticGump<T>` migrations**:
- `ScrollOfAbraxusGump` (Dark Tides). Its layout is a single hard-coded cliloc (1060116) with no per-instance dynamic content - safe to cache.
**Cliloc rule**: Every other quest dialog bakes per-instance cliloc IDs into its layout (quest titles, NPC names, race-specific prompts, era-conditional progress messages, escort destinations). Per the cliloc rule, baking different cliloc numbers into a cached layout would defeat `StaticGump<T>` caching - so all of these are `DynamicGump`.
**Signature changes** to support the migration:
- `QuestObjective.RenderMessage`/`RenderProgress` now take `ref DynamicGumpBuilder builder` (15 overrides updated across Collector, Solen Matriarch, Ambitious Solen Queen, Study of the Solen Hive, Terrible Hatchlings, The Summoning, Uzeraan Turmoil, Witch Apprentice, Emino's Undertaking).
- ML `BaseObjective.WriteToGump` / `BaseObjectiveInstance.WriteToGump` / `BaseReward.WriteToGump` now take `ref DynamicGumpBuilder` (KillObjective, GainSkillObjective, EscortObjective, CollectObjective, DeliverObjective, BaseReward).
**Empty-gump and `Singleton` rules**: All concrete gumps now have private constructors with static `DisplayTo` entry points that null-check the player NetState before allocation. All gumps that shouldn't stack are `Singleton => true`.
**External callers updated**: `MLQuest.SendOffer`/`OnRefuse`, `MLQuestEntry.SendProgressGump`/`SendRewardGump`/`SendReportBackGump`, `MLQuestSystem.QuestGumpRequest` and `ViewQuestsCommand`, `BoonCollector` (Darius/Nedrick), `SirHelper.OnDoubleClick` (no longer caches a single shared `InfoNPCGump` instance - `DisplayTo` constructs one per click and the gump's `Singleton => true` handles deduplication), `RaceChangeDeed.OnDoubleClick`, `PaintedImage.OnDoubleClick`, `ScrollOfAbraxus.OnDoubleClick`, `Impresario.OnTalk`, and `PlayerMobile`'s `BaseQuestGump` alias is now `BaseMLQuestGump`.
**Concrete subclasses found beyond the listed entry points**: `SheetMusicOfferGump` (in Impresario.cs), `PaintedImageGump` (was `PaintedImage.InternalGump`), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump` (the Core base has these embedded across `QuestSystem.cs`/`QuestObjective.cs`/`QuestConversation.cs`/`QuestItemInfo.cs`).
**Code-standards cleanup**: Renamed legacy `m_X` private fields to `_x` in rewritten files; braces on all control flow.
## Summary
Migrates five legacy `Gump`-derived UI dialogs in the NPC and Skill domains to the modern `DynamicGump` builder pipeline. All five gumps were chosen as `DynamicGump` rather than `StaticGump<T>` because their layout shape varies per instance, and several of them carry per-instance localization numbers (cliloc IDs) that the static cache cannot bake (see CLAUDE.md gump-system rule and `dev-docs/gump-system.md`).
Per-gump rationale:
- **`TownCrierGump` (`Mobiles/Townfolk/TownCrier.cs`)** - DynamicGump. Announcement count varies, expiration text is rebuilt per render via `ValueStringBuilder`, and one button per entry is emitted in a loop.
- **`ClaimListGump` (`Mobiles/Vendors/NPC/AnimalTrainer.cs`)** - DynamicGump. The pet list and resulting background/alpha-region heights vary per stabling player.
- **`AnimalLoreGump` (`Skills/AnimalLore.cs`)** - DynamicGump. Page count itself varies (3 pages pre-AOS, 5 pages on AOS) and several `AddHtmlLocalized` calls use cliloc IDs computed from per-creature data (loyalty rating `1049595 + c.Loyalty / 10`, food preference, pack instinct), which violates the StaticGump cliloc-bake rule.
- **`DisguiseGump` (`Items/Skill Items/Thief/DisguiseKit.cs`)** - DynamicGump. Page count and entry order shift on `from.Female`, `Body.IsFemale`, and `startAtHair`.
- **`CommentsGump` (`Gumps/CommentsGump.cs`)** - DynamicGump. Comment list and pagination depend on `Account.Comments` size; the title label encodes the variable account username string.
All five are now `Singleton => true`, have `private` constructors, and expose static `DisplayTo(...)` entry points that validate prerequisites before constructing - guaranteeing no empty gumps (CLAUDE.md Sec.13). All internal refresh paths (prompts, `OnDoubleClick`, command handlers, target callbacks) were updated to call `DisplayTo` rather than `new XGump(...)`. Legacy `m_`-prefixed fields renamed to `_camelCase` (CLAUDE.md Sec.12), and `DisguiseEntry`'s `m_`-prefixed public readonly fields converted to PascalCase auto-properties. `OnResponse` signatures updated to `in RelayInfo info`. No external callers needed updating - all `new XGump(...)` sites lived inside the same files.
## Summary
Removes per-call heap allocations from `Container`'s consume / find / group hot paths and from `BaseCreature.OnDeath`'s fame/karma tracking. The headline wins: kill the `List<List<Item>>` + `Item[][]` + `int[]` grouping bridges in `ConsumeTotal*` / `ConsumeTotalGrouped*` / `GetBestGroupAmount*`, and kill the per-call `Predicate<Item>` allocations in `FindItemsByType(Type)` / `FindItemsByType(Type[])`.
### `Container.cs`
- `ConsumeTotal`, `ConsumeTotalGrouped`, `GetBestGroupAmount` now share four streaming helpers (`HasAmount`, `TryFindGroupMeetingAmount`, `BestGroupTotal`, `ConsumeSlice`) backed by `PooledRefList` instead of allocating per-group lists and jagged arrays. Two-phase validate-then-consume pattern preserved — all-or-nothing semantics for spell reagents, vendor pay, and crafting still hold.
- `(Type)` / `(Type[])` / `(Type[][])` overload trios collapsed to single `ReadOnlySpan<Type>` + `ReadOnlySpan<int>` implementations. Implicit `T[] → ReadOnlySpan<T>` conversion means UOContent callers compile unchanged.
- Unused overloads deleted: `ConsumeTotalGrouped(Type)`, `ConsumeTotalGrouped(Type[][])`, `GetBestGroupAmount(Type)`, `GetBestGroupAmount(Type[][])`, plus the never-called `TryDropItems` hook and its private `ItemStackEntry` struct.
- Fixes a `PooledRefList` leak in `GetBestGroupAmount(Type[], …)` (missing `using`).
- `m_ContainerData` / `m_Items` / `m_TotalGold` / `m_TotalItems` / `m_TotalWeight` / `ContainerData.m_Table` / `ContainerData.logger` renamed to the underscored convention. `m_Items` cross-file rename for the Container-side references in `Item.cs`; `Item.CompactInfo.m_Items` deliberately left alone (separate effort).
- `CheckHold` parent walk simplified; trivial dispatch methods (`CheckHold` overloads, `OnItemAdded`, `OnItemRemoved`, `OnStackAttempt`) get `[MethodImpl(AggressiveInlining)]`; `Destroy` and `DisplayTo` cache `Items` outside the loop; dead comments removed.
### `Item.Enumerable.cs`
- `FindItemsByType(Type)` previously allocated a `Predicate<Item>` per call (method-group conversion). `FindItemsByType(Type[])` allocated a closure capturing `types`. Both now construct the enumerator with a `Type` / `ReadOnlySpan<Type>` field directly, no delegate.
- `FindItemsByTypeEnumerator<T>` gains two constructors plus a `Matches(T)` helper that picks the right filter inline. Constructor chaining via a private 2-arg seed constructor incidentally fixes a pre-existing bug where `PooledRefQueue` was always rented at capacity 0 because `_recurse` hadn't been assigned yet.
- `(Type[])` overload of `FindItemsByType` becomes `(ReadOnlySpan<Type>)`.
- `EnumerateItemsByType(Type)` / `EnumerateItemsByType(ReadOnlySpan<Type>)` / `ListItemsByType(Type)` / `ListItemsByType(ReadOnlySpan<Type>)` simplified to delegate to the new alloc-free overloads instead of filtering manually.
### `Utility.cs`
- `InTypeList<T>(this T, Type[])` and `InTypeList(this Type, Type[])` switched to `ReadOnlySpan<Type>`.
### `BaseCreature.cs`
- `OnDeath` per-death `List<Mobile>` / `List<int>` / `List<int>` for fame/karma tracking switched to `PooledRefList`.
## Summary
Overhauls the fame and karma system to be more era-accurate, based on original design documents and publish notes.
### Karma on player kill → karma on murder report
- Removes karma gain/loss on player kill (was immediate on death)
- Karma is now set to `Kills * -1000` on murder **report** instead
- Fame on player kill now uses the same formula as monster kills (`Fame / 100`)
This karma loss on murder report behaviour was tested on both the demo and live servers, behaviour was matching in terms of karma loss on report. Official UO servers karma loss AMOUNT match with my memory of T2A/UOR with one caveat - it's doubled on live servers (-2000 * kill count). I'm not sure when this changed and this behaviour has always had very poor and incorrect documentation, even 10-20 years ago. I was obsessed with the dread lord title on OSI and the only way I knew how to get it was reach 10 kills then macro them off. Even in publish 16 (when "The Murderer" title was removed) it still required 10 kills. Maybe it changed to -2000*Kills in AOS - that's where I've put the era gating diff.
### Era gates
- **Karma lock** (ankh toggle + auto-lock on negative karma) gated to `Core.UOTD && !Core.AOS` — [didn't exist before Jan 28, 2001](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html)
- **Felucca fame/karma +30% bonus** gated to `Core.LBR` — [added in Publish 16, July 2002](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/)
- **Fame/karma splitting** among damage dealers gated to `Core.UOR` — pre-UOR awards go to last hit only
### Skill karma penalties
- **Provocation** on innocent NPC: NPC says cliloc 501591, karma loss (floor -7500)
- **Stealing** attempt: karma loss on every attempt (floor -5000). Stealing did not cause karma loss at all before!
- **Summon Daemon**: karma loss on successful cast (floor -7000)
- **Corpse carving** (human): -70 for innocent corpses (floor -7000), -20 for freely-aggressable (floor -2000)
- **Bounty head turn-in**: karma gain capped at 2000 (was awarding flat +2000)
### Beneficial action karma
- **Beneficial spells** (heal, cure, etc.): `AwardKarma(caster, target.Karma / 5)` — healing good targets raises karma, healing evil targets lowers it - source is UO98 demo scripts
- **Bandages**: same formula but gain only (skipped if target karma ≤ 0) - see stratics link ("only ever gain karma, not lose it")
Sources: UO98 Demo scripts and playing, [Fame and Karma wiki](https://uo.com/wiki/ultima-online-wiki/player/fame-and-karma/), [UO design doc (Jan 2001)](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html), [Publish 16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/), [Stratics healing reference](https://web.archive.org/web/20001209014200fw_/http://uo.stratics.com/heal.shtml)
### Refactoring
- Extracted `Titles.ComputeKillAwards(killed, map)` shared by player kill and creature kill paths
- Extracted `Titles.SetKarma(m, value, message)` for direct karma assignment (used by murder report)
- Extracted `SendKarmaMessage` and `CheckKarmaLock` helpers from `AwardKarma`
## Summary
- Fixes pets falling behind mounted masters in AOS+ by setting `CurrentSpeed = 0.1` when following master
- Fixes AI timer permanently stopping when `Obey()`/`Think()` returns `false` for transient conditions
- Fixes controlled pets losing AI in inactive sectors (pet follows owner across sector boundary, sector deactivates, AI dies)
- Adds defense-in-depth: AI timer restarts on pet resurrection and order changes
## AI Timer Permanent Stop (Bug Fix)
`AITimer.OnTick()` called `Stop()` when `Obey()` or `Think()` returned `false`. By that point, `ShouldStop()` had already validated the creature is alive, on a valid map, and in an active sector — so any `false` return was a **transient** condition, not terminal. The timer stopped permanently with no mechanism to restart it.
**Scenarios that triggered permanent AI death:**
- Dead bonded pet with attack order (`DoOrderAttack` returned `false` for `IsDeadPet`)
- Failed pet transfer — loyalty refusal, combat, disconnected player, or pending trade (`DoOrderTransfer` returned `false` for 5 different transient conditions)
- Unknown `OrderType` or `ActionType` (defensive defaults)
**Fixes:**
- Removed `Stop()` from the `Obey()`/`Think()` failure path — timer skips the tick and fires again next interval
- Changed `DoOrderAttack()` and all five `DoOrderTransfer()` failure paths to return `true` (correct semantics: these are recoverable states, not "stop AI forever" signals)
- Added `Activate()` call in `ResurrectPet()` — ensures dead bonded pets have AI running after resurrection
- Added `Activate()` call in `OnCurrentOrderChanged()` — self-heals timer if any voice command is issued to a pet with a stopped timer
## Controlled Pet Sector Deactivation (Bug Fix)
`ShouldStop()` stopped the AI timer for **all** `PlayerRangeSensitive` creatures in inactive sectors, including controlled pets. But `Deactivate()` intentionally exempted controlled pets. The exemption was dead code — `ShouldStop()` bypassed it.
This matters when a pet follows its owner across a sector boundary: the owner enters the next sector (active), the pet's old sector deactivates (no more players), and the pet's AI dies. The pet stops following and stands there until the player backtracks far enough to reactivate the sector.
**Fix:** Added `Controlled` check to `ShouldStop()` to match `Deactivate()`. Controlled pets now keep their AI running in inactive sectors. The overhead is negligible — controlled pets are bounded by follower slots.
## Movement Speed Simplification
- Simplifies `AITimer` to use `CurrentSpeed` directly as the tick interval (in seconds), removing the complex multiplier/floor logic in `GetBaseInterval`
- Refactors `DoMoveImpl` speed assignment into explicit if/else for clarity
- AOS+ pets following master use `CurrentSpeed = 0.1` (100ms), matching `RunMountDelay`
## Files Changed
- `AITimer.cs` — removed `Stop()` on Obey/Think failure, added `Controlled` exemption to `ShouldStop()`, simplified interval logic
- `BaseAI.cs` — renamed `_timer` to `AITimer` (public), simplified `Deactivate()`, fixed `ReturnToHome` to use `Activate()`
- `PetOrders.cs` — `DoOrderAttack` and `DoOrderTransfer` return `true` for transient failures
- `PetOrderHandlers.cs` — `OnCurrentOrderChanged()` calls `Activate()` to self-heal stopped timers
- `BaseCreature.cs` — `ResurrectPet()` calls `Activate()`, fixed `GoHome_Callback` PlayerRangeSensitive check
- `AIMovement.cs` — refactored speed assignment, AOS+ follow-master speed fix
## Summary
- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns
## InterpolationHandler Design
`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:
- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields
**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.
This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool
## Changes by Category
### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)
### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter
Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)
### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)
### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop
### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops
### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section
## Test Plan
- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
- Stackalloc no-grow, stackalloc with grow (→pool transition)
- Heap no-grow, heap with grow, heap double grow
- Pre-existing content with and without grow
- Sequential multiple `Append($"...")` calls
- Mixed plain + interpolated Append
- Empty interpolation, literal-only, format specifiers
- Null string holes, ISpanFormattable types
- Dispose after stackalloc→pool grow
# Bounty Boards
<img width="717" height="382" alt="image" src="https://github.com/user-attachments/assets/455e5206-47d8-4449-805c-19b143d059e5" />
<img width="918" height="637" alt="image" src="https://github.com/user-attachments/assets/e78e1ca2-62b8-4abf-8f69-21438bb1b759" />
<img width="340" height="296" alt="image" src="https://github.com/user-attachments/assets/fd17d7e6-8c53-47df-ac58-a89ea3ef665e" />
## Setup
* Setup as part of decorate when bounty system is enabled
* Several bounty board locations with a WarriorGuard spawner in front of the board. Guard spawns and idles around 5 range.
## Tests
### ReportBountyMurdererGump
* Follows same behaviour as ReportMurdererGump
* Extracted common logic
* Didn't use staticgump due to several dynamic parts including input
* Optional bounty with validation >0 and <bankbox.total
* Murder report is honored either way
### Bounty boards
* Open bounty board with many bounties, no bounties
* Keep a bounty board open, invalidate a bounty by turning in the head, then try to click on the post of the now invalid post. As expected: does nothing
* Bounty messages use the last murder time as their post date and expire in 14 days
* Bounty boards/messages are not reliant on serialization; they use serialized fields from MurderContext to build messages when clicked.
* Bounty messages use synthetic serials so they do not have to persist bounty messages as items. They are constructed and sent as raw packets when needed.
* Players only appear on the bounty board if they are a murderer (though a non-murderer can technically still have a bounty if they decayed kills)
* Tested skin/hair color descriptions vs a dozen spot checks
### Head turn in behaviour
* Guard accepts head
* Bounty -> gives bounty
* No bounty -> generic response
* Expired head (24h) -> generic response
NOTE: CUO "latest" has a bug with bounty/bulletinmessages that causes overflow outside of the container. It has nothing to do with this PR. It is fixed here in CUO https://github.com/ClassicUO/ClassicUO/pull/1871
# Murderer title
Bounty system and "murderer" title eliminated in [pub16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/). So UO:LBR and before had bounties and murderer title.
# Pre-T2A caveat
There were differences in pre-T2A, but this cannot be currently implemented because we do not have any pre-T2A systems in general, so at least for now, behavior is consistent with later eras. Pre-T2A was a whole other ballgame, but the bounty system still worked similarly.
## Summary
- Adds the Endless Decanter of Water (introduced in Publish 66.2 / SA era)
- Players throw a full Pitcher of Water at a Water Elemental for a 10% chance to receive the decanter; the pitcher is always destroyed on impact
- Each Water Elemental can only yield one decanter; the state is persisted and previously saved elementals are migrated to the new serialization version
- The decanter auto-refills from a linked water trough when the owner empties it within 10 tiles of the stored trough location
- Linking stores a Point3D + Map snapshot, supporting both static tile troughs and addon troughs
- The decanter is blessed and displays Linked/Unlinked status in its tooltip
## Summary
- Adds `Mobile.Murderer` virtual property and consolidates kill-threshold checks across the codebase
- Tracks ping-pong count: how many times a player crosses the 5-kill murderer threshold (T2A/UOR/UOTD only, disabled on LBR+)
- Adds `[CommandProperty]` to view a player's ping-pong count via the admin panel
- After enough ping-pongs, player is permanently flagged as a murderer regardless of kill count
- Accounts for perma-red players with low kills in murderer status transition notifications
- Implements era-appropriate "I must consider my sins" speech responses:
- **T2A**: contextual cliloc flavor text (502122–502126)
- **UOR–AOS**: raw short/long-term murder counts + ping-pong count if applicable
- **SE+**: localized stats message (1114370)
- Refactors kill-report logic out of `Keywords.cs` into `PlayerMurderSystem.ReportKillsToSelf`
## Testing
- [x] Thoroughly tested and self reviewed
- [x] Test T2A "I must consider my sins" behaviour over all scenarios.
- [x] Test UOR "I must consider my sins" behaviour over all scenarios.
- [x] Test that LBR does not have ping pongs enabled (I must consider my sins)
- [x] Test serialization cross over from v0 -> v1 increments 1 ping pong if player is already red.
## Notes
* Manually setting kills to 5 does not trigger a ping pong, it must go through the actual murder system. This includes if the kills were manually set to 5 and then migrated (as manually setting kills to 5 never adds the player into the murder system - it only happens via ReportMurderer). This is arguably a bug in the existing system, but one that currently only ever happens via staff interaction.
* Thieves guild SuspendOnMurder specifically checks for kills > 0. This means a person with 0 shorts but 5 ping pongs (flagged as murderer) can steal. This may be accurate, as according to a forum post this is how it works on UOSA which is the T2A gold standard.
* This doesn't implement Pre-T2A behaviour which should be that "I must consider my sins" does nothing at all. The reason I didn't implement it for Pre-T2A is then it 100% have to sit behind a feature flag. I don't mind adding it as a feature flag, just let me know.
## Summary
- Adds `public virtual bool Murderer => Kills >= 5` property to `Mobile`, replacing ~30 scattered `Kills >= 5` / `Kills < 5` magic-number checks across the codebase
- `BaseCreature` overrides `Murderer` to also return `true` when `AlwaysMurderer` is set
- Updates `Corpse` serialization to v15, storing `_murderer` as a `bool` field (migrated from `int Kills >= 5` in earlier versions)
### Summary
Updates all calls to container.EnumerateItems() to properly dispose of the underlying PooledRefQueue so that we are properly recycling pooled arrays.
### Summary
- Fix AccountGold gold duplication exploit: When AccountGold.Enabled was true, double-clicking a BankCheck deposited
the full value to the account but then continued creating physical gold piles for the same amount, duplicating the
value
- Fix Deposit/DepositUpTo partial deposit exploit: Both methods created new max-size gold piles and checks without
first filling existing partial stacks, wasting container slots and causing premature "bank full" failures that could
be leveraged to manipulate gold distribution
- Fix BagOfSending bank stacking: Gold and BankCheck items sent via BagOfSending now use Banker.Deposit for efficient
stacking instead of naive TryDropItem, which could fail on a full bank even when existing piles had room
- Improve gold/check deposit efficiency: Banker.Deposit and Banker.DepositUpTo now top off existing gold piles (up to
60k) and bank checks (up to 1M) before creating new items, maximizing use of available container slots
### Summary
- Adds CanSpawnMobile(x, y, minZ, maxZ, canSwim, cantWalk, out spawnZ) overload for finding spawn surfaces within a Z range
- Adds CanSpawnItem(x, y, minZ, maxZ, out spawnZ) for item spawning with Surface+Impassable support (tables, furniture)
- Uses bitmask optimization inspired by Item.DropToWorld's m_OpenSlots pattern for O(1) surface/blocker checks
- HomeRange spawners now use surface detection to set proper Z bounds
### Key Changes
Map.cs:
- CanSpawnMobile with Z-range finds lowest valid surface for mobiles
- CanSpawnItem with Z-range finds lowest valid surface for items (including tables)
- CanFitItem for point-check item placement on Surface+Impassable tiles
- Bitmask approach eliminates nested loops and stackalloc arrays
Spawners:
- Simplified GetSpawnPosition using new Z-range methods
- HomeRange setter detects surface below spawner for proper Z bounds
- Consistent handling for mobiles and items
### Bug Fixes
- Water tiles (Impassable | Wet) no longer block swimming mobs
- Items can now spawn on tables/furniture (Surface+Impassable)
### Test Plan
- Run dotnet test - 631 tests pass
- Manual testing: multi-story spawning, water mobs, item spawning on tables
- Verify HomeRange spawner movement shifts bounds correctly
### Summary
This PR transitions the spawner system from a simple radius-based model to a flexible 3D boundary system.
### Core Changes
* **Replaced `HomeRange` with `SpawnBounds`**: Spawners now use a `Rectangle3D` to define spawn areas instead of a circular integer range.
* **Backward Compatibility**:
* The `HomeRange` property remains as a helper that generates square `SpawnBounds` centered on the spawner.
* Included a migration path (v10 to v11) that automatically converts old range data into new bounds during deserialization.
* **Dynamic Bounds Shifting**: If a spawner is moved, its `SpawnBounds` will automatically shift with it, provided the bounds are currently configured as a centered square.
* **New Spawn Logic**: Added `SpawnLocationIsHome` toggle. If enabled, spawned mobiles treat their exact spawn coordinates as their "Home" rather than the spawner's location.
### Implementation Details
* **Interface Updates**: Updated `ISpawner` to include `WalkingRange`, `SpawnBounds`, and `IsInSpawnBounds()`.
* **UI Enhancements**: The Spawner Controller Gump now displays "Custom" for complex bounds and allows copying of the new boundary properties between spawners.
* **Refactored Constructors**: Streamlined `BaseSpawner`, `ProximitySpawner`, and `RegionSpawner` constructors to support the new data types.
> [!IMPORTANT]
> **Developer Note**
> THIS IS A BREAKING CHANGE TO THE NEW API GUMP.
> Please give us feedback in [discord ](https://muo.gg/discord) if you have issues, need help, or have ideas for a better API change!
### Summary
* Adds support for size/style to dynamic/static builder.
* Drastically simplifies the dynamic/static builder api for AddHtml.
* Cleans up some legacy gump files.
### Summary
* Refactors AI so it is easier to read and maintain
* Fixes NPC speed issues
* Fixes pet sector AI issue that was causing stuttering
* Fixes direction snapping for Melee/Mage AI
* Refactors pet orders
* Refactors speech commands
* Removes scale speed by dex for HS+ (it was a stupid feature anyways)