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
This commit is contained in:
Kamron Batman 2025-12-27 17:01:15 -08:00 • committed by GitHub
parent ebaf104935
commit 6d51b33cf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1604 additions and 214 deletions

View file

@ -1,38 +1,43 @@
using System;
using System.Reflection;
using Xunit;
namespace Server.Tests;
/// <summary>
/// Fixture for all server tests. Attempts to load TileData from client files if available.
/// Configure client path via MODERNUO_CLIENT_PATH environment variable or place files at C:\Ultima Online Classic.
/// </summary>
[CollectionDefinition("Sequential Server Tests", DisableParallelization = true)]
public class ServerFixture : ICollectionFixture<ServerFixture>, IDisposable
{
/// <summary>
/// True if TileData was successfully loaded from client files.
/// </summary>
public static bool TileDataLoaded => TestServerInitializer.TileDataLoaded;
/// <summary>
/// A tile ID known to have the Surface flag. 0 if not found.
/// </summary>
public static ushort SurfaceTileId => TestServerInitializer.SurfaceTileId;
/// <summary>
/// A tile ID known to have the Impassable flag. 0 if not found.
/// </summary>
public static ushort ImpassableTileId => TestServerInitializer.ImpassableTileId;
/// <summary>
/// A tile ID known to have the Wet flag (water). 0 if not found.
/// </summary>
public static ushort WetTileId => TestServerInitializer.WetTileId;
/// <summary>
/// A tile ID known to have both Surface and Impassable flags (tables, furniture). 0 if not found.
/// </summary>
public static ushort SurfaceImpassableTileId => TestServerInitializer.SurfaceImpassableTileId;
public ServerFixture()
{
Core.ApplicationAssembly = Assembly.GetExecutingAssembly(); // Server.Tests.dll
// Load Configurations
ServerConfiguration.Load(true);
// Load an empty assembly list into the resolver
ServerConfiguration.AssemblyDirectories.Add(Core.BaseDirectory);
AssemblyHandler.LoadAssemblies(["Server.dll"]);
Core.LoopContext = new EventLoopContext();
Core.Expansion = Expansion.EJ;
// Configure / Initialize
TestMapDefinitions.ConfigureTestMapDefinitions();
// Configure the world
World.Configure();
Timer.Init(0);
// Load the world
World.Load();
World.ExitSerializationThreads();
TestServerInitializer.Initialize(loadTileData: true);
}
public void Dispose()