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:
parent
ebaf104935
commit
6d51b33cf8
15 changed files with 1604 additions and 214 deletions
176
Projects/Server.Tests/Fixtures/TestServerInitializer.cs
Normal file
176
Projects/Server.Tests/Fixtures/TestServerInitializer.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Shared server initialization logic for test fixtures.
|
||||
/// Ensures the server is only initialized once across all test collections.
|
||||
/// </summary>
|
||||
public static class TestServerInitializer
|
||||
{
|
||||
private const string DefaultDataDirectory = @"C:\Ultima Online Classic";
|
||||
private static bool _initialized;
|
||||
private static readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// True if TileData was successfully loaded from client files.
|
||||
/// </summary>
|
||||
public static bool TileDataLoaded { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tile ID known to have the Surface flag. 0 if not found.
|
||||
/// </summary>
|
||||
public static ushort SurfaceTileId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tile ID known to have the Impassable flag. 0 if not found.
|
||||
/// </summary>
|
||||
public static ushort ImpassableTileId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tile ID known to have the Wet flag (water). 0 if not found.
|
||||
/// </summary>
|
||||
public static ushort WetTileId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tile ID known to have both Surface and Impassable flags (tables, furniture). 0 if not found.
|
||||
/// </summary>
|
||||
public static ushort SurfaceImpassableTileId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test server. Safe to call multiple times - only initializes once.
|
||||
/// </summary>
|
||||
/// <param name="loadTileData">If true, attempts to load TileData from client files.</param>
|
||||
public static void Initialize(bool loadTileData = false)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
// Already initialized, but check if we need to load TileData now
|
||||
if (loadTileData && !TileDataLoaded && TileData.MaxItemValue == 0)
|
||||
{
|
||||
TryLoadTileData();
|
||||
DetectTileIds();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Core.ApplicationAssembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
// Load Configurations
|
||||
ServerConfiguration.Load(true);
|
||||
|
||||
// Try to load TileData if requested
|
||||
if (loadTileData)
|
||||
{
|
||||
TryLoadTileData();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// Detect tile IDs if TileData was loaded
|
||||
if (loadTileData)
|
||||
{
|
||||
DetectTileIds();
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryLoadTileData()
|
||||
{
|
||||
var dataDir = GetDataDirectory();
|
||||
if (string.IsNullOrEmpty(dataDir) || !Directory.Exists(dataDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ServerConfiguration.DataDirectories.Add(dataDir);
|
||||
|
||||
var tileDataPath = Core.FindDataFile("tiledata.mul", false);
|
||||
if (File.Exists(tileDataPath))
|
||||
{
|
||||
TileData.Load();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DetectTileIds()
|
||||
{
|
||||
if (TileData.MaxItemValue == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SurfaceTileId = FindTileWithFlag(TileFlag.Surface);
|
||||
ImpassableTileId = FindTileWithFlag(TileFlag.Impassable);
|
||||
WetTileId = FindTileWithFlag(TileFlag.Wet);
|
||||
SurfaceImpassableTileId = FindTileWithFlags(TileFlag.Surface | TileFlag.Impassable);
|
||||
TileDataLoaded = SurfaceTileId > 0 && ImpassableTileId > 0 && WetTileId > 0;
|
||||
}
|
||||
|
||||
private static ushort FindTileWithFlag(TileFlag flag)
|
||||
{
|
||||
for (ushort i = 1; i <= TileData.MaxItemValue && i < 0xFFFF; i++)
|
||||
{
|
||||
var data = TileData.ItemTable[i];
|
||||
if ((data.Flags & flag) != 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static ushort FindTileWithFlags(TileFlag flags)
|
||||
{
|
||||
for (ushort i = 1; i <= TileData.MaxItemValue && i < 0xFFFF; i++)
|
||||
{
|
||||
var data = TileData.ItemTable[i];
|
||||
if ((data.Flags & flags) == flags)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetDataDirectory()
|
||||
{
|
||||
// Check environment variable first
|
||||
var envDir = System.Environment.GetEnvironmentVariable("MODERNUO_CLIENT_PATH");
|
||||
if (!string.IsNullOrEmpty(envDir) && Directory.Exists(envDir))
|
||||
{
|
||||
return envDir;
|
||||
}
|
||||
|
||||
// Fall back to default directory
|
||||
if (Directory.Exists(DefaultDataDirectory))
|
||||
{
|
||||
return DefaultDataDirectory;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue