Commit graph

100 commits

Author SHA1 Message Date
4161927c27 #W# Fix: Fixed issue with Overworld/Citizen spawners not deleting already spawn npc. 2026-11-05 14:12:04 -05:00
740d06a524 #W# Tradesman: Added citizens that stand round and 'work' 2026-11-04 10:08:06 -05:00
68802df7e2 #W# Spawn: Lots of spawn work and tweaks. 2026-11-02 14:19:47 -05:00
488fa12039 #W# Citizens: Added more monster types and more custom dialog. 2026-11-02 11:23:21 -05:00
d05a274ba6 #W# Added: Made a dynamic citizen spawner that only spawns them on roads and dirt. 2026-10-31 15:35:38 -04:00
e5029166e2 #W# Spawn: Tweaked RegionSpawner so it detects the region when you spawn it. 2026-10-08 09:07:58 -04:00
accb38d7d2 #W# Spawn: Added low tier mobs to the dynamic spawner. 2026-09-26 20:15:54 -04:00
01c0676c7a #W# Save: Just a quick save. 2026-09-23 17:20:43 -04:00
428f7c4c1d #W# Spawns: Still tweaking the Dynamic Spawners. 2026-09-23 14:27:13 -04:00
484fbb6512 #W# Spawns: Working on a Biome spawn system. 2026-09-23 13:02:12 -04:00
4170f92bdc #W# Added: [ESA command exports all spawners on the map. [IS </path/*.json> imports spawners from specified file. 2026-09-21 09:08:13 -04:00
Kamron Batman
c02909e2c8
feat(spawners): virtual OnTick; SpawnerDto carries the group flag (#2640)
## Summary

Two small additive changes a derived spawner needs.

- **`BaseSpawner.OnTick()` is now `virtual`.** A subclass that gates spawning on external state (time windows, event triggers) must gate *timer* spawns without gating the manual `Spawn()` API, and `OnTick` is the only place the two paths differ: it is the timer callback and `Spawn()` is both what it calls and what commands and scripts call. Cost: one virtual dispatch on the existing timer callback; no change to stock behaviour.
- **`group` in the JSON DTO.** `BaseSpawner.Group` (all dead, then respawn) is binary-persisted but was missing from `SpawnerDto`, so it did not survive export/import. Added to the abstract record after `spawnLocationIsHome`, assigned in `ApplyDto` after `InitSpawn` (which resets it), and exported by the three stock `ToDto` implementations.

## Test plan

- [x] A derived spawner overriding `OnTick` with a closed gate: `OnTick()` spawns nothing; manual `Spawn()` still spawns and does not pass through `OnTick`.
- [x] DTO round trip with `Group = true` carries `group` and restores it on `ToSpawner()`.
- [x] `UOContent.Tests` full suite green.
- [ ] CI
2026-09-12 16:33:57 -07:00
Kamron Batman
4fe5dd0eec
fix(spawners): lock column in the spawner gump, read-only locked entries, aligned totals (#2635)
## Summary

Follow-up to #2621, which added a per-entry `Disabled` flag to the spawner gump as a checkbox. The checkbox was placed between the Expand and Delete buttons at x=22, overlapping Expand (5–35), and Delete moved to 46, overlapping the creature text box at 71.

- **Lock column on the far left.** Each entry row gets a toggle at x 5–25: padlock `0x82C` when the entry is disabled (press to unlock), green orb `0x2C88/0x2C89` when enabled (press to lock). The client's gump art has only closed padlocks (`0x82C`, `0x0020`), so the orb marks the unlocked state; XmlSpawner uses the same padlock paired with a blue gem.
- **Expand and Delete are adjacent again** (28 / 61, the original 33px pitch), and everything to the right shifts 23px: creature/#/Max/Prb boxes, headers, the totals row, page arrows, Save/Cancel. The gump is now 369 wide (was 346). Params/Props boxes widen to match.
- **Locked entries are read-only.** Name, max, probability, and Params/Props (when expanded) render as `AddLabelCropped` on a grey tile (`0x23F4`) instead of `AddTextEntry`. There is no read-only text entry in the UO gump protocol, so omitting the entry is the only way to make the field truly non-editable. `CreateArray` already `continue`s on a null type entry, so Save leaves locked entries untouched.
- **Totals row aligned under its columns.** Spawned under `#`, a new total max under `Max`, total weight under `Prb`. Max and weight exclude locked entries, matching `BaseSpawner`, which skips `Disabled` entries when rolling.

## Test plan

- [x] `[props` a spawner with several entries: lock column is on the left, Expand/Delete sit side by side, no overlaps.
- [x] Lock an entry: padlock shows, fields go grey and cannot be typed into; unlock: orb shows, fields are editable again.
- [x] Edit an unlocked entry, lock another, Save: unlocked edits persist, locked entry keeps its values.
- [x] Expand a locked entry: Params/Props are grey and read-only.
- [x] Totals sit under `#` / `Max` / `Prb`; locking an entry drops it from the max and weight totals.
2026-09-11 20:30:02 -07:00
Kamron Batman
a52ce6ef70
refactor(spawners): subclass-owned entries, lifecycle hooks, per-entry Disabled flag (#2621)
## Summary

Moves spawner entry storage out of the abstract `BaseSpawner` into the concrete owner, so a spawner subclass can store its own entry type while every stock code path keeps working. Motivation: an out-of-tree spawner (ModernSpawner) needs `ModernSpawnerEntry : SpawnerEntry` with extra fields; today `BaseSpawner` owns `List<SpawnerEntry>` and a family of non-virtual members, and the serialization generator constructs list elements from the declared element type, so storage has to live in the class that declares the concrete list.

### What changed

- **`BaseSpawner` v13** no longer owns `_entries`. It reads entries through an abstract view and mutates them through an owner contract (`BaseSpawner.Entries.cs`):
  `Entries` (`IReadOnlyList<SpawnerEntry>`, `[IgnoreDupe]`), `EntrySpan` (`ReadOnlySpan<SpawnerEntry>` for hot loops), `CreateEntry`, `AddEntryCore`, `RemoveEntryCore`, `ClearEntriesCore`, `AdoptEntries`, `CloneEntry`, `TransferSpawned`, plus public `RemoveAllEntries()`, `CopyEntriesTo(target)` and protected `RebuildSpawned()`. Every loop inside `BaseSpawner` is an indexed `for` over `EntrySpan`.
- **`Spawner` v2** owns `[SerializedIgnoreDupe] List<SpawnerEntry> _entryList` (generated `EntryList`, protected). `ProximitySpawner`/`RegionSpawner` inherit it unchanged. The `Spawned` rebuild and timer re-arm moved from the base `[AfterDeserialization]` (which runs before derived fields are read) into `Spawner`'s.
- **Save migration**: `MigrateFrom(V12Content)` (and the v10/v11/legacy readers) hand the old list to the owner via `AdoptEntries`; `Spawner.MigrateFrom(V1Content)` restores its own fields and leaves the adopted list alone. Three v12/v1/v0 save blobs captured before the change are committed as fixtures and loaded by tests.
- **Lifecycle hooks** (`BaseSpawner.Hooks.cs`, all no-op by default): `OnStarted`, `OnStopped`, `OnBeforeSpawn(entry)` veto, `OnConfigureSpawned(entry, spawned)` before positioning, entry-aware `GetSpawnPosition(entry, spawned, map)`, `OnSpawned(entry, spawned)`, `OnSpawnedDeath(entry, spawned, killer)`. `BaseCreature.OnDeath` calls `NotifySpawnedDeath` before base death (which deletes the mobile and unlinks the spawner). `Start()` and the `NextSpawn` setter share one start core so `OnStarted` fires on both.
- **`SpawnerEntry` v2**: per-entry `Disabled` (XmlSpawner's entry "lock"), stored inverted so the common case writes nothing in binary or JSON; skipped by weighted selection, live spawns untouched; toggle button per row in `SpawnerGump`. `SetParent` is public and `Parent` is protected so an out-of-tree entry subclass can adopt and dirty-track.
- **DTO**: `SpawnerDto` loses `Entries`; each concrete record declares its own `entries` at the same JSON order, so `Distribution/Data/Spawns/**` is byte-identical. Import adopts the deserialized entry objects instead of recreating them through `AddEntry`, which is what preserves subtype fields (and `disabled`).

### Breaking changes and behaviour changes

- **API:** `BaseSpawner.Entries` is `IReadOnlyList<SpawnerEntry>` instead of `List<SpawnerEntry>`. The generated `AddToEntries`/`RemoveFromEntries`/`InsertIntoEntries`/`RemoveFromEntriesAt`/`ClearEntries` helpers on `BaseSpawner` are gone; use `AddEntry`/`RemoveEntry`/`RemoveAllEntries`/`CopyEntriesTo`. `RemoveAllEntries()` deletes the entries' live spawns as well as the entries (the old generated `ClearEntries()` only cleared the list), which is why it has a new name rather than the old one.
- `SpawnerControllerGump` "copy entries" now goes through `CopyEntriesTo`, which deletes the target's live spawns (previously it cleared the list and left the spawns orphaned) and is a no-op when source == target (previously that wiped the source).
- `RemoveEntry` with an entry the spawner does not own is now a no-op (previously it deleted that entry's spawns).
- `Respawn()` honours `Disabled` because it calls `Spawn()`; `Spawn(int index)`, `RemoveSpawn`, and `RemoveSpawns` ignore it.
- Copying entries between spawners no longer forces a 1-second first spawn; the target re-arms on its normal delay.
- Subclasses that own a different entry list than `Spawner`'s must call `RebuildSpawned()` from their own `[AfterDeserialization]` (`Spawner`'s call runs before their list is read) and, when converting adopted entries into their own type, carry live spawns across with `TransferSpawned`. The in-repo test subclass demonstrates both.

### Performance

Manual harness (`Benchmark_SpawnPath_Manual`, skipped by default): 100k calls, entries all full so `Spawn()` does selection only.

| Path | Before (4bad0cc9e) | After |
|---|---|---|
| `Spawn()` 1 entry | 48.7 ns | 53.2 ns (within run-to-run noise) |
| `Spawn()` 10 entries | 243.7 ns | 153.1 ns |
| `Spawn()` 50 entries | 1077.6 ns | 645.1 ns |
| `Remove()` 10 entries | 139.6 ns | 83.6 ns |

Hooks are no-ops for stock spawners; `OnMovement` is untouched.

### Tests

- `SpawnerEntryOwnershipTests` (add/remove/clear, start after stop, dupe, copy, self-copy, foreign-entry removal, Disabled binary/JSON)
- `SpawnerHookTests` (hook order for mobiles and items, veto, death notification, `NextSpawn` start, a subclass with its own `List<TestEntry>` round-tripping and duping)
- `SpawnerSaveMigrationTests` (v12 fixtures through `Spawner`, `ProximitySpawner`, `RegionSpawner`; new-format byte-identical round trip with a live spawn reference)
- `SpawnerDtoEntryTests` (compact JSON position of `entries`, `disabled` only when set, import adopts the deserialized objects)
- Existing DTO/JSON/spawn-data tests unchanged. UOContent.Tests 801 passed, Server.Tests 869 passed. Migration schemas regenerated (`BaseSpawner.v13`, `Spawner.v2`, `SpawnerEntry.v2`).

Coverage caveats, stated plainly: v10, v11 and the pre-codegen legacy reader could not be captured as fixtures by the current code; each changed only `_entries = …` → `AdoptEntries(…)` into the same sink the v12 fixtures exercise, and is covered by review. The captured v12 fixtures carry no live spawns, so re-linking live `ISpawnable` references is proven by the new-format round trip, not by a legacy blob. The `SpawnerGump` toggle layout could not be checked in a client; the delete button moved from x=38 to x=46 to make room.
2026-09-11 17:41:08 -07:00
Kamron Batman
4bad0cc9e6
refactor(spawners): expose BaseSpawner DTO helpers to out-of-assembly subclasses (#2619)
## Summary

`BaseSpawner.Dto.cs` exposes `DtoName`, `DtoWalkingRange`, `DtoSpawnPositionMode`, `DtoMaxSpawnAttempts`, `DtoHomeRange` and `BoundsFromHomeRange` as `private protected`, which limits them to subclasses in this assembly. A spawner subclass in another assembly that overrides `ToDto()` to produce its own `SpawnerDto` subtype cannot build the DTO without duplicating that logic.

This widens them to `protected`. No behaviour change; nothing else in UOContent is affected.

## Tests

- `dotnet build` clean.
- An external spawner assembly builds and its test suite (419 tests) passes against this commit.
2026-09-08 19:52:43 -07:00
Kamron Batman
84153fba58
fix: Fixes dirty-tracking gaps in generated content: setters, sub-object owners, BaseVendor (#2609)
## Why

Delta world saves re-serialize an entity only when it has been marked dirty. An audit of the generated classes found three ways serialized state changes without a mark; this PR closes the ones that do not need a new generator package.

## What

- **Custom `[SerializableProperty]` setters mark dirty.** Twelve hand-written setters assigned their backing field without `this.MarkDirty()`. `PlagueBeastLord.OpenedBy` was an auto-property carrying the attribute with no backing field at all; it is now a generated field with the same name, order and command-property exposure.
- **Generated sub-objects are linked to their owner.** Without a `[DirtyTrackingEntity]` member the generator emits setters that mark nothing. Eight entity-owned sub-objects now carry the link and receive the owner through the constructor the generator calls: `BOBFilter` (owner is `IEntity`: a `PlayerMobile` or a `BulkOrderBook`), `BOBLargeSubEntry`, `PuzzleChestSolution` and `PuzzleChestSolutionAndTime`, `TalismanAttribute` (the random factories now take the talisman), `VendorItem`, `PlayerBBMessage`, `RaffleEntry`, `ShardPollOption`. Migration schemas were regenerated with the pinned tool; the only change is the value rule argument becoming `DeserializationRequiresParent`.
- **BaseVendor uses the generator; restock amounts are no longer persisted.** The only state it wrote was which buy entries had grown restock amounts, packed by index into the live `SBInfos` tables. Restock is transient now and rebuilds on load; version 1 records are read and discarded through the legacy path. Every vendor subclass now serializes through a generated chain.

## Generator 4.1.0

This PR adopts SerializationGenerator 4.1.0 (modernuo/SerializationGenerator#55): SG3019/SG3020 diagnostics, `[VolatileSerializedState]`, `StopXxx()` timer helpers, and owner-constructor preference for sub-objects (so dictionary values are constructed with their owner; the 4.0.0 relink fallback is gone). `PlayerVendor.v3.json` gains `DeserializationRequiresParent` for its `VendorItem` values so the migration content struct constructs them with their vendor.

SG3019 is an error under `TreatWarningsAsErrors` and generator diagnostics ignore pragmas, so the five contexts that live in whole-file player-keyed persistences (`ChampionTitle`, `ChampionTitleContext`, `MurderContext`, `VirtueContext`, `JailRecord`) now carry a `[DirtyTrackingEntity]` link to their `PlayerMobile`, which is where they will live once those blobs move onto the player record. `JailSystem.EmptyRecord` keeps a null player (`[CanBeNull]`). `ChampionTitle` needs both a context and a player constructor because the generator resolves the rule against the containing type but emits the call with the parent field; a comment in the file records this.

## Wire format

Unchanged. No version bumps except `BaseVendor` 1 to 2 (which now writes nothing of its own). Schema diffs are rule-argument only (`DeserializationRequiresParent`).

## Testing

Server.Tests 848 passed, UOContent.Tests 759 passed.
2026-09-06 09:43:44 -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
d8a64f3316
refactor(spawners): replace DynamicJson with typed SpawnerDto records (#2505)
## Summary

Removes the dated `DynamicJson` JSON helper and migrates spawner JSON (de)serialization to a polymorphic `record SpawnerDto` hierarchy. `DynamicJson` was the last remaining consumer (regions moved off it in #1400).

The key correctness improvement: **System.Text.Json deserializes plain DTO records, never a live `Item`.** Previously, deserializing directly into an `Item` meant STJ constructed world-registered objects *before* the data was validated — a malformed/hand-edited spawn file could leave orphaned spawner Items in the world save. Now a parse failure is GC-only; `dto.ToSpawner()` constructs the spawner only from a fully-validated DTO and self-cleans on failure.

## What changed

- **New:** `SpawnerDto` (abstract) + `SpawnerDataDto` / `RegionSpawnerDto` / `ProximitySpawnerDto`, each marked with a reusable `[JsonDiscoverableType]` opt-in attribute. Auto-discovered at the `Configure` phase — no manual registration list (avoids the regions `Register<T>()` footgun), open to custom spawner subtypes.
- **Symmetric mapping:** `BaseSpawner.ToDto()` (export) ⇄ `SpawnerDto.ToSpawner()` (import). `ToSpawner()` deletes-and-rethrows on any failure, so the importer can never orphan an Item.
- **`SpawnerJsonSerializer`** wires `$type` polymorphism on the `SpawnerDto` root with loud collision/constructibility validation.
- **Import/export commands** rewired to the typed DTO path (reflection `FindTypeByName`/`CreateInstance` removed).
- **Data migration:** the 109 `Distribution/Data/Spawns/**` files moved from `"type"`→`"$type"` and legacy `homeRange`→`spawnBounds`. The runtime still *reads* legacy `homeRange` for external files. The `homeRange→spawnBounds` formula is proven equivalent to the runtime conversion (`BoundsEquivalenceTests`, hr=0/1/3/7).
- **Deleted:** `Projects/Server/Json/DynamicJson.cs`.

Sparse export output matches the legacy `ToJson` (nullable DTO properties + `WhenWritingNull`). Binary world-save serialization is untouched.

## Tests

- DTO round-trip per spawner type; sparse-default omission; legacy `homeRange` read; export/import file round-trip.
- `Import_MalformedFile_LeaksNoWorldItems` — proves a mid-array parse failure constructs zero world Items.
- `AllSpawnFilesLoadTests` — every migrated spawn file deserializes and builds.
- Duplicate-discriminator validation.
- UOContent.Tests 485/485, Server.Tests 710/710, build clean.

## Follow-up (not in this PR)

`Rectangle3DConverter.Write` (in `Projects/Server/`) omits `z1/z2` when `Start.Z == -128`, so a future server-side export of a `homeRange`-style spawner round-trips depth 256→255. Pre-existing and out of scope here (Server change); the migrated data reads correctly. Worth a separate small converter PR.
2026-06-25 23:24:45 -07:00
Kamron Batman
8a34903326
feat: Consolidates staff gump layouts (#2404)
## Summary
- Creates `PropsLayoutExtensions.cs` with reusable extension methods for both legacy `Gump` and `DynamicGumpBuilder` that encapsulate the repeating PropsConfig-style layout patterns (frame, header navigation, entry rows)
- Converts all 12 standardized staff gumps to use the new extensions, reducing ~570 lines of duplicated layout code
- Adds Type and Serial display to PropsGump and SkillsGump headers (e.g. `PlayerMobile (0x1)`)

### Extension methods provided
| Method | Pattern |
|--------|---------|
| `AddPropsFrame` | Background + offset region + origin coordinates |
| `AddPropsHeader` | 3-column: [Prev] [Title] [Next] |
| `AddPropsHeaderWithBack` | 4-column: [Back] [Title] [Prev] [Next] |
| `AddPropsEntryButton` | Label + action button |
| `AddPropsEntryNameValue` | Name + Value + action button |
| `AddPropsEntryTextInput` | Text input + action button |
| `AddPropsEntryLabel` | Label only (no button) |
| `AddPropsEntryType` | Full-width type label |
| `AddPropsEntryBlank` | Separator row |

### Gumps converted
PropsGump, GoGump, WhoGump, SkillsGump (frame+header), EditSkillGump, SetGump, SetObjectGump, SetPoint2DGump, SetPoint3DGump, SetTimeSpanGump (frame), SetListOptionGump (frame+header), CategorizedAddGump (frame+header)

## Test plan
- [x] Verify `[props` gump displays correctly with type + serial in header
- [x] Verify `[skills` gump displays correctly with type + serial in header
- [x] Verify `[go` navigation gump works (prev/next/back)
- [x] Verify property editing gumps (Set, SetObject, SetPoint2D, SetPoint3D, SetTimeSpan, SetListOption)
- [x] Verify `[categorizedadd` gump works
- [x] Verify `[who` gump works with pagination
2026-04-08 11:42:46 -06:00
Bohica
e37ad17d7d
fix: Adds handling empty spawner files (#2360) 2026-03-10 23:04:39 -07:00
Kamron Batman
e1e1a7c640
fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -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
bc6735bd23
feat: Adds grid support for the DynamicGump system (#2306)
### Summary 

- Adds a new grid layout system for DynamicGump with zero heap allocations
 - Migrates SpawnerControllerGump from legacy GumpGrid to DynamicGump
 - Migrates CommandListGump from BaseGridGump to DynamicGump
 - Removes legacy GumpGrid (no longer needed)

 ### New Grid Layout Components

 | Component | Purpose |
 |-----------|---------|
 | `GridCell` | Value type for cell bounds (x, y, width, height) |
 | `GridSizeSpec` | Parses CSS-like sizing specs ("10*", "*", "100") |
 | `GridCalculator` | Computes track positions using stackalloc |
 | `ListViewLayout` | Pagination + column layout for list views |
 | `GridBuilderExtensions` | Extension methods accepting `GridCell` |
 | `GridEntryStyle` | Styling properties for BaseGridGump migration |
 | `GridEntryExtensions` | Entry methods matching BaseGridGump patterns |

 ### Memory Impact

 | Component | Legacy | New |
 |-----------|--------|-----|
 | Grid storage | ~200 bytes heap | 0 bytes (stackalloc) |
 | Column/Row lists | ~400 bytes heap | ~128 bytes stack |
 | ListView | ~300 bytes heap | ~64 bytes stack |
 | **Total per render** | **~900 bytes heap** | **0 bytes heap** |

 ### Test plan

 - [x] All 21 gump tests pass
 - [x] Build succeeds with no warnings
 - [ ] In-game verification of SpawnerControllerGump
 - [ ] In-game verification of CommandListGump (HelpInfo command)
2026-01-03 10:12:22 -08:00
Kamron Batman
ec287d7691
fix: Fixes zero height spawners and normalizes spawn bounds before use. (#2301) 2025-12-31 10:25:42 -08:00
Kamron Batman
598223703f
refactor: Add BitMask256 utility for 256-bit bitmask operations (#2300)
### Summary

- Create BitMask256 struct with scalar operations (benchmarks showed AVX2 vectorization provides no benefit for this size)
- Refactor Map.cs to use BitMask256 for full Z range support (-128 to 127)
- Remove SectorSpawnCache struct, use BitMask256 directly in manager
- Update tests to use BitMask256 directly
- Remove unused test assertions for old 64-bit behavior
2025-12-29 12:54:49 -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
3e8d548f38
feat: Add spawn position caching and spiral scan optimization (#2295)
### Summary

Adds spawn position caching and optimization for constrained spawners (e.g., those near houses, water, or blocked terrain).

### Key features:
- Sector-based bitmap cache (32 bytes per 16x16 sector) stores valid spawn positions
- Spiral scan progressively discovers positions from spawner center outward
- Automatic mode detects constrained spawners after 5+ non-transient failures
- Prevents mob spawning inside private houses (allows public AoS buildings)
- Deduplicates sector lookups for multi-bounds spawners (RegionSpawner)
- Cache invalidation on house placement/demolition
- Moves SpawnBounds to Spawner

### New spawner properties:
- SpawnPositionMode: Automatic (default), Enabled, Disabled, Abandoned
- MaxSpawnAttempts: Configurable attempts before optimization engages (default: 5)
2025-12-28 02:40:21 -08:00
Kamron Batman
6d51b33cf8
feat: Add CanSpawnMobile overload with props Z-range support. (#2293)
### 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
2025-12-27 17:01:15 -08:00
Kamron Batman
ebaf104935
chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Quick
0b34cc4417
feat: Changes Spawner HomeRange to SpawnBounds (#2290)
### 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.
2025-12-26 18:07:26 -08:00
Kamron Batman
7bd5a853a3
feat: Adds size/style support to gump builders, optimizes EscapeHtml (#2257)
> [!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.
2025-11-23 11:35:31 -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
Kamron Batman
4610712205
fix: Fixes respawn command when spawner is off (#1891) 2024-07-28 14:34:23 -07:00
Marcelo Paez Sequeira
832bb24f11
fix: Fixes ProximitySpawner and RegionSpawner ToJson (#1827) 2024-06-06 16:31:48 -07:00
Kamron Batman
5a65a6b8ea
fix: Fixes exporting spawners. (#1826) 2024-06-06 14:19:26 -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
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
328c6daa60
fix: Fixes spawner deserialization (#1760) 2024-05-05 13:10:42 -07:00
Kamron Batman
1a7e7c7c70
fix: Fixes spawner timer deserialization, decimal deserialization, and adds potion keg reverse lookup (#1711)
### Summary
* Fixes spawner timer deserialization
* Adds a check for a null timer and allows the timer to get recreated
* Adds PotionKeg reverse lookup
* Heavily optimizes decimal serialize/deserialize
2024-03-28 13:34:12 -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
c42c20aef9
fix: Fixes spawn entry deserialization (#1693) 2024-03-03 08:45:16 -08:00
Kamron Batman
de0d205fd3
fix: Fixes importing spawners (#1689) 2024-02-19 10:07:51 -08:00
Kamron Batman
d0d7be8de0
fix: Fixes codegenned spawners and adds RunUO spawner import support (#1682)
> [!IMPORTANT]  
> This code change includes important fixes for all spawners after they were converted to codegen!

> [!NOTE] 
> **Developer Note**
> Usage/Description/Aliases attributes were moved to the core so they can be used by the command registration system.

### Summary
- **Fixes spawners not registering their spawns on world load**
- Changes `[GenerateSpawners` to `[ImportSpawners`
- Removes `ConvertPremiumSpawners`
- Adds Premium spawner import support to `[ImportSpawners`
- Adds RunUO XML import support: https://github.com/ruaduck/xmlspawner/blob/ServUOMaster/XmlSpawner/XmlSpawner%20Core/SpawnerExporter.cs
- Fixed an issue where not manually registering an alias meant it wasn't available as a command
2024-02-17 10:46:55 -08:00
Kamron Batman
5ada7a4e50
fix: Codegens spawners (#1678) 2024-02-12 21:14:42 -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
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
Kamron Batman
331f24cb71
fix: Fixes TimeSpan rounding issues (#1610)
### Summary

TLDR - .NET Framework rounded TimeSpan (and some DateTime) methods to the nearest millisecond. This was actually _expected_ accidentally for parts of RunUO.

We are aiming to fix this and clean up some other bad assumptions.

Closes #1604
2023-11-22 18:03:56 -08:00