diff --git a/TechbloxModdingAPI/App/Game.cs b/TechbloxModdingAPI/App/Game.cs index 252171d..8866165 100644 --- a/TechbloxModdingAPI/App/Game.cs +++ b/TechbloxModdingAPI/App/Game.cs @@ -445,7 +445,7 @@ namespace TechbloxModdingAPI.App Block[] blocks = new Block[blockEGIDs.Length]; for (int b = 0; b < blockEGIDs.Length; b++) { - blocks[b] = new Block(blockEGIDs[b]); + blocks[b] = Block.New(blockEGIDs[b]); } return blocks; } diff --git a/TechbloxModdingAPI/Block.cs b/TechbloxModdingAPI/Block.cs index ca66f3a..bab3291 100644 --- a/TechbloxModdingAPI/Block.cs +++ b/TechbloxModdingAPI/Block.cs @@ -11,6 +11,7 @@ using Unity.Mathematics; using Gamecraft.Blocks.GUI; using TechbloxModdingAPI.Blocks; +using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Utility; namespace TechbloxModdingAPI @@ -101,10 +102,9 @@ namespace TechbloxModdingAPI {CommonExclusiveGroups.DAMPEDSPRING_BLOCK_GROUP, id => new DampedSpring(id)}, {CommonExclusiveGroups.TEXT_BLOCK_GROUP, id => new TextBlock(id)}, {CommonExclusiveGroups.TIMER_BLOCK_GROUP, id => new Timer(id)} - };/*.SelectMany(kv => kv.Value.Select(v => (Key: v, Value: kv.Key))) - .ToDictionary(kv => kv.Key, kv => kv.Value);*/ + }; - private static Block New(EGID egid) + internal static Block New(EGID egid) { return GroupToConstructor.ContainsKey(egid.groupID) ? GroupToConstructor[egid.groupID](egid) @@ -299,6 +299,7 @@ namespace TechbloxModdingAPI /// The text displayed on the block if applicable, or null. /// Setting it is temporary to the session, it won't be saved. /// + [TestValue(null)] public string Label { get => BlockEngine.GetBlockInfoViewComponent(this).textLabelComponent?.text; diff --git a/TechbloxModdingAPI/Blocks/BlockEngine.cs b/TechbloxModdingAPI/Blocks/BlockEngine.cs index b06194f..b8eece6 100644 --- a/TechbloxModdingAPI/Blocks/BlockEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlockEngine.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -60,7 +61,7 @@ namespace TechbloxModdingAPI.Blocks var ret = new Block[cubes.count]; for (int i = 0; i < cubes.count; i++) - ret[i] = new Block(cubes[i]); + ret[i] = Block.New(cubes[i]); return ret; } @@ -103,7 +104,14 @@ namespace TechbloxModdingAPI.Blocks var prefabAssetIDOpt = entitiesDB.QueryEntityOptional(block); uint prefabAssetID = prefabAssetIDOpt ? prefabAssetIDOpt.Get().prefabAssetID - : throw new BlockException("Prefab asset ID not found!"); //Set by the game + : uint.MaxValue; + if (prefabAssetID == uint.MaxValue) + { + if (entitiesDB.QueryEntityOptional(block)) //The block exists + throw new BlockException("Prefab asset ID not found for block " + block); //Set by the game + return; + } + uint prefabId = PrefabsID.GetOrCreatePrefabID((ushort) prefabAssetID, material, 1, flipped); entitiesDB.QueryEntityOrDefault(block).prefabID = prefabId; @@ -239,7 +247,7 @@ namespace TechbloxModdingAPI.Blocks { var conn = array[index]; if (conn.machineRigidBodyId == sbid) - set.Add(new Block(conn.ID)); + set.Add(Block.New(conn.ID)); } } diff --git a/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs b/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs index 3b77075..a1ecd7f 100644 --- a/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlockEventsEngine.cs @@ -47,9 +47,8 @@ namespace TechbloxModdingAPI.Blocks public struct BlockPlacedRemovedEventArgs { public EGID ID; - public BlockIDs Type; private Block block; - public Block Block => block ?? (block = new Block(ID)); + public Block Block => block ?? (block = Block.New(ID)); } } \ No newline at end of file diff --git a/TechbloxModdingAPI/Blocks/BlockTests.cs b/TechbloxModdingAPI/Blocks/BlockTests.cs index 5cd32d6..f69315e 100644 --- a/TechbloxModdingAPI/Blocks/BlockTests.cs +++ b/TechbloxModdingAPI/Blocks/BlockTests.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; -using Gamecraft.Wires; +using DataLoader; +using Svelto.Tasks; using Unity.Mathematics; -using TechbloxModdingAPI; +using TechbloxModdingAPI.App; using TechbloxModdingAPI.Tests; using TechbloxModdingAPI.Utility; @@ -19,62 +23,116 @@ namespace TechbloxModdingAPI.Blocks [APITestCase(TestType.Game)] //At least one block must be placed for simulation to work public static void TestPlaceNew() { - Block newBlock = Block.PlaceNew(BlockIDs.Cube, Unity.Mathematics.float3.zero); + Block newBlock = Block.PlaceNew(BlockIDs.Cube, float3.zero); Assert.NotNull(newBlock.Id, "Newly placed block is missing Id. This should be populated when the block is placed.", "Newly placed block Id is not null, block successfully placed."); } [APITestCase(TestType.EditMode)] public static void TestInitProperty() { - Block newBlock = Block.PlaceNew(BlockIDs.Cube, Unity.Mathematics.float3.zero + 2); - if (!Assert.CloseTo(newBlock.Position, (Unity.Mathematics.float3.zero + 2), $"Newly placed block at {newBlock.Position} is expected at {Unity.Mathematics.float3.zero + 2}.", "Newly placed block position matches.")) return; + Block newBlock = Block.PlaceNew(BlockIDs.Cube, float3.zero + 2); + if (!Assert.CloseTo(newBlock.Position, (float3.zero + 2), $"Newly placed block at {newBlock.Position} is expected at {Unity.Mathematics.float3.zero + 2}.", "Newly placed block position matches.")) return; //Assert.Equal(newBlock.Exists, true, "Newly placed block does not exist, possibly because Sync() skipped/missed/failed.", "Newly placed block exists, Sync() successful."); } - /*[APITestCase(TestType.EditMode)] - public static void TestTextBlock() - { - TextBlock textBlock = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler - Assert.Errorless(() => { textBlock = Block.PlaceNew(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew() raised an exception: ", "Block.PlaceNew() completed without issue."); - if (!Assert.NotNull(textBlock, "Block.PlaceNew() returned null, possibly because it failed silently.", "Specialized TextBlock is not null.")) return; - if (!Assert.NotNull(textBlock.Text, "TextBlock.Text is null, possibly because it failed silently.", "TextBlock.Text is not null.")) return; - if (!Assert.NotNull(textBlock.TextBlockId, "TextBlock.TextBlockId is null, possibly because it failed silently.", "TextBlock.TextBlockId is not null.")) return; - } - - [APITestCase(TestType.EditMode)] - public static void TestMotor() + [APITestCase(TestType.EditMode)] + public static void TestBlockIDCoverage() { - Block newBlock = Block.PlaceNew(BlockIDs.MotorS, Unity.Mathematics.float3.zero + 1); - Motor b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler - Assert.Errorless(() => { b = newBlock.Specialise(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() returned null, possibly because it failed silently.", "Specialized Motor is not null.")) return; - if (!Assert.CloseTo(b.Torque, 75f, $"Motor.Torque {b.Torque} does not equal default value, possibly because it failed silently.", "Motor.Torque close enough to default.")) return; - if (!Assert.CloseTo(b.TopSpeed, 30f, $"Motor.TopSpeed {b.TopSpeed} does not equal default value, possibly because it failed silently.", "Motor.Torque is close enough to default.")) return; - if (!Assert.Equal(b.Reverse, false, $"Motor.Reverse {b.Reverse} does not equal default value, possibly because it failed silently.", "Motor.Reverse is default.")) return; + Assert.Equal( + FullGameFields._dataDb.GetValues().Keys.Select(ushort.Parse).OrderBy(id => id) + .SequenceEqual(Enum.GetValues(typeof(BlockIDs)).Cast().OrderBy(id => id) + .Except(new[] {(ushort) BlockIDs.Invalid})), true, + "Block ID enum is different than the known block types, update needed.", + "Block ID enum matches the known block types."); } - [APITestCase(TestType.EditMode)] - public static void TestPiston() + [APITestCase(TestType.EditMode)] + public static void TestBlockIDs() { - Block newBlock = Block.PlaceNew(BlockIDs.PneumaticPiston, Unity.Mathematics.float3.zero + 1); - Piston b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler - Assert.Errorless(() => { b = newBlock.Specialise(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() returned null, possibly because it failed silently.", "Specialized Piston is not null.")) return; - if (!Assert.CloseTo(b.MaximumExtension, 1.01f, $"Piston.MaximumExtension {b.MaximumExtension} does not equal default value, possibly because it failed silently.", "Piston.MaximumExtension is close enough to default.")) return; - if (!Assert.CloseTo(b.MaximumForce, 1.0f, $"Piston.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Piston.MaximumForce is close enough to default.")) return; + float3 pos = new float3(); + foreach (BlockIDs id in Enum.GetValues(typeof(BlockIDs))) + { + if (id == BlockIDs.Invalid) continue; + try + { + Block.PlaceNew(id, pos); + pos += 0.2f; + } + catch (Exception e) + { //Only print failed case + Assert.Fail($"Failed to place block type {id}: {e}"); + return; + } + } + + Assert.Pass("Placing all possible block types succeeded."); } [APITestCase(TestType.EditMode)] - public static void TestServo() - { - Block newBlock = Block.PlaceNew(BlockIDs.ServoAxle, Unity.Mathematics.float3.zero + 1); - Servo b = null; // Note: the assignment operation is a lambda, which slightly confuses the compiler - Assert.Errorless(() => { b = newBlock.Specialise(); }, "Block.Specialize() raised an exception: ", "Block.Specialize() completed without issue."); - if (!Assert.NotNull(b, "Block.Specialize() returned null, possibly because it failed silently.", "Specialized Servo is not null.")) return; - if (!Assert.CloseTo(b.MaximumAngle, 180f, $"Servo.MaximumAngle {b.MaximumAngle} does not equal default value, possibly because it failed silently.", "Servo.MaximumAngle is close enough to default.")) return; - if (!Assert.CloseTo(b.MinimumAngle, -180f, $"Servo.MinimumAngle {b.MinimumAngle} does not equal default value, possibly because it failed silently.", "Servo.MinimumAngle is close enough to default.")) return; - if (!Assert.CloseTo(b.MaximumForce, 60f, $"Servo.MaximumForce {b.MaximumForce} does not equal default value, possibly because it failed silently.", "Servo.MaximumForce is close enough to default.")) return; - }*/ + public static IEnumerator TestBlockProperties() + { //Uses the result of the previous test case + var blocks = Game.CurrentGame().GetBlocksInGame(); + for (var index = 0; index < blocks.Length; index++) + { + if (index % 50 == 0) yield return Yield.It; //The material or flipped status can only be changed 130 times per submission + var block = blocks[index]; + if (!block.Exists) continue; + foreach (var property in block.GetType().GetProperties()) + { + //Includes specialised block properties + if (property.SetMethod == null) continue; + var testValues = new (Type, object, Predicate)[] + { + //(type, default value, predicate or null for equality) + (typeof(long), 3, null), + (typeof(int), 4, null), + (typeof(double), 5.2f, obj => Math.Abs((double) obj - 5.2f) < float.Epsilon), + (typeof(float), 5.2f, obj => Math.Abs((float) obj - 5.2f) < float.Epsilon), + (typeof(bool), true, obj => (bool) obj), + (typeof(string), "Test", obj => (string) obj == "Test"), //String equality check + (typeof(float3), (float3) 2, obj => math.all((float3) obj - 2 < (float3) float.Epsilon)), + (typeof(BlockColor), new BlockColor(BlockColors.Aqua, 2), null), + (typeof(float4), (float4) 5, obj => math.all((float4) obj - 5 < (float4) float.Epsilon)) + }; + var propType = property.PropertyType; + if (!propType.IsValueType) continue; + (object valueToUse, Predicate predicateToUse) = (null, null); + foreach (var (type, value, predicate) in testValues) + { + if (type.IsAssignableFrom(propType)) + { + valueToUse = value; + predicateToUse = predicate ?? (obj => Equals(obj, value)); + break; + } + } + + if (propType.IsEnum) + { + var values = propType.GetEnumValues(); + valueToUse = values.GetValue(values.Length / 2); + predicateToUse = val => Equals(val, valueToUse); + } + + if (valueToUse == null) + { + Assert.Fail($"Property {block.GetType().Name}.{property.Name} has an unknown type {propType}, test needs fixing."); + yield break; + } + + property.SetValue(block, valueToUse); + object got = property.GetValue(block); + var attr = property.GetCustomAttribute(); + if (!predicateToUse(got) && (attr == null || !Equals(attr.PossibleValue, got))) + { + Assert.Fail($"Property {block.GetType().Name}.{property.Name} value {got} does not equal {valueToUse} for block {block}."); + yield break; + } + } + } + + Assert.Pass("Setting all possible properties of all registered API block types succeeded."); + } [APITestCase(TestType.EditMode)] public static void TestDampedSpring() diff --git a/TechbloxModdingAPI/Blocks/BlueprintEngine.cs b/TechbloxModdingAPI/Blocks/BlueprintEngine.cs index a6f65f3..eece947 100644 --- a/TechbloxModdingAPI/Blocks/BlueprintEngine.cs +++ b/TechbloxModdingAPI/Blocks/BlueprintEngine.cs @@ -75,7 +75,7 @@ namespace TechbloxModdingAPI.Blocks int count = selectedBlocksInGroup.Count(); var ret = new Block[count]; for (uint i = 0; i < count; i++) - ret[i] = new Block(selectedBlocksInGroup.Get(i)); + ret[i] = Block.New(selectedBlocksInGroup.Get(i)); selectedBlocksInGroup.FastClear(); return ret; } @@ -222,7 +222,7 @@ namespace TechbloxModdingAPI.Blocks new object[] {playerID, blueprintData, entitySerialization, entitiesDB, entityFactory}); var blocks = new Block[placedBlocks.count]; for (int i = 0; i < blocks.Length; i++) - blocks[i] = new Block(placedBlocks[i]); + blocks[i] = Block.New(placedBlocks[i]); return blocks; } diff --git a/TechbloxModdingAPI/Blocks/Wire.cs b/TechbloxModdingAPI/Blocks/Wire.cs index 289dee7..6cbfdb9 100644 --- a/TechbloxModdingAPI/Blocks/Wire.cs +++ b/TechbloxModdingAPI/Blocks/Wire.cs @@ -48,7 +48,7 @@ namespace TechbloxModdingAPI.Blocks WireEntityStruct wire = signalEngine.MatchPortToWire(port, end.Id, out exists); if (exists) { - return new Wire(new Block(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort); + return new Wire(Block.New(wire.sourceBlockEGID), end, wire.sourcePortUsage, endPort); } return null; } @@ -67,7 +67,7 @@ namespace TechbloxModdingAPI.Blocks WireEntityStruct wire = signalEngine.MatchPortToWire(port, start.Id, out exists); if (exists) { - return new Wire(start, new Block(wire.destinationBlockEGID), startPort, wire.destinationPortUsage); + return new Wire(start, Block.New(wire.destinationBlockEGID), startPort, wire.destinationPortUsage); } return null; } diff --git a/TechbloxModdingAPI/Player.cs b/TechbloxModdingAPI/Player.cs index 01b5c24..a5e5dc5 100644 --- a/TechbloxModdingAPI/Player.cs +++ b/TechbloxModdingAPI/Player.cs @@ -387,7 +387,7 @@ namespace TechbloxModdingAPI { var egid = playerEngine.GetThingLookedAt(Id, maxDistance); return egid != EGID.Empty && egid.groupID != CommonExclusiveGroups.SIMULATION_BODIES_GROUP - ? new Block(egid) + ? Block.New(egid) : null; } diff --git a/TechbloxModdingAPI/Players/PlayerEngine.cs b/TechbloxModdingAPI/Players/PlayerEngine.cs index f0c8a11..b4df628 100644 --- a/TechbloxModdingAPI/Players/PlayerEngine.cs +++ b/TechbloxModdingAPI/Players/PlayerEngine.cs @@ -469,7 +469,7 @@ namespace TechbloxModdingAPI.Players for (int j = 0; j < blocks.count; j++) { var egid = pointer[j]; - ret[j] = new Block(egid); + ret[j] = Block.New(egid); } return ret; diff --git a/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs b/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs index d9cfedb..6b78fa8 100644 --- a/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs +++ b/TechbloxModdingAPI/Tests/TechbloxModdingAPIPluginTest.cs @@ -324,9 +324,9 @@ namespace TechbloxModdingAPI.Tests GameClient.SetDebugInfo("InstalledMods", InstalledMods); Block.Placed += (sender, args) => - Logging.MetaDebugLog("Placed block " + args.Type + " with ID " + args.ID); + Logging.MetaDebugLog("Placed block " + args.Block); Block.Removed += (sender, args) => - Logging.MetaDebugLog("Removed block " + args.Type + " with ID " + args.ID); + Logging.MetaDebugLog("Removed block " + args.Block); /* CommandManager.AddCommand(new SimpleCustomCommandEngine((float d) => { UnityEngine.Camera.main.fieldOfView = d; }, diff --git a/TechbloxModdingAPI/Tests/TestRoot.cs b/TechbloxModdingAPI/Tests/TestRoot.cs index 9085470..2ac7c62 100644 --- a/TechbloxModdingAPI/Tests/TestRoot.cs +++ b/TechbloxModdingAPI/Tests/TestRoot.cs @@ -149,71 +149,59 @@ namespace TechbloxModdingAPI.Tests Game currentGame = Game.CurrentGame(); // in-game tests yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading - foreach (Type t in testTypes) - { - foreach (MethodBase m in t.GetMethods()) - { - APITestCaseAttribute a = m.GetCustomAttribute(); - if (a != null && a.TestType == TestType.Game) - { - try - { - m.Invoke(null, new object[0]); - } - catch (Exception e) - { - Assert.Fail($"Game test '{m}' raised an exception: {e.ToString()}"); - } - yield return Yield.It; - } - } - } - currentGame.ToggleTimeMode(); - yield return new WaitForSecondsEnumerator(5).Continue(); - // simulation tests - foreach (Type t in testTypes) - { - foreach (MethodBase m in t.GetMethods()) - { - APITestCaseAttribute a = m.GetCustomAttribute(); - if (a != null && a.TestType == TestType.SimulationMode) - { - try - { - m.Invoke(null, new object[0]); - } - catch (Exception e) - { - Assert.Fail($"Simulation test '{m}' raised an exception: {e.ToString()}"); - } - yield return Yield.It; - } - } - } - currentGame.ToggleTimeMode(); - yield return new WaitForSecondsEnumerator(5).Continue(); - // build tests - foreach (Type t in testTypes) - { - foreach (MethodBase m in t.GetMethods()) - { - APITestCaseAttribute a = m.GetCustomAttribute(); - if (a != null && a.TestType == TestType.EditMode) - { + var testTypesToRun = new[] + { + TestType.Game, + TestType.SimulationMode, + TestType.EditMode + }; + for (var index = 0; index < testTypesToRun.Length; index++) + { + foreach (Type t in testTypes) + { + foreach (MethodBase m in t.GetMethods()) + { + APITestCaseAttribute a = m.GetCustomAttribute(); + if (a == null || a.TestType != testTypesToRun[index]) continue; + + object ret = null; try - { - m.Invoke(null, new object[0]); - } - catch (Exception e) - { - Assert.Fail($"Build test '{m}' raised an exception: {e.ToString()}"); - } + { + ret = m.Invoke(null, new object[0]); + } + catch (Exception e) + { + Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}"); + } + + if (ret is IEnumerator enumerator) + { //Support enumerator methods with added exception handling + bool cont; + do + { //Can't use yield return in a try block... + try + { //And with Continue() exceptions aren't caught + cont = enumerator.MoveNext(); + } + catch (Exception e) + { + Assert.Fail($"{a.TestType} test '{m}' raised an exception: {e}"); + cont = false; + } + + yield return Yield.It; + } while (cont); + } + yield return Yield.It; - } - } - } + } + } + + if (index + 1 < testTypesToRun.Length) //Don't toggle on the last test + currentGame.ToggleTimeMode(); + yield return new WaitForSecondsEnumerator(5).Continue(); + } // exit game - yield return new WaitForSecondsEnumerator(5).Continue(); yield return ReturnToMenu().Continue(); } diff --git a/TechbloxModdingAPI/Tests/TestValueAttribute.cs b/TechbloxModdingAPI/Tests/TestValueAttribute.cs new file mode 100644 index 0000000..48794c9 --- /dev/null +++ b/TechbloxModdingAPI/Tests/TestValueAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace TechbloxModdingAPI.Tests +{ + [AttributeUsage(AttributeTargets.Property)] + public class TestValueAttribute : Attribute + { + public object PossibleValue { get; } + + /// + /// + /// When set, the property test accepts the specified value in addition to the test input.
+ /// Useful if setting the property isn't always possible. + /// + ///
+ public TestValueAttribute(object possibleValue) => PossibleValue = possibleValue; + } +} \ No newline at end of file