### 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.
New Jail System for MUO
----------------------------
Commands:
[jail [player] [reason] - Jail with time escalation per offense (5 to 120 minutes, GM only)
[unjail [player] - Manual release from jail regardless of time (GM only)
[jailinfo [player] - Check jail status and history (GM only)
[jailrecord - Checks their own jail record stats (player access)
Jail/Unjail can be found in the client view of a player in Admin Gump

Jail record gump, invoked with [jailrecord (30 second cooldown)

Fixes serialization/deserialization edge cases with BitArray. If you use BitArray, you will need to migrate.
1. Change the type in the migration JSON file (if there is one) from `BitArray` to `byte[]` for all the versions you need to migrate.
2. Then in the `MigrateFrom`, use the following function to convert the field from a byte[] back to the BitArray.
Example Migration JSON:
```json
{
"name": "RestrictedSpells",
"type": "byte[]",
"rule": "ArrayMigrationRule",
"ruleArguments": [
"byte",
"PrimitiveTypeMigrationRule",
""
]
},
```
Migration function to use in MigrateFrom:
```cs
public static BitArray MigrateBitArray(byte[] data, int bitLength) => new(data) { Length = bitLength };
```
Example use:
```cs
private void MigrateFrom(V0Content content)
{
// ... deserialize
_restrictedSpells = content.RestrictedSpells.MigrateBitArray(SpellRegistry.Types.Length);
_restrictedSkills = content.RestrictedSkills.MigrateBitArray(SkillInfo.Table.Length);
// ... rest of deserialize
}
```
### Summary
Refactors ScheduledEvent and EventScheduler API to use TimeOnly so recurrence offset is explicit.
API:
```cs
public ScheduledEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
)
```
Example:
```cs
// Schedule a daily event at 8:00 AM UTC
EventScheduler.DailyAt(
new DateTime(2024, 6, 1, 8, 0, 0, DateTimeKind.Utc),
() => Console.WriteLine("Daily event triggered!")
);
// Schedule a custom recurring event at 3:30 PM UTC every Monday
var recurrence = new WeeklyRecurrencePattern(1, DaysOfWeek.Monday);
EventScheduler.Shared.ScheduleEvent(
DateTime.UtcNow,
new TimeOnly(15, 30),
() => Console.WriteLine("Weekly Monday event!"),
recurrence
);
```
### Summary
* Adds an event scheduler.
* Adds conveniences for hourly, daily, weekly, biweekly, monthly, ordinal monthly, and yearly recurrences
Example:
```cs
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
// Specify the time of the day, and the day of the week you want it to occur. Make sure it is translated into Utc.
// The next occurrence will be _after_ the specified date/time.
var scheduledEvent = EventScheduler.WeeklyAt(new DateTime(2025, 04, 26, 17, 00, 00), StartEvent, tz);
void StartEvent()
{
World.Broadcast(0x30, false, "The event has started!");
}
Console.WriteLine("Event starts on {0}", scheduledEvent.NextOccurrence);
```
In this example, on _Saturday, May 3rd, 2025 @ 5pm ET_, the message "The event has started!" will be broadcasted.
### 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.
### Summary
* Optimized GetString by eliminating the intermediate string allocation.
** Note **: Encoding.GetChars() is still really inefficient, especially when strings are not aligned or have regular ascii/unicode characters. Thankfully we generally don't have to worry about these odd edge cases, but if they happen then GetChars can allocate hundreds of bytes.
This is a benchmark for just the related changes. NonSpecial are just ascii characters, while the other tests include control codes.
```cs
| Method | Mean | Error | StdDev | Gen0 | Allocated |
|------------------------------- |---------:|--------:|--------:|-------:|----------:|
| GetString | 306.7 ns | 5.96 ns | 6.86 ns | 0.0124 | 200 B |
| GetStringNotSpecial | 257.0 ns | 4.83 ns | 4.52 ns | 0.0114 | 184 B |
| GetStringSpanHelpers | 166.4 ns | 3.26 ns | 3.48 ns | - | - |
| GetStringSpanHelpersNotSpecial | 137.3 ns | 1.57 ns | 1.22 ns | - | - |
```
### Summary
On some operating systems (like hosted Linux VMs), the `TimeStamp.GetTimeStamp()` CPU tick count will wrap around. This is generally not an issue, except for legacy reasons the TickCount is returned in milliseconds instead of ticks. This means when the values wrap around, they are already divided by the CPU Frequency (usually 1million) and then converted to milliseconds. That means the delta between the tick count before and after wrapping is off by a magnitude of (Frequency / 1000).
Example:
TimeStamp A = 9223372036654775807
Some time has passed:
TimeStamp B = -9223372036654775809
The raw delta is 400_000_000 (400ms) when you do `unchecked(A - B)`.
If we do the calculation AFTER converting it to milliseconds, then:
TickCount A = 9223372036654
TickCount B = -9223372036654
The raw delta is -18446744073308 instead of 400_000_000.
To fix this the calculation was changed so `long` -> `ulong`, then divided, then converted back to `long`, effectively bypassing wrap-around issue.
The new TickCount values in our example become:
TickCount A = 9223372037054
TickCount B = 9223372036654
The delta is 400 (in milliseconds). 🎉