Commit graph

15 commits

Author SHA1 Message Date
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
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
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
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
279b10dd0f
feat: Replaces params array with params ReadOnlySpan (#2125) 2025-02-13 21:19:02 -08:00
Kamron Batman
5ada7a4e50
fix: Codegens spawners (#1678) 2024-02-12 21:14:42 -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
73c65a43ad
fix: Cleans up movement code (#787)
* Removes FastMovementImpl since it isn't used and I am not convinced it is better.
* Moves some of the new movement logic from FastMovementImpl to MovementImpl
* Makes MovementImpl more readable
2021-09-16 23:23:22 -07:00
Kamron Batman
0d1fffd9fc
fix: Fixes spawners, adds convertpremiumspawners, exportspawners, and replaces with neruns distro (#751)
### Additions
* Adds `[exportspawners <relative json file path to distro>`
  * If no path is provided, it will be saved to the `Data\Spawns` folder with the current timestamp as the file name.
  * Supports global, facet, region, multi, and area
* Fixes client disconnect for bad spawner generation command.
* Moves spawner commands to their own folder.
* Adds GUIDs for spawners. This allows for easy replacement during import to reduce duplication.
* Allows exporting the name of the spawner.
* Fixes globbing when using [generatespawners
* Adds [convertpremiumspawners to convert Premium Spawners (from Neruns distro)

Possibly in .NET 6 serializing JSON will be more performant using JSON nodes.


### Screenshots
![Screen_Shot_2021-08-31_at_10 20 06_PM](https://user-images.githubusercontent.com/3953314/131618495-cf7e3ae5-58e5-4f86-9d3c-3cc49906d9fc.png)
![Screen_Shot_2021-08-31_at_10 21 08_PM](https://user-images.githubusercontent.com/3953314/131618502-c39cc368-2266-47a5-9b87-e5ffa108b875.png)
![Screen_Shot_2021-08-31_at_10 13 29_PM](https://user-images.githubusercontent.com/3953314/131618512-f9807f1b-76e4-4503-989d-42324798fbf5.png)
2021-09-03 21:52:44 -07:00
Kamron Batman
8149620b0c
Fixes brace style (#248) 2020-09-13 21:49:46 -07:00
Kamron Batman
ad3775c4d7
Formats UO Content (#201) 2020-08-27 18:30:38 -07:00
Kamron Batman
390f30e706
Convert Regions/Spawns to JSON (#138) 2020-05-26 00:34:29 -07:00