fix(ci): run test projects on CI; remove brittle OPL attribute tests (#2513)
## Problem Three coupled issues, each hiding the next: 1. **CI passed despite failing tests, with no test logs.** ([example run](https://github.com/modernuo/ModernUO/actions/runs/28639143286/job/84931544255) — the `Test` step produced zero output and the job went green.) 2. **Two `EmitsLowerStatReqWhenPassed` tests** fail with `KeyNotFoundException: '1060435'`. 3. Once CI actually ran the tests, **~337 UOContent tests failed** with `FileNotFoundException: tiledata.mul was not found` — the test bootstrap force-loaded copyrighted client data that CI doesn't have. ## Root causes & fixes ### 1. CI ran zero tests (`fix(ci)`) The `Test` step ran `dotnet test --no-restore`, but the `Build` step only restores/builds `Application` — never the test projects. Without a restore, the test projects have no `project.assets.json`, so `Microsoft.NET.Test.Sdk`'s targets aren't imported, they aren't recognized as test projects, and `dotnet test` runs the `VSTest` target against **zero** projects → no output, **exit 0**. - Both jobs now run `dotnet test --logger trx --results-directory ./TestResults` (test projects restore and run) **plus a guard** that fails the job if no `.trx` is produced — a permanent backstop against silent zero-test passes. ### 2. Impossible OPL tests (`fix(ci)` + `test(opl)`) #2501 deliberately emits `LowerStatReq` (`1060435`) **inline in each item**, not in `GetProperties`. A follow-up "fix" dropped the `lowerStatReq:` argument to make the tests compile but left the assertions expecting `1060435`. - Removed the two impossible tests, then removed the **entire `Tests/PropertyList/` OPL attribute set** from #2501: these assert exact cliloc/value/order of OPL emission per item base — a one-time proof of the #2501 rewire, now a permanent tax on modding (any admin reorder/value change/added line reddens the build). The one non-trivial case (LowerStatReq) is what just broke, because the test was wrong. Inline emission stays covered by the `BaseArmor`/`BaseClothing` tests. ### 3. Tile-data-dependent tests crashed CI (`test(uocontent)`) UOContent.Tests' collection-fixture constructor force-loaded `tiledata.mul` unconditionally. On CI (no client files) it threw, and xUnit failed **every test in the collection** with the same error — mostly collateral (packet/scheduler/spawner tests that don't need tile data). - Mirror Server.Tests' graceful pattern: `TestServerInitializer` probes for `tiledata.mul` and only loads tile/multi data (and runs the tile-dependent configure steps) when present, exposing `TileDataLoaded` so the fixture no longer throws. - Add a shared `TileDataRequirement.SkipIfMissing()` guard and apply it to exactly the **31** pathfinding/multi/AI tests that genuinely need real tile data (`[SkippableFact]`/`[SkippableTheory]`). ## Verification (all local) | Scenario | Server.Tests | UOContent.Tests | |---|---|---| | **Client data absent (CI)** | 726 pass, 17 skip, **0 fail** | 469 pass, 32 skip, **0 fail** | | **Client data present (dev)** | 726 pass, 0 skip, **0 fail** | 501 pass, 0 skip, **0 fail** | - Full `dotnet test` exits **0**; TRX files produced; the no-test guard trips (exit 1) only when zero `.trx` are produced.
This commit is contained in:
parent
8c6eab5fca
commit
a706ef1449
29 changed files with 124 additions and 546 deletions
|
|
@ -30,6 +30,13 @@ internal static class TestServerInitializer
|
|||
private static bool _initialized;
|
||||
private static readonly Lock _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// True if the UO client tile data was found and loaded. When false (e.g. CI, where the
|
||||
/// copyrighted client files are absent), tile/map/multi-dependent tests must skip rather than
|
||||
/// fail. Guard such tests with <c>Skip.If(!TestServerInitializer.TileDataLoaded, ...)</c>.
|
||||
/// </summary>
|
||||
public static bool TileDataLoaded { get; private set; }
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
|
|
@ -62,7 +69,10 @@ internal static class TestServerInitializer
|
|||
// flags are populated before anything that reads TileData (MultiData, MovementImpl,
|
||||
// CheckMovement). Without this, TileData.MaxItemValue is 0 at MultiData.Configure()
|
||||
// time, causing every MCL tile ID to be masked to 0 and stored as ID=0 in Tiles[x][y].
|
||||
ForceLoadTileData();
|
||||
// The copyrighted client files are absent on CI; when tiledata.mul is missing we skip
|
||||
// the tile/map/multi-dependent bootstrap and leave TileDataLoaded false so those tests
|
||||
// skip instead of failing the whole collection from the fixture constructor.
|
||||
TileDataLoaded = TryForceLoadTileData();
|
||||
|
||||
// Production runs every static Configure() via AssemblyHandler.Invoke("Configure");
|
||||
// the fixture calls a curated subset, so configure the pathfinding singleton here so
|
||||
|
|
@ -70,11 +80,15 @@ internal static class TestServerInitializer
|
|||
// calls Find. ServerConfiguration is already loaded above, so the setting resolves.
|
||||
BitmapAStarAlgorithm.Configure();
|
||||
|
||||
// Multi component lists (multi.mul / MultiCollection.uop). Production invokes this via
|
||||
// AssemblyHandler.Invoke("Configure"); the curated fixture subset must call it so that
|
||||
// BaseMulti.Components (MultiData.GetComponents) returns real footprints instead of
|
||||
// MultiComponentList.Empty. Required by the Multi pathfinding tests.
|
||||
MultiData.Configure();
|
||||
if (TileDataLoaded)
|
||||
{
|
||||
// Multi component lists (multi.mul / MultiCollection.uop). Production invokes this via
|
||||
// AssemblyHandler.Invoke("Configure"); the curated fixture subset must call it so that
|
||||
// BaseMulti.Components (MultiData.GetComponents) returns real footprints instead of
|
||||
// MultiComponentList.Empty. Required by the Multi pathfinding tests. Depends on the
|
||||
// client files, so it only runs when tile data loaded.
|
||||
MultiData.Configure();
|
||||
}
|
||||
|
||||
World.Configure();
|
||||
Timer.Init(0);
|
||||
|
|
@ -86,14 +100,23 @@ internal static class TestServerInitializer
|
|||
DecayScheduler.Configure();
|
||||
Server.Engines.Spawners.SpawnerJsonSerializer.Configure();
|
||||
|
||||
VerifyTrammelTileDataLoaded();
|
||||
if (TileDataLoaded)
|
||||
{
|
||||
VerifyTrammelTileDataLoaded();
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ForceLoadTileData()
|
||||
private static bool TryForceLoadTileData()
|
||||
{
|
||||
var tileDataPath = Core.FindDataFile("tiledata.mul", false);
|
||||
if (string.IsNullOrEmpty(tileDataPath) || !File.Exists(tileDataPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var loadMethod = typeof(TileData).GetMethod(
|
||||
"Load",
|
||||
BindingFlags.Static | BindingFlags.NonPublic
|
||||
|
|
@ -105,6 +128,7 @@ internal static class TestServerInitializer
|
|||
);
|
||||
}
|
||||
loadMethod.Invoke(null, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void VerifyTrammelTileDataLoaded()
|
||||
|
|
|
|||
18
Projects/UOContent.Tests/Fixtures/TileDataRequirement.cs
Normal file
18
Projects/UOContent.Tests/Fixtures/TileDataRequirement.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Shared guard for tests that require the copyrighted UO client tile/map data (tiledata.mul and
|
||||
/// friends), which is absent on CI. Call <see cref="SkipIfMissing"/> as the first statement of a
|
||||
/// <c>[SkippableFact]</c>/<c>[SkippableTheory]</c> so the test is skipped — not failed — when the
|
||||
/// data was not loaded. See <see cref="TestServerInitializer.TileDataLoaded"/>.
|
||||
/// </summary>
|
||||
internal static class TileDataRequirement
|
||||
{
|
||||
public static void SkipIfMissing() =>
|
||||
Skip.If(
|
||||
!TestServerInitializer.TileDataLoaded,
|
||||
"Requires UO client tile data (tiledata.mul); absent on CI."
|
||||
);
|
||||
}
|
||||
|
|
@ -130,9 +130,10 @@ public class BitmapAStarAlgorithmTests
|
|||
blocker.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void DynamicObstaclePass_RejectsCellOccupiedByImpassableItem()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ public class BoatPathTests
|
|||
private const int WaterX = 1450;
|
||||
private const int WaterY = 1770;
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void BoatDeck_HasWalkableSurfaceCells()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
|
||||
|
|
@ -44,9 +45,10 @@ public class BoatPathTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void BoatDeck_FootprintShape_IsPositionInvariant()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// The property Phase 2's local-frame, movement-invariant boat cache must preserve:
|
||||
// the deck's covered-cell shape (in local coords) is identical at two world positions.
|
||||
var map = Map.Maps[MapId];
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ public class FoundationRedesignTests
|
|||
private const int PlaceX = 1500;
|
||||
private const int PlaceY = 1600;
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void SwappingComponents_ChangesFootprint()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var foundation = new SwappableFoundation(0x74); // GuildHouse footprint
|
||||
try
|
||||
{
|
||||
|
|
@ -38,9 +39,10 @@ public class FoundationRedesignTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void RedesignReRegistered_RoutesNewFootprintToLivePath()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
Assert.NotNull(map);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ public class HousePathRoutingTests
|
|||
public WalkerStub() => Body = 0xC9;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void PathAround_NeverTraversesAWallCell()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var prevThreshold = StepCache.Instance.MissPromotionThreshold;
|
||||
StepCache.Instance.MissPromotionThreshold = 1;
|
||||
|
|
@ -74,9 +75,10 @@ public class HousePathRoutingTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void Demolish_ReopensCoveredCells()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
Assert.NotNull(map);
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ public class MultiEdgeCaseTests
|
|||
/// <c>GetStaticAndMultiTiles</c> yields tiles from BOTH multis; the synthesizer must still
|
||||
/// agree with CheckMovement everywhere over the union footprint + halo.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void OverlappingMultis_SynthesizerMatchesCheckMovement()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
|
||||
|
|
@ -75,9 +76,10 @@ public class MultiEdgeCaseTests
|
|||
/// blocks the rest — that vacuity is a fixture concern, not a synthesizer divergence (the
|
||||
/// synthesizer agrees with the oracle at every direction either way).
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void BoatOverWater_SynthesizerMatchesCheckMovement()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
|
||||
|
|
@ -98,9 +100,10 @@ public class MultiEdgeCaseTests
|
|||
/// re-registration pattern HouseFoundation uses on commit) and assert the synthesizer reads
|
||||
/// the LIVE, post-redesign <c>Components</c> — i.e. it matches CheckMovement on the NEW shape.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void RedesignedFoundation_SynthesizerMatchesNewFootprint()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
|
||||
|
|
@ -136,10 +139,11 @@ public class MultiEdgeCaseTests
|
|||
/// dungeon cave-wall corner; add InlineData rows here — the assertion already covers them by
|
||||
/// construction. (Left intentionally unhunted: do not invent tree/dungeon coords.)
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[SkippableTheory]
|
||||
[InlineData(MapId, HouseX, HouseY)] // known-good open Trammel placement (passes today)
|
||||
public void UserSuppliedScenarios_SynthesizerMatchesCheckMovement(int mapId, int x, int y)
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[mapId];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ public class MultiMaskCacheTests
|
|||
private const int PlaceX = 1480;
|
||||
private const int PlaceY = 1620;
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void TryResolveCoveringMulti_FindsPlacedMulti_AndLocalIndices()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
|
||||
|
|
@ -38,9 +39,10 @@ public class MultiMaskCacheTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void IsInteriorLocalCell_TrueDeepInside_FalseAtEdge()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var multi = new TestMulti(GuildHouseId);
|
||||
try
|
||||
{
|
||||
|
|
@ -122,9 +124,10 @@ public class MultiMaskCacheTests
|
|||
Assert.False(MultiMaskCache.TerrainTopBelow(map, PlaceX, PlaceY, (sbyte)(ground - 50)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void PathThroughHouseInterior_IncrementsMultiMaskCacheHits()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// (PlaceX,PlaceY)=(1480,1620) is cluttered (footprint overlaps tall map statics → dirty), so it
|
||||
// would never serve the interior cache under the footprint-clean gate. Use a known flat/clear
|
||||
// spot so a house there is footprint-clean and its interior cells serve from the cache.
|
||||
|
|
@ -242,9 +245,10 @@ public class MultiMaskCacheTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void ComputeFootprintClean_TrueAtNormalPlacement_FalseWhenSunk()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// (PlaceX,PlaceY) is a cluttered spot whose footprint overlaps tall map statics, so a guild
|
||||
// house there is never footprint-clean. Use a known flat/clear spot for the clean assertion.
|
||||
const int CleanX = 1560;
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ public class MultiMaskSynthesisTests
|
|||
private const int PlaceX = 1480;
|
||||
private const int PlaceY = 1620;
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void ComputeMultiMaskAt_MatchesCheckMovement_OverFootprintAndHalo()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ public class MultiSplitRoutingTests
|
|||
private const int PlaceX = 1500;
|
||||
private const int PlaceY = 1600;
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void PlacedMulti_RoutesFootprintAndHalo_ToFallthroughMulti()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
Assert.NotNull(map);
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ public class MultiWalkabilityTests
|
|||
public WalkerStub() => Body = 0xC9;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void WallCell_CannotBeEnteredFromAnyAdjacentCell()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
|
||||
|
|
@ -86,9 +87,10 @@ public class MultiWalkabilityTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void FloorCell_IsStandable()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
StepCache.Instance.Clear();
|
||||
var map = Map.Maps[MapId];
|
||||
map.GetAverageZ(PlaceX, PlaceY, out _, out var z, out _);
|
||||
|
|
|
|||
|
|
@ -347,9 +347,10 @@ public class StepCacheFileTests
|
|||
/// to <see cref="LazyReader_DoesNotMaterializeUntilQueried"/> which proves the
|
||||
/// default lazy behavior.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void TryOpenLazyReader_WithPreloadFlag_MaterializesAllChunksImmediately()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
cache.MissPromotionThreshold = 1;
|
||||
|
|
@ -401,9 +402,10 @@ public class StepCacheFileTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void LazyReaderHit_BypassesMissTrackerOnFirstTouch()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
cache.MissPromotionThreshold = 1; // eager build for save phase
|
||||
|
|
|
|||
|
|
@ -54,9 +54,10 @@ public class StepCacheLifecycleTests
|
|||
Assert.Equal(1L, stats.FallthroughNotBuilt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void TryGetMask_SecondTouchWithinWindow_PromotesAndBuilds()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
cache.MissPromotionThreshold = 2;
|
||||
|
|
@ -112,9 +113,10 @@ public class StepCacheLifecycleTests
|
|||
Assert.Equal(2L, cache.GetStats().FallthroughNotBuilt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void TryGetMask_MultipleCallsInSameFindGeneration_StayInFallthrough()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
cache.MissPromotionThreshold = 2;
|
||||
|
|
@ -207,9 +209,10 @@ public class StepCacheLifecycleTests
|
|||
Assert.Equal((byte)0, lookup.WalkMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void MultiCoveredCell_AndHalo_RouteToFallthrough()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var cache = StepCache.Instance;
|
||||
cache.Clear();
|
||||
cache.MissPromotionThreshold = 1; // eager build so a multi-free cell serves immediately
|
||||
|
|
|
|||
|
|
@ -111,9 +111,10 @@ public class StepCacheParityTests
|
|||
/// (Atlantic coast) and asserts at least one cell has a non-zero WetMask. Catches the
|
||||
/// failure mode where StepProbe silently bakes zero swim output everywhere.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void SwimBake_ProducesWetCells_OnKnownWaterRegion()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ public class StaticWalkabilityParityTests
|
|||
_output = output;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[SkippableTheory]
|
||||
[InlineData("britain_inn_dense", 1480, 1610, 32)]
|
||||
[InlineData("trammel_open_plain", 1500, 1600, 32)]
|
||||
public void BakerMatchesCheckMovement(string label, int xStart, int yStart, int size)
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
|
||||
|
|
|
|||
|
|
@ -124,9 +124,10 @@ public class StepProbeTests
|
|||
Assert.True(found, "Expected at least one fully-flat open cell in (1500..1532, 1600..1632)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_BritainInnDense_HasCellWithBlockedDirections()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// Invariant: inside the dense Britain inn region, at least one cell must have
|
||||
// at least one direction blocked by a static. Protects against a regression
|
||||
// where the baker reports everything as walkable (the original false-pass bug).
|
||||
|
|
@ -181,9 +182,10 @@ public class StepProbeTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_PinnedCell_TrammelOpenPlainOrigin()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// PINNING test: locks specific output for Trammel (1500, 1600).
|
||||
// Cell at z=10 with mask 0xC1 (N + W + NW only walkable). The other five
|
||||
// directions are blocked by water (E/SE/S/SW are wet tiles, NE is shore).
|
||||
|
|
@ -214,9 +216,10 @@ public class StepProbeTests
|
|||
Assert.Equal((sbyte)10, result.GetWalkZ(Direction.Up));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void ComputeMaskAt_PinnedCell_BritainInnDenseOrigin()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
// PINNING test: locks specific output for Trammel (1480, 1610).
|
||||
// Cell at z=20 with mask 0x3F (N/NE/E/SE/S/SW walkable, W/NW blocked by
|
||||
// a wall to the west). All walkable directions stay flat at z=20.
|
||||
|
|
|
|||
|
|
@ -176,9 +176,10 @@ public class ApproachTargetTests
|
|||
Assert.True(caught, "chaser must catch a target that walks away then stops");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void UnreachableTarget_GivesUp_AndIdles()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
map.GetAverageZ(1500, 1601, out _, out var z, out _);
|
||||
|
|
@ -243,9 +244,10 @@ public class ApproachTargetTests
|
|||
Assert.True(stayedIdle, "after giving up, the creature must idle, not shuffle");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[SkippableFact]
|
||||
public void WallBetween_RoutesAround_ReachesTarget()
|
||||
{
|
||||
TileDataRequirement.SkipIfMissing();
|
||||
var map = Map.Maps[1];
|
||||
Assert.NotNull(map);
|
||||
map.GetAverageZ(1500, 1600, out _, out var z, out _);
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class AosArmorAttributesPropertiesTests
|
||||
{
|
||||
private static Dictionary<int, string> Decode(ObjectPropertyList opl)
|
||||
{
|
||||
opl.Terminate();
|
||||
var buffer = opl.Buffer;
|
||||
var map = new Dictionary<int, string>();
|
||||
var pos = 15;
|
||||
while (true)
|
||||
{
|
||||
var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos));
|
||||
pos += 4;
|
||||
if (cliloc == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos));
|
||||
pos += 2;
|
||||
map[cliloc] = Encoding.Unicode.GetString(buffer, pos, byteLen);
|
||||
pos += byteLen;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitsMageArmorAndSelfRepair_DoesNotReadLowerStatReqOrDurabilityFromContainer()
|
||||
{
|
||||
var attrs = new AosArmorAttributes(null)
|
||||
{
|
||||
MageArmor = 1,
|
||||
SelfRepair = 4,
|
||||
LowerStatReq = 50, // container value must NOT be auto-emitted (it's passed in by the consumer)
|
||||
DurabilityBonus = 10 // never emitted by this method
|
||||
};
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl); // no lowerStatReq arg
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("", map[1060437]); // MageArmor (no-arg)
|
||||
Assert.Equal("4", map[1060450]); // SelfRepair
|
||||
Assert.False(map.ContainsKey(1060435)); // LowerStatReq NOT read from container
|
||||
Assert.False(map.ContainsKey(1060410)); // DurabilityBonus excluded
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitsLowerStatReqWhenPassed()
|
||||
{
|
||||
var attrs = new AosArmorAttributes(null) { MageArmor = 1, LowerStatReq = 50 };
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl); // computed value passed by the consumer, not the raw 50
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("77", map[1060435]); // emitted from the param, not the container's 50
|
||||
Assert.Equal("", map[1060437]); // MageArmor still emitted
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class AosAttributesPropertiesTests
|
||||
{
|
||||
private static Dictionary<int, string> Decode(ObjectPropertyList opl)
|
||||
{
|
||||
opl.Terminate();
|
||||
var buffer = opl.Buffer;
|
||||
var map = new Dictionary<int, string>();
|
||||
var pos = 15;
|
||||
while (true)
|
||||
{
|
||||
var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos));
|
||||
pos += 4;
|
||||
if (cliloc == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos));
|
||||
pos += 2;
|
||||
map[cliloc] = Encoding.Unicode.GetString(buffer, pos, byteLen);
|
||||
pos += byteLen;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitsRawAttributesInCanonicalOrder()
|
||||
{
|
||||
var attrs = new AosAttributes(null)
|
||||
{
|
||||
DefendChance = 5,
|
||||
BonusStr = 10,
|
||||
NightSight = 1,
|
||||
SpellChanneling = 1,
|
||||
WeaponSpeed = 7
|
||||
};
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl);
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("5", map[1060408]); // DefendChance
|
||||
Assert.Equal("10", map[1060485]); // BonusStr
|
||||
Assert.Equal("", map[1060441]); // NightSight (no-arg)
|
||||
Assert.Equal("", map[1060482]); // SpellChanneling (no-arg)
|
||||
Assert.Equal("7", map[1060486]); // WeaponSpeed
|
||||
Assert.False(map.ContainsKey(1060401)); // WeaponDamage not set
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppliesComputedBonuses()
|
||||
{
|
||||
var attrs = new AosAttributes(null) { WeaponDamage = 10, AttackChance = 4, Luck = 100 };
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl, damageBonus: 5, hitChanceBonus: 3, luckBonus: 50);
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("15", map[1060401]); // WeaponDamage + damageBonus
|
||||
Assert.Equal("7", map[1060415]); // AttackChance + hitChanceBonus
|
||||
Assert.Equal("150", map[1060436]); // Luck + luckBonus
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class AosWeaponAttributesPropertiesTests
|
||||
{
|
||||
private static Dictionary<int, string> Decode(ObjectPropertyList opl)
|
||||
{
|
||||
opl.Terminate();
|
||||
var buffer = opl.Buffer;
|
||||
var map = new Dictionary<int, string>();
|
||||
var pos = 15;
|
||||
while (true)
|
||||
{
|
||||
var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos));
|
||||
pos += 4;
|
||||
if (cliloc == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos));
|
||||
pos += 2;
|
||||
map[cliloc] = Encoding.Unicode.GetString(buffer, pos, byteLen);
|
||||
pos += byteLen;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitsHitEffectsUseBestSkillMageWeaponSelfRepair()
|
||||
{
|
||||
var attrs = new AosWeaponAttributes(null)
|
||||
{
|
||||
UseBestSkill = 1,
|
||||
HitFireball = 12,
|
||||
HitLeechHits = 20,
|
||||
MageWeapon = 25,
|
||||
SelfRepair = 3
|
||||
};
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl);
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("", map[1060400]); // UseBestSkill (no-arg)
|
||||
Assert.Equal("12", map[1060420]); // HitFireball
|
||||
Assert.Equal("20", map[1060422]); // HitLeechHits
|
||||
Assert.Equal("5", map[1060438]); // MageWeapon => 30 - 25
|
||||
Assert.Equal("3", map[1060450]); // SelfRepair
|
||||
Assert.False(map.ContainsKey(1060435)); // LowerStatReq not emitted without the param
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmitsLowerStatReqWhenPassed()
|
||||
{
|
||||
var attrs = new AosWeaponAttributes(null) { MageWeapon = 25 };
|
||||
|
||||
var opl = new ObjectPropertyList(null);
|
||||
attrs.GetProperties(opl); // computed value passed by the weapon
|
||||
var map = Decode(opl);
|
||||
|
||||
Assert.Equal("40", map[1060435]); // lower requirements, in cliloc order before MageWeapon
|
||||
Assert.Equal("5", map[1060438]); // MageWeapon still emitted
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class BaseArmorPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Armor_AttributeLineSet_Preserved()
|
||||
{
|
||||
var armor = new PlateChest();
|
||||
try
|
||||
{
|
||||
armor.Attributes.DefendChance = 5;
|
||||
armor.Attributes.BonusDex = 8;
|
||||
armor.Attributes.Luck = 40;
|
||||
armor.Attributes.SpellChanneling = 1;
|
||||
armor.Attributes.IncreasedKarmaLoss = 2;
|
||||
armor.ArmorAttributes.MageArmor = 1;
|
||||
armor.ArmorAttributes.SelfRepair = 3;
|
||||
armor.ArmorAttributes.LowerStatReq = 50;
|
||||
|
||||
var map = ItemOplTestHelper.DecodeAttributeLines(armor);
|
||||
|
||||
Assert.Equal("5", map[1060408]); // DefendChance
|
||||
Assert.Equal("8", map[1060409]); // BonusDex
|
||||
Assert.Equal("40", map[1060436]); // Luck (GetLuckBonus()==0 unequipped)
|
||||
Assert.Equal("", map[1060482]); // SpellChanneling
|
||||
Assert.Equal("2", map[1075210]); // IncreasedKarmaLoss
|
||||
Assert.Equal("", map[1060437]); // MageArmor
|
||||
Assert.Equal("3", map[1060450]); // SelfRepair
|
||||
Assert.Equal("50", map[1060435]); // LowerStatReq (inline via GetLowerStatReq)
|
||||
}
|
||||
finally
|
||||
{
|
||||
armor.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class BaseClothingPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Clothing_AttributeLineSet_Preserved()
|
||||
{
|
||||
var shirt = new FancyShirt();
|
||||
try
|
||||
{
|
||||
shirt.Attributes.DefendChance = 5;
|
||||
shirt.Attributes.Luck = 25;
|
||||
shirt.Attributes.SpellChanneling = 1;
|
||||
shirt.ClothingAttributes.MageArmor = 1;
|
||||
shirt.ClothingAttributes.SelfRepair = 2;
|
||||
shirt.ClothingAttributes.LowerStatReq = 30;
|
||||
shirt.ClothingAttributes.DurabilityBonus = 15;
|
||||
|
||||
var map = ItemOplTestHelper.DecodeAttributeLines(shirt);
|
||||
|
||||
Assert.Equal("5", map[1060408]); // DefendChance
|
||||
Assert.Equal("25", map[1060436]); // Luck (raw)
|
||||
Assert.Equal("", map[1060482]); // SpellChanneling
|
||||
Assert.Equal("", map[1060437]); // MageArmor
|
||||
Assert.Equal("2", map[1060450]); // SelfRepair
|
||||
Assert.Equal("30", map[1060435]); // LowerStatReq (direct)
|
||||
Assert.Equal("15", map[1060410]); // DurabilityBonus (direct)
|
||||
}
|
||||
finally
|
||||
{
|
||||
shirt.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class BaseJewelPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Jewel_AttributeLineSet_Preserved()
|
||||
{
|
||||
var ring = new GoldRing();
|
||||
try
|
||||
{
|
||||
ring.Attributes.DefendChance = 5;
|
||||
ring.Attributes.BonusStr = 10;
|
||||
ring.Attributes.Luck = 100;
|
||||
ring.Attributes.NightSight = 1;
|
||||
ring.Attributes.SpellChanneling = 1;
|
||||
ring.Attributes.IncreasedKarmaLoss = 3;
|
||||
|
||||
var map = ItemOplTestHelper.DecodeAttributeLines(ring);
|
||||
|
||||
Assert.Equal("5", map[1060408]); // DefendChance
|
||||
Assert.Equal("10", map[1060485]); // BonusStr
|
||||
Assert.Equal("100", map[1060436]); // Luck (raw; jewel has no luck bonus)
|
||||
Assert.Equal("", map[1060441]); // NightSight
|
||||
Assert.Equal("", map[1060482]); // SpellChanneling
|
||||
Assert.Equal("3", map[1075210]); // IncreasedKarmaLoss (Core.ML EJ)
|
||||
}
|
||||
finally
|
||||
{
|
||||
ring.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class BaseTalismanPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetProperties_EmitsAosAttributeLines()
|
||||
{
|
||||
var item = new RandomTalisman();
|
||||
try
|
||||
{
|
||||
item.Attributes.DefendChance = 10;
|
||||
item.Attributes.BonusStr = 5;
|
||||
item.Attributes.Luck = 50;
|
||||
item.Attributes.NightSight = 1;
|
||||
item.Attributes.SpellChanneling = 1;
|
||||
item.Attributes.IncreasedKarmaLoss = 2;
|
||||
|
||||
var lines = ItemOplTestHelper.DecodeAttributeLines(item);
|
||||
|
||||
Assert.True(lines.ContainsKey(1060408)); // DefendChance
|
||||
Assert.Equal("10", lines[1060408]);
|
||||
Assert.True(lines.ContainsKey(1060485)); // BonusStr
|
||||
Assert.Equal("5", lines[1060485]);
|
||||
Assert.True(lines.ContainsKey(1060436)); // Luck
|
||||
Assert.Equal("50", lines[1060436]);
|
||||
Assert.True(lines.ContainsKey(1060441)); // NightSight
|
||||
Assert.Equal("", lines[1060441]);
|
||||
Assert.True(lines.ContainsKey(1060482)); // SpellChanneling
|
||||
Assert.Equal("", lines[1060482]);
|
||||
Assert.True(lines.ContainsKey(1075210)); // IncreasedKarmaLoss
|
||||
Assert.Equal("2", lines[1075210]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
item.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class BaseWeaponPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Weapon_AttributeLineSet_Preserved()
|
||||
{
|
||||
var weapon = new Longsword();
|
||||
try
|
||||
{
|
||||
weapon.Attributes.DefendChance = 5;
|
||||
weapon.Attributes.BonusInt = 6;
|
||||
weapon.Attributes.SpellChanneling = 1;
|
||||
weapon.WeaponAttributes.UseBestSkill = 1;
|
||||
weapon.WeaponAttributes.HitFireball = 12;
|
||||
weapon.WeaponAttributes.MageWeapon = 25;
|
||||
weapon.WeaponAttributes.SelfRepair = 3;
|
||||
|
||||
var map = ItemOplTestHelper.DecodeAttributeLines(weapon);
|
||||
|
||||
Assert.Equal("5", map[1060408]); // DefendChance
|
||||
Assert.Equal("6", map[1060432]); // BonusInt
|
||||
Assert.Equal("", map[1060482]); // SpellChanneling
|
||||
Assert.Equal("", map[1060400]); // UseBestSkill
|
||||
Assert.Equal("12", map[1060420]); // HitFireball
|
||||
Assert.Equal("5", map[1060438]); // MageWeapon (30 - 25)
|
||||
Assert.Equal("3", map[1060450]); // SelfRepair
|
||||
}
|
||||
finally
|
||||
{
|
||||
weapon.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Server;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
public static class ItemOplTestHelper
|
||||
{
|
||||
// Builds the item's OPL and returns attribute/property cliloc lines (>= 1060000),
|
||||
// ignoring base-item lines (name, weight, etc.) so tests isolate the attribute surface.
|
||||
public static Dictionary<int, string> DecodeAttributeLines(Item item)
|
||||
{
|
||||
var opl = new ObjectPropertyList(item);
|
||||
item.GetProperties(opl);
|
||||
opl.Terminate();
|
||||
|
||||
var buffer = opl.Buffer;
|
||||
var map = new Dictionary<int, string>();
|
||||
var pos = 15;
|
||||
while (true)
|
||||
{
|
||||
var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos));
|
||||
pos += 4;
|
||||
if (cliloc == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos));
|
||||
pos += 2;
|
||||
var arg = Encoding.Unicode.GetString(buffer, pos, byteLen);
|
||||
pos += byteLen;
|
||||
|
||||
if (cliloc is >= 1060000 and < 1080000)
|
||||
{
|
||||
map[cliloc] = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpellbookPropertiesTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetProperties_EmitsAosAttributeLines()
|
||||
{
|
||||
var item = new Spellbook();
|
||||
try
|
||||
{
|
||||
item.Attributes.CastRecovery = 3;
|
||||
item.Attributes.LowerManaCost = 8;
|
||||
item.Attributes.Luck = 75;
|
||||
item.Attributes.NightSight = 1;
|
||||
item.Attributes.SpellChanneling = 1;
|
||||
item.Attributes.IncreasedKarmaLoss = 2;
|
||||
|
||||
var lines = ItemOplTestHelper.DecodeAttributeLines(item);
|
||||
|
||||
Assert.True(lines.ContainsKey(1060412)); // CastRecovery
|
||||
Assert.Equal("3", lines[1060412]);
|
||||
Assert.True(lines.ContainsKey(1060433)); // LowerManaCost
|
||||
Assert.Equal("8", lines[1060433]);
|
||||
Assert.True(lines.ContainsKey(1060436)); // Luck
|
||||
Assert.Equal("75", lines[1060436]);
|
||||
Assert.True(lines.ContainsKey(1060441)); // NightSight
|
||||
Assert.Equal("", lines[1060441]);
|
||||
Assert.True(lines.ContainsKey(1060482)); // SpellChanneling
|
||||
Assert.Equal("", lines[1060482]);
|
||||
Assert.True(lines.ContainsKey(1075210)); // IncreasedKarmaLoss
|
||||
Assert.Equal("2", lines[1075210]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
item.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1010,8 +1010,6 @@ namespace Server
|
|||
return value;
|
||||
}
|
||||
|
||||
// lowerStatReq is passed in because the weapon folds the resource's lower-requirements into
|
||||
// GetLowerStatReq(); emitted in cliloc order (1060435) between the Hit* block and MageWeapon.
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
int prop;
|
||||
|
|
@ -1200,9 +1198,6 @@ namespace Server
|
|||
return value;
|
||||
}
|
||||
|
||||
// lowerStatReq is passed in because consumers compute it differently: armor folds in the
|
||||
// resource's ArmorLowerRequirements via GetLowerStatReq(), clothing reads it raw. Emitted in
|
||||
// cliloc order (1060435) ahead of MageArmor/SelfRepair.
|
||||
public void GetProperties(IPropertyList list)
|
||||
{
|
||||
int prop;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue