## Summary
Replaces the basic `publish.cmd`/`publish.sh` scripts with an interactive **BuildTool** — a C# console app using [Spectre.Console](https://spectreconsole.net/) that guides users through publishing, prerequisite checking, and cross-compilation.
### Why
The community found the existing publish scripts unhelpful for newcomers. They worked but didn't walk users through the process, didn't check prerequisites, and provided no feedback when things went wrong.
### What's New
**Interactive BuildTool** (`Projects/BuildTool/`)
- NativeAOT-compiled C# console app with true-color ASCII logo and ModernUO brand gold/silver palette
- Guided publish wizard with step-by-step back navigation (Ctrl+C or menu "Back" to go to previous step)
- Prerequisite checking: .NET SDK version, VC++ Redistributable (Windows), native libraries (Linux/macOS)
- .NET SDK auto-install offer via Microsoft's official install scripts
- Platform detection: Windows 10 vs 11 (build number), macOS codenames, Linux distro + kernel version
- Cross-compilation support: skips native library checks, shows target prerequisites after build
- Non-interactive mode for CI: `--config Release --skip-prereqs`
- Backward-compatible positional args: `publish.cmd release win x64` still works
**Shell Wrappers** (`publish.cmd`, `publish.ps1`, `publish.sh`)
- Try native BuildTool binary first (downloaded from GitHub Releases)
- Fall back to `dotnet run --project Projects/BuildTool` if unavailable
- SDK bootstrapping: offer to install .NET if not found
**CI/CD Updates**
- Build/test workflows target `Projects/Application/Application.csproj` instead of the solution (excludes BuildTool and test projects from publish)
- New `build-tool-release.yml` workflow builds NativeAOT binaries for win-x64, win-arm64, osx-arm64, linux-x64, linux-arm64
- Minimum SDK bumped to 10.0.201 (required for Serialization Generator 2.14.3 / Roslyn 5.3.0)
**Other Changes**
- Solution converted from `.sln` to `.slnx`
- Updated README with interactive mode instructions and deployment guidance
## Screenshots
<img width="320" height="378" alt="image" src="https://github.com/user-attachments/assets/83c057c5-3992-4dbd-99fa-0e3c24ef6428" />
<img width="749" height="554" alt="image" src="https://github.com/user-attachments/assets/e4ee4d6f-71d4-47f9-86b6-8fd1ca3c3e7a" />
## Summary
- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns
## InterpolationHandler Design
`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:
- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields
**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.
This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool
## Changes by Category
### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)
### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter
Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)
### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)
### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop
### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops
### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section
## Test Plan
- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
- Stackalloc no-grow, stackalloc with grow (→pool transition)
- Heap no-grow, heap with grow, heap double grow
- Pre-existing content with and without grow
- Sequential multiple `Append($"...")` calls
- Mixed plain + interpolated Append
- Empty interpolation, literal-only, format specifiers
- Null string holes, ISpanFormattable types
- Dispose after stackalloc→pool grow
## Summary
- Refactors the poison system to separate `Index` (globally unique ID) from `Level` (tier within a family), enabling multiple poison families (Standard, Darkglow, Parasitic) to coexist without collisions
- Implements Darkglow and Parasitic poison special effects from Mondain's Legacy: Darkglow boosts damage by 10% when attacker is ranged, Parasitic heals the attacker for damage dealt in melee range
- Fixes several bugs: `Register()` crashing on duplicate `Level` values across families, `IncreaseLevel()` crossing family boundaries, `InfectiousStrike` and `NinjaWeapons` stripping poison family via level-based lookups, and `ArchCure`/`CleansingWinds` using raw `Level + 1` instead of `IncreaseLevel()`
## Changes
**`Projects/Server/Poison.cs`** — Adds `PoisonFamily` enum and abstract `Family` property. Adds `Index` as unique identifier. Fixes `Register()` to check `Index` uniqueness (not `Level`) and validate the new poison's name (not the existing one's). Fixes `IncreaseLevel()` to use `Index + 1`, naturally respecting family boundaries via Index gaps. Replaces linear name lookup with `Dictionary`-based `PoisonsByName`.
**`Projects/UOContent/Misc/Poison.cs`** — Adds `family` parameter to `PoisonImpl`. Implements Darkglow effect (10% damage boost when `From` >1 tile, cliloc 1072850) and Parasitic effect (heals `From` for damage dealt within 1 tile, cliloc 1060203) in `PoisonTimer.OnTick()`. Renames `m_` fields to `_` convention.
**`Projects/UOContent/Misc/PoisonKinds.cs`** — New file. Moves poison registration out of `PoisonImpl` into `PoisonKinds.Configure()`. Adds `PoisonFamily` to Darkglow/Parasitic registrations. Provides extension properties (`Lesser`, `Deadly`, `LesserDarkglow`, etc.), `GetPoison(int level)` (standard-only), `GetPoisonByFamilyAndLevel()`, and `IsDarkglow`/`IsParasitic` instance helpers.
**`Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs`** — Family-aware poison scaling: Darkglow caps at Deadly (Poisoning/33.3), Parasitic caps at Lethal (Poisoning/25), Standard unchanged. Level bump uses `IncreaseLevel()` with family boundary check.
**`Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs`** — EvilOmen level bump uses `Poison.IncreaseLevel()` instead of `Poison.GetPoison(Level + 1)`.
**`Projects/UOContent/Spells/Fourth/ArchCure.cs`** and **`CleansingWindsSpell.cs`** — Replace `poison.Level + 1` with `Poison.IncreaseLevel(poison).Level` for family-safe cure chance calculation.
**`Projects/Server/Serialization/SerializationExtensions.cs`** — Serializes/deserializes `Index` instead of `Level`.
**`DarkglowPotion.cs`** / **`ParasiticPotion.cs`** — Point to actual Darkglow/Parasitic poisons instead of placeholder `Greater`.
**`PotionKeg.cs`** / **`BasePotion.cs`** — Adds Darkglow, Parasitic, Invisibility, and FlintsPungentBrew to `PotionEffect` enum and keg label support.
## Test plan
- [ ] `dotnet build` compiles cleanly (verified, 0 warnings 0 errors)
- [ ] Verify `PoisonKinds.Configure()` registers all poisons without throwing (Register bug fix)
- [ ] Standard poison behavior unchanged — PoisonField, PoisonSpell, SerpentArrow, SavageShaman, TrappableContainer all use `GetPoison(int level)` which now correctly filters to Standard family
- [ ] Darkglow: poison tick deals +10% damage when attacker is >1 tile away, sends "Darkglow poison increases your damage!" message
- [ ] Parasitic: poison tick heals attacker for damage dealt when within 1 tile, sends heal message
- [ ] InfectiousStrike preserves poison family and respects family-specific skill scaling
- [ ] EvilOmen + NinjaWeapons level bump stays within poison family
- [ ] ArchCure/CleansingWinds cure chance calculations work correctly across all poison families
- [ ] Serialization round-trips correctly using Index
## Summary
- **Timer-aware idle sleep**: Exposes `Timer.MillisecondsUntilNextTick()` to calculate remaining ms until the next timer wheel tick (0–8ms). The game loop sleeps for that duration minus a 1ms safety margin, instead of spinning at 100% CPU.
- **I/O completion wakeup**: Replaces `Thread.Sleep` with `NetState.WaitForCompletion()`, which uses platform-native completion notification (RIO `RIONotify` on Windows, `eventfd` on Linux, `kevent` timeout on macOS) to wake immediately when network data arrives during sleep.
- **Always-on**: Removes the debug-only `core.enableIdleCPU` config gate. The sleep is self-regulating — under load, `MillisecondsUntilNextTick` returns 0 so no sleep occurs (zero overhead). On idle, CPU drops from ~100% to ~1%.
- **CPS calculation cleanup**: Replaces the 128-element ring buffer with an EMA (exponential moving average) for `CyclesPerSecond`/`AverageCPS` — fewer allocations, no LINQ `.Average()` call each sample.
- **IORingGroup 1.0.6**: Adds `WaitForCompletion(int timeoutMs)` to the `IIORingGroup` interface with platform implementations:
- **Windows**: `RIONotify` arms the CQ event, `WaitForSingleObject` with timeout
- **Linux**: `eventfd` registered with io_uring, `poll()` with timeout
- **macOS**: `kevent()` with timeout
## Test plan
- [ ] Build succeeds on all platforms (`dotnet build`)
- [ ] Empty server: verify CPU usage drops from ~100% to ~1% idle
- [ ] Loaded server: verify no added latency — `MillisecondsUntilNextTick` returns 0 when timers are firing, sleep is skipped
- [ ] Connect a client during idle — verify connection accepted within one timer tick (~8ms)
- [ ] Verify `[admin` gump shows reasonable CPS values (EMA convergence)
## Summary
Server-side movement throttle that prevents speed hacking while accurately identifying cheaters with detection of lagging connections.
**Key features:**
- Credit buffer (200ms) absorbs timing jitter from legitimate players
- Movement queue handles larger bursts, draining at proper game-tick intervals
- RTT measurement distinguishes network lag from speed hacks
- Queue depth detection catches ACK-throttled speed hacks (going straight)
## How It Works
**Throttle** (prevention): Movements arriving too early either consume credit or get queued. The queue drains at
correct intervals, so speed hackers can't move faster regardless of what they send.
**Detection** (identification): Combines multiple signals to identify cheaters:
| Signal | What it catches |
|--------|-----------------|
| Queue depth ≥4 sustained | ACK-throttled speed hacks (client limits unacked moves to 5) |
| Movement rate >1.05x | Direction-change speed hacks where timing is visible |
| Stable RTT + high queue | Eliminates false positives from laggy players |
**RTT-Aware Logic:**
- Probes only sent to players actively moving (event-driven, not global loop)
- Stable low-latency + problems = suspicious
- Unstable/high-latency + problems = probably just lag, throttle handles it
## Configuration
```json
{
"movementThrottle.maxCredit": 200,
"movementThrottle.softQueueLimit": 6,
"movementThrottle.hardQueueLimit": 10,
"movementThrottle.debugLogging": false
}
```
## Summary
- Adds `public virtual bool Murderer => Kills >= 5` property to `Mobile`, replacing ~30 scattered `Kills >= 5` / `Kills < 5` magic-number checks across the codebase
- `BaseCreature` overrides `Murderer` to also return `true` when `AlwaysMurderer` is set
- Updates `Corpse` serialization to v15, storing `_murderer` as a `bool` field (migrated from `int Kills >= 5` in earlier versions)
### Summary
Updates all calls to container.EnumerateItems() to properly dispose of the underlying PooledRefQueue so that we are properly recycling pooled arrays.
### Summary
- Bump IORingGroup 1.0.0 → 1.0.1 — fixes a disconnect handling bug in the native ring layer
- Fix ghost NetStates — Dispose() set _running = false before checking it, so the "force immediate disconnect" path
was dead code. Capture wasRunning before clearing it, add [Obsolete] guard, and route internal callers through
DisposeInternal()
- Fix unauthenticated socket cleanup — graceful disconnect on unauthed connections could get stuck with pending sends;
now force-immediate after Disconnect() if DisconnectPending is already set
- Replace ConcurrentQueue<NetState> _disposed with Queue<NetState> — server is single-threaded; moved the field into
the Network partial class where it's consumed
- Move ConnectingSocketIdleLimit into the Network partial class alongside DisconnectUnattachedSockets
- Reset activity timer on receive, not just send — receiving data directly proves liveness instead of relying on the
ping→pong→send round-trip to reset the timer
- Move CheckAllAlive from Timer into Slice — the timer fired before I/O completions were processed, so after server
stalls (world saves), buffered client pings hadn't reset timestamps yet, causing false disconnects. Now runs at the
end of Slice() after all recv completions are handled
- Lower inactivity timeout 90s → 30s, check interval 90s → 5s — clients ping every ~1s, so 30s of silence is ~30
missed pings; worst-case detection drops from ~180s to ~35s
- Simplify CheckAlive — early-return when socket is null or alive; force-kill stuck DisconnectPending sockets
immediately instead of calling Disconnect() again
- Remove unused imports from GameEncryption.cs
### Summary
Changes component verification so the order is now housing.bin, then txt files in client, then txt files in Data/Components folder on the server.
> [!IMPORTANT]
> **Breaking Changes**
> - DecodePacket and EncodePacket delegates replaced with IClientEncryption interface
> - NetState.Connection (Socket) replaced with internal RingSocket management
> - NetState.RecvPipe and NetState.SendPipe removed (buffers managed internally)
## Summary
Upgrades the networking stack from PollGroup-based I/O to io_uring, significantly improving I/O performance on Linux.
This also adds native client encryption support for encrypted UO clients.
## Major Changes
io_uring Networking Architecture
- Replaced PollGroup with IORingGroup for async socket I/O operations
- Removed Pipe.cs (mirrored ring buffer) and TcpServer.cs in favor of RingSocketManager
- Added NetState.Network.cs - centralized network infrastructure handling accept, recv, send, and disconnect
completions
- Added SocketHelper.cs - platform-specific socket utilities for raw socket handle operations (getpeername,
getsockname)
- Buffer management now handled by RingSocketManager with configurable slab allocation
### Client Encryption Support
- Added full encryption stack in Network/Encryption/:
- EncryptionConfig.cs - configurable encryption modes (None, Unencrypted, Encrypted, Both)
- EncryptionManager.cs - encryption detection and initialization for login/game packets
- LoginEncryption.cs - handles login packet encryption with version-derived keys
- GameEncryption.cs - handles game server encryption using Twofish
- TwofishEngine.cs - optimized Twofish block cipher implementation
- LoginKeys.cs - encryption key table for client versions
- IClientEncryption.cs - interface for client encryption implementations
### NetState Improvements
- Replaced Socket Connection with RingSocket _socket for managed socket lifecycle
- Changed from GCHandle polling to event-based completion processing
- Disconnect handling now properly waits for pending sends to flush
- Simplified connecting socket management using lazy queue removal
### Configuration
- New settings: network.encryptionMode and network.encryptionDebug
- Encryption mode flags: Unencrypted, Encrypted, or Both
### Dependencies
- Replaced PollGroup NuGet package with IORingGroup
- Linux requires liburing-dev / liburing-devel package
### Test plan
- Verify server starts and accepts connections on Linux with io_uring
- Verify server starts and accepts connections on Windows (fallback to IOCP)
- Test unencrypted client connections (ClassicUO with encryption disabled)
- Test encrypted client connections if available
- Verify graceful disconnect flushes pending data
- Confirm CI builds pass on all target platforms
## Summary
- Adds proper Latin1 encoding support, replacing CP1252 usage throughout the codebase
- Adds specialized, optimized string decoding methods with safe string filtering for each encoding type
- Filters invalid Unicode characters (C0/C1 control codes, non-characters) by removal rather than replacement since
the UO client renders nothing for these characters
- Fixes UTF-16 null terminator position handling to correctly advance by 2 bytes
## Changes
TextEncoding.cs
- Added SearchValues-based invalid byte/char detection for efficient filtering
- Added encoding-specific GetString methods: GetStringAscii, GetStringLatin1, GetStringUtf8, GetStringBigUni,
GetStringLittleUni
- Each method supports a safeString parameter for filtering invalid characters
- Little-endian UTF-16 uses direct memory cast for zero-copy decoding on LE systems
- Invalid characters are removed (not replaced with U+FFFD) since the client renders nothing for them
SpanReader.cs
- Added ReadLatin1() and ReadLatin1Safe() methods
- Rewrote encoding-specific read methods to use optimized TextEncoding.GetString* methods
- Fixed UTF-16 null terminator handling: position now correctly advances by byteLength (2) instead of 1
SpanWriter.cs
- Added WriteLatin1 and WriteLatin1Null methods
## Packet Updates
- Updated all packet code to use Latin1 encoding instead of CP1252
- Affected: account packets, equipment packets, menu packets, message packets, mobile packets, player packets, secure
trade packets, vendor packets, gump packets, book packets, mahjong packets
## Filtering Behavior
Invalid characters filtered in safe mode:
```
┌───────────────┬────────────────────────┐
│ Range │ Description │
├───────────────┼────────────────────────┤
│ 0x00-0x1F │ C0 control codes │
├───────────────┼────────────────────────┤
│ 0x7F │ DEL │
├───────────────┼────────────────────────┤
│ 0x80-0x9F │ C1 control codes │
├───────────────┼────────────────────────┤
│ 0xFFFE-0xFFFF │ Unicode non-characters │
└───────────────┴────────────────────────┘
```
Note: Surrogate pairs (0xD800-0xDFFF) are not filtered because proper validation requires context checking for paired
vs unpaired surrogates. The UO client renders nothing for these anyway.
## Test Plan
- All 631 Server.Tests pass
- Verified client rendering behavior using TestUnicodeGump command (pages 1-5)
- Confirmed U+FFFD, unpaired surrogates, and non-characters all render as blank in client
- Verified Latin1 characters (0xA0-0xFF) display correctly
- Verified C1 control codes (0x80-0x9F) are filtered and don't display
### Summary
* Fixes migration a socket from one poll group to another
* Fixes epoll/wepoll incompatibility
* Updates minimum support to Windows 10+ (Server 2019+)
* Enhances performance by supporting synchronous IOCP completions when available (up to 30% increase in performance).
Fix [add command failing with ambiguous type names + refactor for performance
### Problem
[add blight would fail with "No type with that name was found" because multiple types contain "blight" (e.g., Server.Items.Blight, Server.Ethics.Evil.Blight, Server.Items.BlightGrippedLongbow, Server.Items.QuiverOfBlight). The old code only succeeded when exactly one type matched the search regardless of constructability and inheriting Mobi les/Items.
### Solution
Exact match takes priority: If a type's name exactly equals the search string (case-insensitive), use it directly. Otherwise, show the AddGump with all partial matches.
- [add blight → Creates Blight (exact name match)
- [add bligh → Shows gump with Blight, BlightGrippedLongbow, etc.
### Refactoring
- CommandEventArgs context: Added GetContext<T>/SetContext<T> to pass resolved type through the command chain without method signature changes
- Removed TrySetupTarget duplication: Validation now happens only in ValidateArgs, eliminating redundant code paths
- Split type matching:
- ExactMatch(string) → Returns Type for exact name match (used by [add)
- MatchEmptyCtor(string) → Returns ConstructorInfo[] for gump display (empty-callable constructors only)
### Memory & Performance Improvements
| Optimization | Benefit |
|-------------------------------|----------------------------------------------------------------------------------------------|
| _mobileItemTypes cache | Filters Mobile/Item types once per assembly, reused on all subsequent searches |
| ReadOnlySpan<string> for args | Avoids string[] heap allocations when slicing arguments |
| ValueStringBuilder | Stack-allocated string building, avoids StringBuilder heap allocation |
| Single type resolution | Type resolved once in ValidateArgs, passed via context to Execute (was resolved 2-3x before) |
### 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
### Summary
- Adds CanSpawnMobile(x, y, minZ, maxZ, canSwim, cantWalk, out spawnZ) overload for finding spawn surfaces within a Z range
- Adds CanSpawnItem(x, y, minZ, maxZ, out spawnZ) for item spawning with Surface+Impassable support (tables, furniture)
- Uses bitmask optimization inspired by Item.DropToWorld's m_OpenSlots pattern for O(1) surface/blocker checks
- HomeRange spawners now use surface detection to set proper Z bounds
### Key Changes
Map.cs:
- CanSpawnMobile with Z-range finds lowest valid surface for mobiles
- CanSpawnItem with Z-range finds lowest valid surface for items (including tables)
- CanFitItem for point-check item placement on Surface+Impassable tiles
- Bitmask approach eliminates nested loops and stackalloc arrays
Spawners:
- Simplified GetSpawnPosition using new Z-range methods
- HomeRange setter detects surface below spawner for proper Z bounds
- Consistent handling for mobiles and items
### Bug Fixes
- Water tiles (Impassable | Wet) no longer block swimming mobs
- Items can now spawn on tables/furniture (Surface+Impassable)
### Test Plan
- Run dotnet test - 631 tests pass
- Manual testing: multi-story spawning, water mobs, item spawning on tables
- Verify HomeRange spawner movement shifts bounds correctly
### Summary
This PR transitions the spawner system from a simple radius-based model to a flexible 3D boundary system.
### Core Changes
* **Replaced `HomeRange` with `SpawnBounds`**: Spawners now use a `Rectangle3D` to define spawn areas instead of a circular integer range.
* **Backward Compatibility**:
* The `HomeRange` property remains as a helper that generates square `SpawnBounds` centered on the spawner.
* Included a migration path (v10 to v11) that automatically converts old range data into new bounds during deserialization.
* **Dynamic Bounds Shifting**: If a spawner is moved, its `SpawnBounds` will automatically shift with it, provided the bounds are currently configured as a centered square.
* **New Spawn Logic**: Added `SpawnLocationIsHome` toggle. If enabled, spawned mobiles treat their exact spawn coordinates as their "Home" rather than the spawner's location.
### Implementation Details
* **Interface Updates**: Updated `ISpawner` to include `WalkingRange`, `SpawnBounds`, and `IsInSpawnBounds()`.
* **UI Enhancements**: The Spawner Controller Gump now displays "Custom" for complex bounds and allows copying of the new boundary properties between spawners.
* **Refactored Constructors**: Streamlined `BaseSpawner`, `ProximitySpawner`, and `RegionSpawner` constructors to support the new data types.
### Summary
* Adds better `SaveFlag` support.
* Fixes `SortedSet` support.
* Adds custom comparer support for `SortedSet`.
* Fixes various bugs.
* Adds support for `struct`, `record`, and `generic` classes.
* Adds support for `readonly` fields by skipping serializaton entirely.
* Adds support for interface fields that inherit ISerializable
* Adds tests.
See: 59a92cc49a
### Summary
Adds `XInRangeByDistance` and `XInBoundsByDistance` methods to `Map.cs`:
**Item Distance Enumeration:**
```cs
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p);
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p) where T : Item;
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p);
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p) where T : Item;
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(int x, int y, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(int x, int y, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInBoundsByDistance(Rectangle2D bounds, , bool makeBoundsInclusive = false);
ItemDistanceEnumerable<T> GetItemsInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Item;
```
**Mobile Distance Enumeration:**
```cs
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p);
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p) where T : Mobile;
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p);
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p) where T : Mobile;
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(int x, int y, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(int x, int y, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false);
MobileDistanceEnumerable<T> GetMobilesInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Mobile;
```
**Client Distance Enumeration:**
```cs
ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p, int range);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p, int range);
ClientDistanceEnumerable GetClientsInRangeByDistance(int x, int y, int range);
ClientDistanceEnumerable GetClientsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false);
```
**Example Usage:**
How to use `minDistance` to terminate early when all subsequent mobiles in the iteration will be at an increasing min distance.
```csharp
var playerLocation = player.Location;
const int maxRange = 100;
const int maxMobiles = 12;
var closestMobiles = new SortedSet<Mobile>(Comparer<Mobile>.Create((x, y) =>
{
var distX = x.GetDistanceToSqrt(playerLocation);
var distY = y.GetDistanceToSqrt(playerLocation);
int result = distX.CompareTo(distY);
if (result == 0)
{
result = (x?.Serial ?? Serial.MinusOne).CompareTo(y?.Serial ?? Serial.MinusOne);
}
return result;
}));
int lastMinDistance = 0;
foreach (var (mobile, minDistance) in map.GetMobilesInRangeByDistance(playerLocation, maxRange))
{
// Stop if we have enough and distance starts increasing
if (closestMobiles.Count >= maxMobiles && minDistance > lastMinDistance)
{
break;
}
closestMobiles.Add(mobile);
lastMinDistance = minDistance;
}
// Results are already ordered by proximity
foreach (var mobile in closestMobiles)
{
var actualDistance = mobile.GetDistanceToSqrt(playerLocation);
Console.WriteLine($"{mobile.Name}: ActualDist={actualDistance:F2}");
}
```
> [!IMPORTANT]
> **Dev Note:** This is an **important** patch as the bug could lead to major issues like:
> * Multis/Players disappearing from view or not being counted during game logic.
> * World processes (e.g., area checks, targeting) failing to detect entities correctly.
> * General stability and correctness concerns for core map functionality.
>
> **Important Breaking Change**: Multis now properly use the map link list. This means modifying a multi while iterating will cause the server to crash. The crash _is expected_. Please modify/fix code accordingly to create a list using `PooledRefQueue` or `PooledRefList` instead of moving/deleting multis while inside the foreach.
### Summary
* Fixes a bug where deleting/moving a multi (boat/house) in some circumstances can use undefined behavior due to unsafe changes to List<BaseMulti>
* Fixes a bug where Multis may not be considered while searching due to a bug causing the sector search to end early.
> [!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.