diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 105899543..d61839a1c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -52,7 +52,12 @@ jobs: - name: Migration Changes run: git diff --exit-code ./**/Migrations/*.v*.json - name: Test - run: dotnet test --no-restore + run: | + dotnet test --logger trx --results-directory ./TestResults + if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then + echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures." + exit 1 + fi build-linux: runs-on: ubuntu-latest @@ -106,4 +111,9 @@ jobs: - name: Build run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs - name: Test - run: dotnet test --no-restore + run: | + dotnet test --logger trx --results-directory ./TestResults + if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then + echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures." + exit 1 + fi diff --git a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs index 33bdb3634..076f88141 100644 --- a/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs +++ b/Projects/UOContent.Tests/Fixtures/TestServerInitializer.cs @@ -30,6 +30,13 @@ internal static class TestServerInitializer private static bool _initialized; private static readonly Lock _lock = new(); + /// + /// 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 Skip.If(!TestServerInitializer.TileDataLoaded, ...). + /// + 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() diff --git a/Projects/UOContent.Tests/Fixtures/TileDataRequirement.cs b/Projects/UOContent.Tests/Fixtures/TileDataRequirement.cs new file mode 100644 index 000000000..7b9af1f27 --- /dev/null +++ b/Projects/UOContent.Tests/Fixtures/TileDataRequirement.cs @@ -0,0 +1,18 @@ +using Xunit; + +namespace Server.Tests; + +/// +/// Shared guard for tests that require the copyrighted UO client tile/map data (tiledata.mul and +/// friends), which is absent on CI. Call as the first statement of a +/// [SkippableFact]/[SkippableTheory] so the test is skipped — not failed — when the +/// data was not loaded. See . +/// +internal static class TileDataRequirement +{ + public static void SkipIfMissing() => + Skip.If( + !TestServerInitializer.TileDataLoaded, + "Requires UO client tile data (tiledata.mul); absent on CI." + ); +} diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs index ab9497729..20d729c0f 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/BitmapAStarAlgorithmTests.cs @@ -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); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/BoatPathTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/BoatPathTests.cs index a9796de7f..694374757 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/BoatPathTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/BoatPathTests.cs @@ -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]; diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/FoundationRedesignTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/FoundationRedesignTests.cs index 3fdb4fa3e..e82520ce9 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/FoundationRedesignTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/FoundationRedesignTests.cs @@ -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); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs index 41caa6255..8ecbd0591 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/HousePathRoutingTests.cs @@ -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); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiEdgeCaseTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiEdgeCaseTests.cs index eb12df7d6..8aa0b8d19 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiEdgeCaseTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiEdgeCaseTests.cs @@ -25,9 +25,10 @@ public class MultiEdgeCaseTests /// GetStaticAndMultiTiles yields tiles from BOTH multis; the synthesizer must still /// agree with CheckMovement everywhere over the union footprint + halo. /// - [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). /// - [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 Components — i.e. it matches CheckMovement on the NEW shape. /// - [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.) /// - [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]; diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs index 8a544897c..7487ce8de 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskCacheTests.cs @@ -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; diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskSynthesisTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskSynthesisTests.cs index 923784f4f..b415ec3d6 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskSynthesisTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiMaskSynthesisTests.cs @@ -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 _); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs index 4775b8fac..1103cdb83 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiSplitRoutingTests.cs @@ -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); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs index 59e583ac6..d8d46e7f8 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/Multi/MultiWalkabilityTests.cs @@ -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 _); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs index 0d865d08d..1ef3aa8ab 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheFileTests.cs @@ -347,9 +347,10 @@ public class StepCacheFileTests /// to which proves the /// default lazy behavior. /// - [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 diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs index 244c51534..436f78c7e 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheLifecycleTests.cs @@ -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 diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs index 149a9fe13..0dc1eb12f 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepCacheParityTests.cs @@ -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. /// - [Fact] + [SkippableFact] public void SwimBake_ProducesWetCells_OnKnownWaterRegion() { + TileDataRequirement.SkipIfMissing(); var map = Map.Maps[1]; Assert.NotNull(map); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs index df8ff5da5..429d60ca7 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeParityTests.cs @@ -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); diff --git a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeTests.cs b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeTests.cs index cfca235da..9a32d66bf 100644 --- a/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeTests.cs +++ b/Projects/UOContent.Tests/Tests/Engines/Pathing/StepProbeTests.cs @@ -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. diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs index fa3eb23bd..923f4aff9 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/ApproachTargetTests.cs @@ -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 _); diff --git a/Projects/UOContent.Tests/Tests/PropertyList/AosArmorAttributesPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/AosArmorAttributesPropertiesTests.cs deleted file mode 100644 index 760bec89e..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/AosArmorAttributesPropertiesTests.cs +++ /dev/null @@ -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 Decode(ObjectPropertyList opl) - { - opl.Terminate(); - var buffer = opl.Buffer; - var map = new Dictionary(); - 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 - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/AosAttributesPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/AosAttributesPropertiesTests.cs deleted file mode 100644 index 6571c1312..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/AosAttributesPropertiesTests.cs +++ /dev/null @@ -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 Decode(ObjectPropertyList opl) - { - opl.Terminate(); - var buffer = opl.Buffer; - var map = new Dictionary(); - 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 - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/AosWeaponAttributesPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/AosWeaponAttributesPropertiesTests.cs deleted file mode 100644 index f9f3ecdc9..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/AosWeaponAttributesPropertiesTests.cs +++ /dev/null @@ -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 Decode(ObjectPropertyList opl) - { - opl.Terminate(); - var buffer = opl.Buffer; - var map = new Dictionary(); - 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 - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/BaseArmorPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/BaseArmorPropertiesTests.cs deleted file mode 100644 index 5176e6db6..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/BaseArmorPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/BaseClothingPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/BaseClothingPropertiesTests.cs deleted file mode 100644 index d7d51c3f3..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/BaseClothingPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/BaseJewelPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/BaseJewelPropertiesTests.cs deleted file mode 100644 index 30acb6cf1..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/BaseJewelPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/BaseTalismanPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/BaseTalismanPropertiesTests.cs deleted file mode 100644 index d42684658..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/BaseTalismanPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/BaseWeaponPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/BaseWeaponPropertiesTests.cs deleted file mode 100644 index 213e59cb9..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/BaseWeaponPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/ItemOplTestHelper.cs b/Projects/UOContent.Tests/Tests/PropertyList/ItemOplTestHelper.cs deleted file mode 100644 index 1b4acf26b..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/ItemOplTestHelper.cs +++ /dev/null @@ -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 DecodeAttributeLines(Item item) - { - var opl = new ObjectPropertyList(item); - item.GetProperties(opl); - opl.Terminate(); - - var buffer = opl.Buffer; - var map = new Dictionary(); - 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; - } -} diff --git a/Projects/UOContent.Tests/Tests/PropertyList/SpellbookPropertiesTests.cs b/Projects/UOContent.Tests/Tests/PropertyList/SpellbookPropertiesTests.cs deleted file mode 100644 index 4f14d1f71..000000000 --- a/Projects/UOContent.Tests/Tests/PropertyList/SpellbookPropertiesTests.cs +++ /dev/null @@ -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(); - } - } -} diff --git a/Projects/UOContent/Misc/AOS.cs b/Projects/UOContent/Misc/AOS.cs index 9ac9158c4..6a9ec278c 100644 --- a/Projects/UOContent/Misc/AOS.cs +++ b/Projects/UOContent/Misc/AOS.cs @@ -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;