Fixed prefab update for nonexistent blocks Removed Type from block placed/removed event args Added test to check the block ID enum (whether it has any extra or missing IDs) Added test to place every block on the ID enum Added test to set and verify each property of each block type (type-specific properties are also set when they can be through the API) Added support for enumerator test methods with exception handlingtags/v2.0.0
@@ -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; | |||
} | |||
@@ -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. | |||
/// </summary> | |||
[TestValue(null)] | |||
public string Label | |||
{ | |||
get => BlockEngine.GetBlockInfoViewComponent<TextLabelEntityViewStruct>(this).textLabelComponent?.text; | |||
@@ -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<PrefabAssetIDComponent>(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<DBEntityStruct>(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<GFXPrefabEntityStructGPUI>(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)); | |||
} | |||
} | |||
@@ -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)); | |||
} | |||
} |
@@ -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<TextBlock>(BlockIDs.TextBlock, Unity.Mathematics.float3.zero + 1); }, "Block.PlaceNew<TextBlock>() raised an exception: ", "Block.PlaceNew<TextBlock>() completed without issue."); | |||
if (!Assert.NotNull(textBlock, "Block.PlaceNew<TextBlock>() 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<Motor>(); }, "Block.Specialize<Motor>() raised an exception: ", "Block.Specialize<Motor>() completed without issue."); | |||
if (!Assert.NotNull(b, "Block.Specialize<Motor>() 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<CubeListData>().Keys.Select(ushort.Parse).OrderBy(id => id) | |||
.SequenceEqual(Enum.GetValues(typeof(BlockIDs)).Cast<ushort>().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<Piston>(); }, "Block.Specialize<Piston>() raised an exception: ", "Block.Specialize<Piston>() completed without issue."); | |||
if (!Assert.NotNull(b, "Block.Specialize<Piston>() 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<Servo>(); }, "Block.Specialize<Servo>() raised an exception: ", "Block.Specialize<Servo>() completed without issue."); | |||
if (!Assert.NotNull(b, "Block.Specialize<Servo>() 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<TaskContract> 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<object>)[] | |||
{ | |||
//(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<object> 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<TestValueAttribute>(); | |||
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() | |||
@@ -75,7 +75,7 @@ namespace TechbloxModdingAPI.Blocks | |||
int count = selectedBlocksInGroup.Count<EGID>(); | |||
var ret = new Block[count]; | |||
for (uint i = 0; i < count; i++) | |||
ret[i] = new Block(selectedBlocksInGroup.Get<EGID>(i)); | |||
ret[i] = Block.New(selectedBlocksInGroup.Get<EGID>(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; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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; | |||
@@ -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>((float d) => { UnityEngine.Camera.main.fieldOfView = d; }, | |||
@@ -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<APITestCaseAttribute>(); | |||
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<APITestCaseAttribute>(); | |||
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<APITestCaseAttribute>(); | |||
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<APITestCaseAttribute>(); | |||
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<TaskContract> 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(); | |||
} | |||
@@ -0,0 +1,18 @@ | |||
using System; | |||
namespace TechbloxModdingAPI.Tests | |||
{ | |||
[AttributeUsage(AttributeTargets.Property)] | |||
public class TestValueAttribute : Attribute | |||
{ | |||
public object PossibleValue { get; } | |||
/// <summary> | |||
/// <param name="possibleValue"> | |||
/// When set, the property test accepts the specified value in addition to the test input.<br /> | |||
/// Useful if setting the property isn't always possible. | |||
/// </param> | |||
/// </summary> | |||
public TestValueAttribute(object possibleValue) => PossibleValue = possibleValue; | |||
} | |||
} |