Commit graph

148 commits

Author SHA1 Message Date
7a4daa801e #W# Change: Trammel is now Underworld. 2026-08-31 18:29:10 -04:00
7cf3e68b37 #W# Change: Felucca is now Veridia 2026-08-31 18:00:19 -04:00
Kamron Batman
2935eafe24
feat: convert all delta-time serialization to anchored time (#2589)
## Summary

Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save.

The answer to "is it possible everywhere": **yes** — including the one case that looked impossible.

## The GenericPersistence problem, solved

`GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected.

## Converted

- **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`.
- **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`.
- **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version.

**Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them.

## Verification

- Build 0 errors / 0 warnings; **837 + 708 tests green**.
- Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched.
- **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties).

## Notes for review

- `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes.
- BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes.

## Enforcement

`WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
2026-08-22 19:34:43 -07:00
Kamron Batman
b042edcf0b
refactor: fold hand-written serializable property setters into field hooks (#2587)
## Summary

Folds **113** hand-written `[SerializableProperty]` members into plain `[SerializableField]` declarations using the v4 setter hooks — value coercion/vetoes via `allowFieldChange`, post-change side effects via `fieldChanged` (whose `oldValue` parameter covers the old-house/old-sender unsubscribe patterns). Net **-450 lines** of setter boilerplate.

```cs
// before
[SerializableProperty(1)]
[CommandProperty(AccessLevel.GameMaster)]
public int Charges
{
    get => _charges;
    set
    {
        _charges = Math.Clamp(value, 0, MaxCharges);
        InvalidateProperties();
        this.MarkDirty();
    }
}

// after
[SerializableField(1, allowFieldChange: nameof(AllowChargesChange))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[InvalidateProperties]
private int _charges;

private bool AllowChargesChange(ref int value)
{
    value = Math.Clamp(value, 0, MaxCharges);
    return true;
}
```

## How sites were selected

A classifier parsed all 204 `[SerializableProperty]` sites and converted only those matching strict shapes: getter is exactly `get => _field;`, the assignment comes first (after at most an equality guard), and relocated side effects contain no `return`, no `value` mutation, and no field re-assignment. Everything else was left alone deliberately:

- **~34 custom getters** (fallback defaults like `_x == -1 ? Default : _x`, self-healing refs) — no setter hook can express these.
- **~35 pre-assignment logic** (durability Unscale/Scale sandwiches, old-state captures like PotionKeg's pile weight).
- **virtual/override members, name-mismatched backing fields (`m_`), exotic semantics** (guards' `Focus` does work on *equal* assignment; `ChampionSpawn.Active` never assigns its field).

Five sites the classifier refused were converted by hand where the hooks fit cleanly: `ReceiverCrystal.Sender`, `PlayerVendor.House`, `PlayerBarkeeper.House` (old-value unsubscribe via `oldValue`), `BaseSuit.AccessLevel` (its existing virtual `OnAccessLevelChanged` already had the exact callback shape), and `DyeTub.DyedHue` (a true veto: `AllowDyedHueChange(ref int value) => _redyable`).

## Verification

- Build: **0 errors, 0 warnings**.
- **Schema regeneration produces zero Migrations changes** — the conversion is wire- and schema-neutral by construction (same orders, types, and property names), and CI's schema diff check enforces it.
- **835 + 708 tests green.**

## Behavioral notes (all strict improvements, called out for review)

- Generated setters skip everything when the incoming value equals the current one; a few converted setters previously re-ran side effects on equal assignment (redundant `Update()`-style refreshes).
- Generated setters always `MarkDirty()` on change; several converted setters never did (e.g. `DyeTub.DyedHue`, `MorphItem` ranges) — their changes only persisted if something else dirtied the entity. Those latent persistence bugs are fixed by construction.
2026-08-22 18:20:39 -07:00
Kamron Batman
73f9688083
feat: adopt serialization generator v4 (field-side linkage, anchored timers) (#2586)
## Summary

Adopts ModernUO.Serialization 4.0.0 across the engine. Three commits, reviewable independently:

1. **Package + tool bump to 4.0.0** (`Server.csproj`, `UOContent.csproj`, `dotnet-tools.json`).
2. **Timers → `[DeserializeTimer]`** — the 8 drifting timers (BaseLight, TreasureMapChest, MarkContainer, FillableContainer, DeathRobe, DecayedCorpse, Corpse, BaseEscortable) now store their next tick as **anchored time**: server downtime no longer consumes the remaining delay, and idle-world saves are byte-stable. This changes their wire format, so each class bumps its serialization version with a `MigrateFrom` that replays the old delta-time read through the migration schema (the new `vN.json` files carry `@AnchoredTimer`; the old ones keep `@TimerDrift`, which the generator reads forever). The 2 wall-clock timers (Aquarium, FountainOfLife) keep their exact format via `wallClock: true` — no bump. Restart methods drop their `TimeSpan.MinValue` sentinel checks: v4 invokes them **only when a timer was actually running at save**.
3. **Linkage → field-side declarations** — 175 conversions across 25 files: `[SerializableFieldSaveFlag(order)]`/`[SerializableFieldDefault(order)]` become `[SaveFlag(nameof(...), nameof(...))]` on the field, and `[SerializableFieldChanged(order)]` becomes the `fieldChanged:` argument of `[SerializableField]`. **Wire-neutral: zero migration schemas changed.**

## Verification

- Solution builds with **0 errors, 0 warnings**; all three 4.0.0 packages verified indexed on nuget.org (no local feed needed).
- **835 + 708 tests green.**
- Generated output inspected: old-version content structs replay `ReadDeltaTime` (e.g. `V3Content.DecayTimerNext = reader.ReadDeltaTime()`), current versions write/read anchored time with the gated restart, and the wall-clock classes emit byte-identical `Write`/`ReadDateTime` framing.
- Schema tool run is committed (CI's `git diff --exit-code` schema check passes): exactly the 8 expected new `vN.json` files, nothing else touched.
- The conversion was scripted with a class-scoped resolver (order → same-class `[SerializableField(order)]`/`[SerializableProperty(order)]`); it planned 175/175 with zero ambiguities before applying.

## Notes

- New `MigrateFrom`s use the content structs' provided `XxxDelay` property, matching the pre-existing idiom in Corpse's and TreasureMapChest's older migrations.
- Follow-up candidate (separate PR, wire-neutral, any time): fold the ~150 eligible hand-written `[SerializableProperty]` setters (clamps, post-change side effects) down to `[SerializableField]` with `allowFieldChange`/`fieldChanged` hooks.
2026-08-22 17:54:02 -07:00
Kamron Batman
cce035f1c3
fix: Removes unnecessary dictionary removal guards (#2565)
## What

`Dictionary<K,V>.Remove` and `HashSet<T>.Remove` do not bump the collection's version, so removing an entry during a `foreach` does not invalidate the enumerator. A number of loops were still paying for a `PooledRefQueue`/`PooledRefList` to collect keys and drain them in a second pass. This drops those guards.

## Why it's safe

Verified against .NET 10.0.10 rather than taken on trust, since the documented guarantee covers only `Dictionary<TKey,TValue>.Remove` while several of these call sites are `HashSet<T>` or enumerate `.Keys`/`.Values`:

| Case | Result |
|---|---|
| `Dictionary` foreach + `Remove` | safe, all entries visited |
| `Dictionary.Keys` / `.Values` foreach + `Remove` | safe, all entries visited |
| `HashSet` foreach + `Remove` | safe, all entries visited |
| `Dictionary` foreach + `Remove` **then `Add`** | throws `InvalidOperationException` |

Reflection on `_version` confirms the mechanism: neither `Dictionary.Remove` nor `HashSet.Remove` touches it. Because `Remove` never bumps the version, the `Keys` and `Values` enumerators are just as safe as the dictionary's own, even though only `Dictionary.Remove` documents the behaviour. No entries were skipped in any case.

The `HashSet` half is confirmed by [stephentoub on dotnet/dotnet-api-docs#8177](https://github.com/dotnet/dotnet-api-docs/issues/8177#issuecomment-1167251052): *"Both HashSet and Dictionary have been improved to support removal during enumeration. The docs may just benefit from updating."* The gap is in the documentation, not the runtime.

`Remove` followed by `Add` in the same enumeration still throws. That is the line this PR does not cross.

## Guards removed

`VisibilityList`, `ChampionTitleSystem`, `Channel`, `BombingRun`, `Ruleset`, `PuzzleChest`, `RaceChangeGump`, `StepCache`, `PlayerMurderSystem`, `VirtueSystem`, `ProjectedItem`, `StaminaSystem`, `AIGroupMovement`, `PromotedGuard`, `AutoDenylist`, `LoginAllowlist`, `AntiMacroSystem`, `DetectHidden`.

Both collection kinds are covered: `Dictionary` (including loops over `.Keys` and `.Values`) and `HashSet` (`ProjectedItem._active`, `PlayerMurderSystem._contextTerms`, `StaminaSystem._resetHash`). In `StaminaSystem.ResetTimer` the `Count == queue.Count → Clear()` branch goes away with the queue — it only existed to avoid paying for N individual removes.

Where the collection supports it, `Contains` + `Remove` and `TryGetValue` + `Remove` also collapse into a single lookup (`if (list.Remove(x))`, `if (m_Pending.Remove(ns, out var state))`).

`Utility.Tidy<K,V>` keeps its two branches: when `K` is serializable the value is not inspected, otherwise the value is. Only the serializable side may be cast, so `Dictionary<Mobile, int>` and `Dictionary<Mobile, string>` stay valid.

## Deliberately unchanged

**`BaseCreature.LoyaltyTimer.OnTick`** keeps its deferred-delete queue. Removing from `World.Mobiles` while enumerating it is safe, but `Mobile.Delete()` is not a `Remove` — it runs `OnDelete`/`OnAfterDelete`, the `OnParentDeleted` cascade over the creature's pack, `DropHolding()`, and region and guild callbacks. Anything in that surface that constructs a `Mobile` is an `Add` into the dictionary being enumerated, which does invalidate it. `BaseHire.PayTimer.OnTick` has the same shape and is likewise untouched.

**Spatial-query buffers** — `GuardedRegion.CallGuards`, `Thunderstorm`, `Exorcism`, `LeverPuzzleController`, `BaseCreature.TeleportPets` — are a different hazard. They buffer the result of a range query because the drain moves or harms mobiles, which mutates sectors mid-enumeration.

**Re-entrant drains.** The `_users` sets in `Firebomb` and the explosion, conflagration and confusion-blast potions look like this pattern but are not: the loop collects, `Clear()`s, and only then runs `Target.Cancel` on each, which can re-enter. `AnimalTrainer` enumerates `pm.Stabled` and drains through `RemoveStabled`, which nulls the `Stabled` field once it empties — safe for an in-flight enumerator, which holds the set reference rather than the field, but subtle enough not to be worth inlining on a cold path.

## Verification

`dotnet build` clean with 0 warnings; 810 Server and 684 UOContent tests pass.
2026-08-08 11:50:01 -07:00
Kamron Batman
a8acfa31f8
refactor: decompose mobile/corpse hair, delete VirtualHairInfo, fix removal serial (#2462) (#2463)
Fixes #2462

## Summary
Removes the per-object `VirtualHairInfo` heap wrapper for mobile/corpse hair. Hair is now stored **inline** on `Mobile` and `Corpse` as `int _hairItemId` / `int _hairHue` plus a lazily-allocated, **non-serialized** ephemeral `Serial _hairSerial` (in the high virtual-serial range) — and likewise for facial hair. The `VirtualHairInfo` class is deleted, with a **lossless** save migration.

This delivers three things:
1. **Fixes a hair-removal bug.** `Delta(MobileDelta.Hair)` is deferred (it enqueues; `ProcessDeltaQueue` runs later in the tick). The old `HairItemID = 0` setter nulled `_hair` *immediately*, so by the time `ProcessDelta` built the remove packet the equipped virtual serial was already gone — the old `??=` code then re-materialized a **fresh** serial (≠ the equipped one), so clients never removed the right entity, and it left a phantom ItemId-0 object behind. The serial now lives on the entity and **persists across removal**, so remove packets carry the correct serial.
2. **Lightens the entity.** No heap hair object; bald mobiles allocate nothing (the serial is minted lazily only when hair is present). This was the original reason `HairItemID`/`HairHue` exist.
3. **Removes `VirtualHairInfo` entirely**, keeping the high-range virtual serial behavior.

## How
- **Mobile** (manual serialization): inline `_hairItemId/_hairHue/_hairSerial` (+facial); lazy `HairSerial`/`FacialHairSerial`; `ProcessDelta` reads those. Serialization **v36 → v37** — the v30-v37 deserialize is unified, reading the legacy per-hair `VirtualHairInfo` version int only when `version < 37`. Setting item id to 0 clears the hue (matching the old object-nulling) while retaining the serial.
- **Corpse** (codegen serialization): decomposed to `[SerializableField] int _hairItemId/_hairHue` (+facial) + ephemeral serial; **v16 → v17** with `MigrateFrom(V16Content)`.
- **Lossless migration:** the loader validates exact byte length, and the old corpse hair is a presence-bool-gated block, so a tiny **migration-only** `LegacyHairInfo` reader (no runtime role) consumes the legacy `[bool][int ver][int itemId][int hue]` bytes. Frozen `Corpse.v14/v15/v16.json` are retyped to it; `v17.json` describes the new int fields.
- All consumers updated to discrete accessors: `OutgoingMobilePackets`, `CorpsePackets`, corpse subclasses (`MilitiaFighterCorpse`, `SchmendrickApprenticeCorpse`), and the packet test mirrors.
- `VirtualHair.cs` renamed to `OutgoingVirtualHairPackets.cs` (the only type left in it after `VirtualHairInfo` was removed).

## Test Plan
- [x] Full solution build: **0 warnings, 0 errors** (`TreatWarningsAsErrors`).
- [x] `Server.Tests`: **708 passed** (incl. new `RemoveHairUsesEquippedSerial` / `RemoveFacialHairUsesEquippedSerial` proving the serial survives removal + hue clears).
- [x] `UOContent.Tests` corpse/hair: **6 passed** (incl. `CorpseHairMigrationTests` asserting the legacy hair bytes are consumed exactly — the loader's length invariant).
- [x] Generated migration code inspected: V14/V15/V16 readers consume the legacy block byte-for-byte; serial never written to disk.

## Upgrade notes
- Old Mobile (v30–v36) and Corpse (v13–v16) saves load losslessly.
- Minor cosmetic-only change: `SchmendrickApprenticeCorpse` hair/facial-hair RNG draws shift order within each pair (same draw count); irrelevant for a quest NPC corpse.
2026-06-06 14:33:28 -07:00
Chuck Thier
b150c48328
feat: Adds rope teleporter for New Haven Mines (#2439)
### Summary

- Add InteractiveTeleporter that teleports on double-click
- Add support to decorate command
- Add rope teleporters to the New Haven mines in the decoration file
2026-05-03 17:23:31 -07:00
Kamron Batman
29e4ecdb1b
fix: Preserve corpse notoriety across server restart (#2426)
BaseCreatures are deleted on death (Mobile.OnDeath calls Delete for non-players), so after save/restart the corpse's _owner reference resolves to null. CorpseNotoriety gated its entire creature branch on `target.Owner is BaseCreature`, falling through to player-corpse logic once the reference vanished. That made monster corpses turn red (body.IsMonster -> Murderer) and innocent NPC corpses turn grey (null is not PlayerMobile -> CanBeAttacked) on the next restart.

Snapshots the relevant owner state into CorpseFlag at corpse creation: OwnerWasBaseCreature, OwnerWasSummoned, OwnerWasAnimatedDead. Folds the standalone _murderer bool into CorpseFlag.Murderer for consistency with Criminal. CorpseNotoriety now consults the flags so the creature branch stays correct without a live mobile reference.

Bumps Corpse serialization to v16 with a MigrateFrom(V15Content) that maps the old Murderer bool onto the new flag. Pre-fix corpses already on disk decay within 7 minutes; their first post-restart color may be wrong, which is acceptable.

Also documents that the schema generator must be run after every version bump (`dotnet tool run ModernUOSchemaGenerator -- ModernUO.slnx`) since `dotnet build` does not emit migration JSON files.
2026-05-03 02:15:01 -07:00
Kamron Batman
598c3c125c
perf: Migrate Item Interaction gumps to DynamicGump/StaticGump (#2421)
## Summary

Migrates 11 player-facing Item Interaction gumps from legacy `Gump` to `StaticGump<T>` or `DynamicGump`.

| File | Gump | Base | Reason |
|---|---|---|---|
| `Items/Deeds/NameChangeDeed.cs` | `NameChangeDeedGump` | `StaticGump<T>` | Fully fixed layout |
| `Items/Deeds/HairRestylingDeed.cs` | `HairRestylingDeedGump` (renamed from `InternalGump`) | `DynamicGump` | Per-race/per-gender cliloc IDs and gump images vary every instance |
| `Items/Deeds/HolidayTreeDeed.cs` | `HolidayTreeChoiceGump` | `StaticGump<T>` | Fully fixed (Classic/Modern radio) |
| `Items/Deeds/NewPlayerTicket.cs` | `NewPlayerTicketGump` (renamed from `InternalGump`) | `StaticGump<T>` | Fully fixed gift menu |
| `Items/Misc/PromotionalToken.cs` | `PromotionalTokenGump` | `DynamicGump` | `TextDefinition` (`ItemGumpName`) varies per token subclass — cliloc/string differs |
| `Items/Misc/SpecialHairDye.cs` | `SpecialHairDyeGump` | `StaticGump<T>` | Static entry array drives a fixed layout |
| `Items/Misc/InteriorDecorator.cs` | `InteriorDecoratorGump` (renamed from `InternalGump`) | `DynamicGump` | Button art flips per `decorator.Command` (Turn/Up/Down) |
| `Items/Special/8th Anniversary Items/Dawn's Music Box/DawnsMusicBoxGump.cs` | `DawnsMusicBoxGump` | `DynamicGump` | Page count derived from variable `Tracks.Count` |
| `Items/Misc/PowerGenerator.cs` | `PowerGeneratorGump` (renamed from `GameGump`) | `DynamicGump` | Variable side length 3-6 and per-step node hues |
| `Gumps/HeritageTokenGump.cs` | `HeritageTokenGump` | `StaticGump<T>` | All 7 pages fully baked — only static cliloc IDs |
| `Gumps/ConfirmHeritageGump.cs` | `ConfirmHeritageGump` | `DynamicGump` | Confirmation cliloc chosen at runtime per item selected |

All gumps use `Singleton => true`, private constructors, and a static `DisplayTo` entry point that validates prerequisites before constructing.

External callers updated (`HeritageToken`, `DawnsMusicBox`) to the new `DisplayTo` entry points. `Console.WriteLine` in `ConfirmHeritageGump` exception handler replaced with `LogFactory`-backed logger.
2026-04-26 11:31:49 -07:00
Kamron Batman
802849bebc
perf: Migrate Bulletin/Poll/SOS gumps to DynamicGump (#2418)
## Summary

Migrates three legacy `Gump`-based dialogs to the modern builder API:

- **PlayerBBGump** (`Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs`) — `DynamicGump`. The bulletin board renders a different post per page (variable per-instance state), so the layout cannot be cached. Now `Singleton`, with a private constructor and a static `DisplayTo` entry point. Scroll/banish/delete/post-props buttons mutate `_page` and self-refresh via `SendGump(this)` instead of allocating a fresh gump on every click. Prompt-driven flows (post message / set title / post greeting) re-enter through `DisplayTo` after the prompt completes.
- **MessageGump** + **OldMessageGump** (`Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs`) — `StaticGump<T>`. Both render a fixed structure (background + body + button) where only the formatted sextant coordinate string varies per SOS bottle. The cliloc IDs that *appear in the gump packet* are constant (`MessageGump` uses 1018326; `OldMessageGump` uses no `AddHtmlLocalized` at all — its message is pre-formatted into a string before construction), so the layout cache is safe per the cliloc rule. The varying coordinate text is fed through an HTML placeholder via `BuildStrings`. Both gumps are now `Singleton` with private constructors and static `DisplayTo` entry points.
- **ShardPollGump** (`Projects/UOContent/Misc/ShardPoller.cs`) — `DynamicGump`. The gump's structure changes both with the number of poll options (variable loop) and with the `editing` flag (admin sees radios + add-option row + result percentages; players see only radios). The dual-purpose view is preserved as a single `DynamicGump` with the `editing` flag still controlling layout shape — staff path verified to still render the editor with the totals header, vote percentages, and "Create new option" radio. `Closable = false` becomes `builder.SetNoClose()`. Now `Singleton`, with private constructor and a `DisplayTo` that returns the gump instance so `EventSink_Login_Callback` can still call `QueuePoll` on it. The cancel/edit re-issue paths use `SendGump(this)` for self-refresh; the queued login flow uses `DisplayTo` so each queued poll gets its own gump.

External callers (`OnDoubleClick`, `PostPrompt`, `SetTitlePrompt`, `ShardPollPrompt`, `EventSink_Login_Callback`, the Timer-delayed queued poll send) all updated to use the new `DisplayTo` entry points. Legacy `m_X` field naming was already absent in two of the three files; the bulletin board fields kept their `_camelCase` names. `dotnet build Projects/UOContent/UOContent.csproj` reports 0 warnings, 0 errors.
2026-04-25 20:49:12 -07:00
Kamron Batman
83f771c99a
perf: Migrate Travel/Moongate gumps to DynamicGump (#2412)
## Summary

Migrates the three player-facing travel/moongate gumps from legacy `Gump` to the modern `DynamicGump` system.

- `GoGump` (Gumps/Go/GoGump.cs) → `DynamicGump`. The category-tree layout's row count varies with the current `GoCategory`'s child count and pagination. Refreshes via `SendGump(this)` after mutating `_node`/`_page` instead of allocating a new instance per nav/page click.
- `MoongateGump` (Items/Misc/PublicMoongate.cs) → `DynamicGump`. The destination tab strip and per-map pages are gated by ruleset (sigil bearer, murderer, faction facet) and expansion/young flag, plus the configured map selection. The set of pages and the active-map swap make the layout shape per-instance.
- `MoongateConfirmGump` (Items/Skill Items/Magical/Misc/Moongate.cs) → `DynamicGump`. Per the **dynamic-cliloc rule**, the gump bakes one of two different cliloc numbers (1062050 Felucca-warning vs 1062049 generic confirm) and selects between an AOS and pre-AOS layout shape — both characteristics force `DynamicGump` rather than `StaticGump<T>` because cached layout bytes would otherwise lock in the wrong cliloc/shape.

All three now use a `private` constructor with a `public static DisplayTo(...)` entry point that validates prerequisites before any gump is allocated (empty-gump rule, CLAUDE.md §13). All are `Singleton` and use `SendGump(this)` self-refresh on internal navigation. Updated callers: `PublicMoongate.UseGate` and `Moongate.BeginConfirmation` now call `DisplayTo(...)`.
2026-04-25 17:03:24 -07:00
Jack
597c81345e
feat: Adjusts fame and karma system with era gates for OSI accuracy (#2389)
## 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`
2026-04-25 11:19:57 -07:00
Jack
9f39198fab
feat: Adds pre-T2A-pub15 bounty system (#2377)
# 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.
2026-03-22 12:35:16 -07:00
Jack
6745cf2075
feat: Adds Mobile.Murderer virtual property, consolidates kill-threshold checks (#2355)
## 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)
2026-03-06 08:36:41 -08:00
Bohica
2b80286d2e
fix: Fixes arrow animations by using 0xC7 when possible. (#2343)
### Summary

This is a partial fix for servers that enable 0xC7 and clients that support 0xC7. A proper fix should be made on ClassicUO.
2026-03-05 23:23:20 -08:00
Kamron Batman
e1e1a7c640
fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Kamron Batman
c494fb4cc3
fix: Fixes container enumeration not recycling pooled arrays (#2341)
### Summary

Updates all calls to container.EnumerateItems() to properly dispose of the underlying PooledRefQueue so that we are properly recycling pooled arrays.
2026-02-17 09:54:32 -08:00
Kamron Batman
4b392079e9
fix: Fixes exploits with bank box deposits. Makes stacking more efficient (#2337)
### 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
2026-02-13 20:34:35 -08:00
Kamron Batman
ee2cd1d18d
feat: Adds new decay system and SkipSerialization (#2311)
## Summary

- Replaces scan-based decay checking during world saves with an event-driven timer wheel scheduler
- Removes virtual property checks from serialization hot path, achieving 6-8x faster world saves

## Changes

### DecayScheduler (new):
- Timer wheel with 12 HashSet buckets (5-min intervals) + PriorityQueue for active processing
- O(1) register/unregister vs O(n) scan of all items
- Auto start/stop when items exist/empty
- Configurable tick interval with jitter to prevent system synchronization

### Item.cs:
- Added ScheduledDecayTime computed property
- Added UpdateDecayRegistration() called from SetLastMoved(), property setters, AddItem()/RemoveItem()
- Hooks in Visible, Movable, Spawner setters and Delete()

### World.cs:
- Removed _decayQueue, EnqueueForDecay(), ProcessDecay()
- ItemPersistence.Serialize() now tight loop without virtual calls

## Performance

| Metric        | Before     | After     | Improvement |
|---------------|------------|-----------|-------------|
| Items (230K)  | 245K ticks | 34K ticks | 8x faster   |
| Mobiles (43K) | 56K ticks  | 6K ticks  | 9x faster   |
| Total         | 302K ticks | 40K ticks | 7.5x faster |

## Configuration

decay.maxItemsPerTick = 250       # Items processed per tick
decay.tickInterval = 256ms        # Base processing interval
decay.bucketInterval = 5min       # Timer wheel bucket size
decay.jitterMaxMs = 25            # ±25ms tick jitter
2026-01-08 11:43:51 -08:00
Kamron Batman
d3b43f6f0e
feat: Fixes spawner bugs with position finding, Adds [ShowSpawnerBorders (#2297)
### Summary

- Fixes BaseSpawner and Spawner bugs.
- Adds [ShowSpawnerBorders to see the spawn bounds.
2025-12-28 18:26:03 -08:00
Kamron Batman
ebaf104935
chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Kamron Batman
8048711483
fix: Fixes ConditionTeleporter.DenyFollowers (#2284) 2025-11-29 16:28:14 -08:00
Kamron Batman
4836bff5eb
fix: Eliminates List allocations in various places. (#2158) 2025-11-16 18:33:53 -08:00
Kamron Batman
40ba85d7b8
fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Kamron Batman
2c5441731e
fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Kamron Batman
2eba2868a6
fix: Optimizes items to use default weights. (Part 1) (#2240) 2025-07-24 14:44:39 -07:00
Kamron Batman
da517f56c4
chore: Fixes variable types for possible performance issues (#2172) 2025-05-01 21:54:41 -07:00
Kamron Batman
4aa272d429
feat: Adds an Item/Mobile memory leak detector. Fixes minor leak in doors. (#2159)
### Summary

Adds the command [TrackLeaks to enable tracking item/mobiles that have been deleted but still have dangling references. Requires adding the _TRACK_LEAKS_ define constant during build.
2025-04-15 19:14:45 -07:00
Kamron Batman
56e3086ed1
fix: Makes hairdye gump static (#2138) 2025-03-04 15:55:05 -08:00
Kamron Batman
279b10dd0f
feat: Replaces params array with params ReadOnlySpan (#2125) 2025-02-13 21:19:02 -08:00
Kamron Batman
8282b00ca2
feat: Moves gumps out of the core (#1916)
> [!Important]
> **Developer Note**
> This code change will **completely move gumps out of the core**


### Summary

- Adds `GetGumps()` convenience which exposes methods to Find/Close/Send multiple gumps. This helper is a performance improvement by eliminating the Dictionary<Player, List> lookup for gumps.
2024-08-09 19:07:32 -07:00
mdodkins
91e37fb8d4
fix: Hair and facial hair "teleporting" when mobile dies several times (#1901)
### Summary

* Added World.NewVirtual for creating virtual serial numbers
* Reserved range 0x7EEEEEEE to 0x7FFFFFFF for virtual serials
* Hair and Facial hair (for mobiles) now use virtual serials instead of FakeSerial() functions
* Consolidated virtual hair to a single `VirtualHairInfo` class.

Corpse hair and facial hair now persists across save/load and hair and facial hair no longer teleport to newest corpse.
2024-08-07 20:10:23 -07:00
Kamron Batman
f58117a877
fix: Moves ContextMenu out of core, streamlines code, fixes bugs (#1873)
## Summary
- Removes allocation of a `List<ContextMenuEntry>` every time a context menu is created.
- Moves packet/context menu creation logic out of the core
- Fixes tame entry

## BREAKING CHANGE
> [!Important]
> **Developer Note**
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
> ```
> and similar functions changed to
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
> ```
2024-07-20 21:33:23 -07:00
Kamron Batman
38731d98b6
fix: Fixes splitting potions before error (#1862)
### Summary

Most potions have various checks, such as a cooldown. This fixes an edge case where potions get split from their stack before erroring.
2024-07-06 16:39:47 -07:00
Kamron Batman
ac3e4fcc97
Fixes more potion throwing edge cases (#1852) 2024-07-03 18:04:03 -07:00
Kamron Batman
19f65dcabe
fix: Fixes edge cases with on off items (#1832)
### Summary
- Generalizes the On/Off toggle items concept
- Updates the OnOffGump so it is static
- Standardizes OnOff items so they can be used by staff


### Notes
Decided to not fix #1417 because it is not clear that the clilocs or errors are for that purpose. Can't test this on OSI anyways.
2024-06-10 18:11:35 -07:00
Kamron Batman
9c7cb5d778
fix: Fixes dupe property copying. Adds IgnoreDupe (#1811)
## Summary

### Changes
- Adds `[IgnoreDupe]` and `[SerializedIgnoreDupe]`
- Updates all _known_ classes that need the attribute. Some might be missing, please helps us find them!
- Adds `Item.Dupe()` command and encapsulates `CopyProperties` and `OnAfterDuped`. This is also overridable.
- Updates Dupe command to use the new logic.
- Fixes duping multiple kinds of objects that used to be outright broken.

### Bug Fixes
- Fixes issue with durability after duping
- Fixes issue with hue after duping

> [!Note]
> **Developer Note**
> Customizing how duping an item works now requires two steps:
> 1. Add `[IgnoreDupe]` or `[SerializedIgnoreDupe]` to the property/field
> 2. Add custom logic in an `OnAfterDuped` override
>
> When do you need to do this?
> *When the property being copied is not a primitive, and you need to manually deep-clone the contents of the property such as with Lists, Dictionaries, or sub classes.*
2024-06-02 15:04:54 -07:00
Kamron Batman
8efa90b3ce
fix: Codegens spell items (#1765)
### Summary
- Fixes some serialization issues with spell items
- Fixes accessibility issues with field spells and removes the awkward "InternalItem" concept
- Codegens the fields/items used in spells
2024-05-08 19:48:39 -07:00
Kamron Batman
26dfde19ee
fix: Consolidates Color/Center html (#1762)
### Summary
- Fixes bad color in virtual check gump
- Consolidates the Color/Center html strings for all gumps
2024-05-07 23:56:32 -07:00
Kamron Batman
becd7aad05
fix: Cleans up FixHtml (#1757) 2024-05-03 21:24:31 -07:00
Kamron Batman
cfe9e04c78
fix: Optimizes gump relay info (Prep for Static Gumps) (#1698) 2024-03-07 20:54:53 -08:00
Kamron Batman
1fdef9f389
fix: Codegens Heartwood quest givers (#1687)
### Summary
- Codegens quest givers (mostly heartwood)
- Removes their `RandomList` calls in favor of random bool or switch.
2024-02-18 20:22:28 -08:00
Kamron Batman
e994505ad0
fix: Adds safety to corpses and cleans up quests (#1685)
### Summary
- Adds some more checks in case a corpse's owner is somehow null. Would like to eventually allow null owner corpses, but more work is needed.
- Cleans up variable unboxing and reassignment in quests. Also flattens quest logic. Still more work needs to be done.
- Codegens more quest items/mobiles.
2024-02-18 19:33:08 -08:00
Kamron Batman
4479555156
fix: Fixes missing methods. Adds more admin gump world building (#1675)
### Summary
- Fixe commands missing in helpinfo.
- Adds more admin gump world building options. 

### Screenshots

<img src="https://github.com/modernuo/ModernUO/assets/3953314/3001aa65-4f46-4ba4-89ed-745fefdd27c3" width="50%">
2024-02-10 20:31:25 -08:00
Kamron Batman
fffda53263
fix: Adds command help, webpage, and fixes issues with other commands (#1669)
### Summary

- Fixes `[AdvancedSearch` being accessible by players 😱 
- Adds `[GenCommands` to generate the same commands html page on https://muo.gg/commands.
- Fixes `[helpinfo` so all commands properly show up!

> [!WARNING]  
> ### Developer Warning:
> Commands must now be registered in the `Configure` bootup phase.
> If a command is not registered early enough, it may not be available to systems like [helpinfo
> that cache their information.

> [!NOTE]  
> ### Developer Note:
> Various commands related to generating content have been changed to _Developer_ and above access level.

### Screenshots
<img width="673" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/b105b5c9-5eb4-4ace-93ff-1bfb31e7132f">

<img width="547" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/e97487e8-47a5-4aa7-89cc-9fe3deda584d">
2024-02-10 00:19:19 -08:00
Kamron Batman
96c3f2eb91
fix: Use pooled ref for corpse instanced loot (#1645) 2023-12-26 10:51:58 -08:00
Kamron Batman
b7882df136
fix: Fixes serialization and dirty tracking conveniences (#1627)
### Summary
- Fixes issues with non-_ISerializable_ objects having bad serialization (Skill/Stat. Mods)
- Fixes issues with MarkDirty and namespaces: https://github.com/modernuo/SerializationGenerator/issues/29
- Fixes missing dirty tracking for some data structures.
- Fixes cascading MarkDirty for non-serializable types that have owners that are also non-serializable types.

### New Codegenned API
When a data structure (Array, List, Dictionary, HashSet, etc) is source generated for serialization, new methods are added which will handle dirty tracking. Use these instead of the built in methods for that data structure.

_All Data Structures_
```cs
public void Clear<PropertyName>();
```

_Lists and Sets_
```cs
public void AddTo<PropertyName>(V value);
public void RemoveFrom<PropertyName>(V value);
```

_Lists_
```cs
public void InsertInto<PropertyName>(int index, V value);
public void RemoveFrom<PropertyName>At(int index);
```

_Dictionaries_
```cs
public void AddTo<PropertyName>(T key, V value);
public void RemoveFrom<PropertyName>(T key);
public void ReplaceIn<PropertyName>(T key, V value);
```

Over time, they will be cleaned up and more variants added such as `bool RemoveFrom<PropertyName>(T key, out V value)`
2023-12-03 15:55:31 -08:00
Kamron Batman
f21c9687b2
fix: Refactors getting static tiles to use enumerators (#1611)
### Summary
* Refactors getting static/multi tiles to not use allocations.
* `TileList` is now only used during bootstrapping and uses rented buffers to eliminate extra allocations.
* Replaces `StaticTile[] GetStaticTiles` with:
    ```cs
        Map.StaticTileEnumerable GetStaticTiles(int x, int y);
        Map.StaticTileEnumerable GetStaticAndMultiTiles(int x, int y);
        Map.StaticTileEnumerable GetMultiTiles(int x, int y);
    ```
* Removes `Synchronized` and `lock` from TileMatrix. It is no longer considered a multi-thread safe system.
2023-11-26 09:39:46 -08:00
Voxpire
3109f59d4d
fix: Updates container searches for non-generic types (#1567) 2023-11-23 09:29:45 -08:00