ModernUO/Projects/UOContent/Engines/Spawners/BaseSpawner.Migrations.cs
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

69 lines
2.2 KiB
C#

using System.Collections.Generic;
using Server.Items;
namespace Server.Engines.Spawners;
public abstract partial class BaseSpawner
{
// Static dictionary to track pending HomeRange migrations from v10
// Keyed by spawner instance, stores the HomeRange value to convert to SpawnBounds
private static readonly Dictionary<BaseSpawner, int> _pendingHomeRangeMigrations = new();
private void MigrateFrom(V10Content content)
{
_guid = content.Guid;
_returnOnDeactivate = content.ReturnOnDeactivate;
_entries = content.Entries;
_walkingRange = content.WalkingRange;
_wayPoint = content.WayPoint;
_group = content.Group;
_minDelay = content.MinDelay;
_maxDelay = content.MaxDelay;
_count = content.Count;
_team = content.Team;
_running = content.Running;
_end = _running ? content.End : Core.Now;
// Defer SpawnBounds calculation to AfterDeserialization when Location is available
if (content.HomeRange > 0)
{
_pendingHomeRangeMigrations[this] = content.HomeRange;
}
_spawnLocationIsHome = false;
}
private void Deserialize(IGenericReader reader, int version)
{
_guid = reader.ReadGuid();
_returnOnDeactivate = reader.ReadBool();
var count = reader.ReadInt();
_entries = new List<SpawnerEntry>(count);
for (var i = 0; i < count; ++i)
{
var entry = new SpawnerEntry(this);
entry.Deserialize(reader);
_entries.Add(entry);
}
_walkingRange = reader.ReadInt();
_wayPoint = reader.ReadEntity<WayPoint>();
_group = reader.ReadBool();
_minDelay = reader.ReadTimeSpan();
_maxDelay = reader.ReadTimeSpan();
_count = reader.ReadInt();
_team = reader.ReadInt();
// Store homeRange for migration in AfterDeserialization when Location is available
var homeRange = reader.ReadInt();
if (homeRange > 0)
{
_pendingHomeRangeMigrations[this] = homeRange;
}
_running = reader.ReadBool();
_end = _running ? reader.ReadDeltaTime() : Core.Now;
}
}