Commit graph

859 commits

Author SHA1 Message Date
Kamron Batman
1a7c94a442
feat: Adds Network Packet Documentation (#2302) 2026-01-01 17:52:56 -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
9887818e7e
fix: Prevent cascading deletes during world deserialization (#2298)
### Summary

Fixes an issue where deleting entities during world load can cause cascading delete failures when the world is not fully loaded.
2025-12-28 17:54:53 -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
bde072f81c
feat: Bumps Serialization Generator for better support (#2292)
### 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
2025-12-26 14:27:24 -08:00
Kamron Batman
1f0f272915
feat: Asks the name of the shard (#2283) 2025-11-29 12:37:09 -08:00
Kamron Batman
6a7d49b679
fix: Fixes allocating CompactInfo when changing the weight of an item (#2282)
### Summary

* Fixes extra allocations and computing weight values when changing the weight of an item.
2025-11-29 12:28:27 -08:00
Kamron Batman
5fb0e806c5
feat: Adds proper OSI tracking up to 120 tiles. (#2281)
### Summary

Fixes tracking skill to 10 tiles per 10% up to 120 tiles.
2025-11-29 09:55:52 -08:00
Kamron Batman
ad26ab6260
fix: Fixes returning min distance from GetXInRangeByDistance (#2280) 2025-11-28 11:56:06 -08:00
Kamron Batman
06443ba0ca
fix: Cleans up the GetXDistance methods. (#2279) 2025-11-28 11:01:20 -08:00
Kamron Batman
f2ce860c18
feat: Adds Map.GetXByDistance (for tracking skill). Fixes negative range checks. (#2252)
### 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}");
}
```
2025-11-28 10:57:54 -08:00
Kamron Batman
d3fdb180b3
fix: Fixes searching multis/clients. Adds missing map enumeration tests (#2278)
> [!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.
2025-11-27 11:59:47 -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
4836bff5eb
fix: Eliminates List allocations in various places. (#2158) 2025-11-16 18:33:53 -08:00
Kamron Batman
6f64cddd0b
feat: Optimizes HTML Escape (#2273)
### Summary

Optimizes HTML escaping by using a vectorized search.
2025-11-16 10:32:29 -08:00
Kamron Batman
6f5b7f7a6b
fix: Fixes SpanWriter/SpanReader tests (#2263) 2025-11-13 00:00:19 -08:00
Kamron Batman
68caaab92c
fix: Makes SpanWriter/SpanReader exceptions clearer (#2262) 2025-11-12 23:34:13 -08:00
Kamron Batman
3a3f5ee518
feat: Adds .NET 10 / C# 14 support. (#2258)
### Summary
* Adds .NET 10 support
* Bumps to C# 14
2025-11-11 11:26:34 -08:00
Kamron Batman
df171926bb
fix: Fixes potential notoriety caching issue in Mobile.cs (#2251)
Replaces the 2D packet cache with a simpler one outlining the packet flag changes
2025-09-11 06:32:48 -07:00
Kamron Batman
c2e44a58f6
fix: Moves container enumerables to Item. (#2238) 2025-07-24 14:26:35 -07:00
Kamron Batman
2df62fba5a
chore: Bumps various dependencies (#2237) 2025-07-22 16:04:04 -07:00
Bohica
173037ae77
fix: Prevents recursion in BaseCreature.DoHarmful() (#2202) 2025-07-22 16:01:56 -07:00
Kamron Batman
416e1be735
fix: Adds GetDistanceToSqrt for Point3D and centralizes all the calls (#2233) 2025-07-18 09:24:41 -07:00
Bohica
5f0561b7fc
feat: New Jail System (#2215)
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
![Screenshot 2025-06-12 030226](https://github.com/user-attachments/assets/a9f1a736-0316-464c-8958-c589ea0a1dcb)

Jail record gump, invoked with [jailrecord (30 second cooldown)
![Screenshot 2025-06-12 030320](https://github.com/user-attachments/assets/c19b3e76-3042-48ae-a00d-c70ef564bc84)
2025-07-16 20:41:21 -07:00
Kamron Batman
edf16effa0
fix: Fixes container not properly consuming in some cases (#2230) 2025-07-06 10:23:37 -07:00
Bohica
d3e4606050
fix: Fixes SpanReader.Read to fix bounds checks and now returns bytes written (#2211) 2025-06-12 17:25:32 -07:00
Kamron Batman
14bc38e375
fix: Removes the absurb 6 hour skill edge case and fixes target cancellations (#2212) 2025-06-07 13:58:01 -07:00
Kamron Batman
c47ecadace
fix: Fixes item child properties in the wrong order when overridden (#2196) 2025-05-24 12:15:49 -07:00
Kamron Batman
972e7723ae
fix: Reverts change for trade window that causes an exploit (#2189) 2025-05-17 21:25:48 -07:00
Kamron Batman
f2526c82f3
fix: Fixes Freeshard Protocol (UOGateway) support. (#2183) 2025-05-12 12:24:24 -07:00
Kamron Batman
05825a11e2
fix: Fixes props for interfaces and adds account manipulation (#2179) 2025-05-09 15:39:47 -07:00
Kamron Batman
4beda6a29d
fix: Eliminate intermediate string in typecache (#2176) 2025-05-06 21:04:54 -07:00
Kamron Batman
5cb97cc6de
fix: Fixes BitArray serialization (#2170)
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
    }

```
2025-04-30 19:45:55 -07:00
Kamron Batman
21a4092dd8
fix: Changes EventScheduler API so it is more explicit (#2164)
### 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
);
```
2025-04-28 16:16:33 -07:00
Kamron Batman
78feea86b4
feat: Adds scheduler with wallclock timer (#2163)
### 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.
2025-04-26 22:27:08 -07:00
Kamron Batman
e54782441e
fix: Fixes TcpServer edge cases where packets are split on login (#2160) 2025-04-15 19:29:17 -07:00
Kamron Batman
4aa272d429
feat: Adds an Item/Mobile memory leak detector. Fixes minor leak in doors. (#2159)
### Summary

Adds the command [TrackLeaks to enable tracking item/mobiles that have been deleted but still have dangling references. Requires adding the _TRACK_LEAKS_ define constant during build.
2025-04-15 19:14:45 -07:00
Kamron Batman
1e349f4369
fix: Fixes mutate speech character limit (#2157)
### Summary

* Fixes the accidental limitation of dead character speech (OoOo) to 256 characters.
* Optimizes it by 1.5x

```cs
| Method                  | Mean      | Error    | StdDev   | Allocated |
|------------------------ |----------:|---------:|---------:|----------:|
| ManuaLoopMutation       | 147.01 ns | 1.298 ns | 1.084 ns |         - |
| SpanLoopMutation        |  92.03 ns | 0.583 ns | 0.487 ns |         - |
```
2025-04-13 22:55:44 -07:00
Kamron Batman
61d074213b
chore(deps): Updates MailKit to 4.11.0 and Microsoft deps to 9.0.4 (#2156) 2025-04-12 12:54:40 -07:00
Kamron Batman
5a23d9f6f8
fix: Fixes an edge case in GetString that can cause a crash while parsing (#2155) 2025-04-12 12:52:10 -07:00
Kamron Batman
872de8d095
fix: Optimizes GetString to eliminate allocations (#2154)
### 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 |      - |         - |
```
2025-04-12 12:42:15 -07:00
Kamron Batman
fdf8c5cf23
fix: Fixes critical bug in TickCount calculation. (#2151)
### 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). 🎉
2025-04-09 16:46:53 -07:00
Kamron Batman
ad361001c5
fix: Fixes CUO connecting with new TCP Peek Filtering (#2137) 2025-03-04 11:00:20 -08:00
Kamron Batman
2a8c62e8be
fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134) 2025-02-27 22:19:38 -08:00
Kamron Batman
0265e0673a
fix: Fixes empty spellbooks with EA client on Pre-AOS (#2131) 2025-02-25 20:46:20 -08:00
Kamron Batman
9b361822f3
fix: Fixes issue with delete all of missing type on deserialize (#2129) 2025-02-19 15:36:29 -08:00