Search the source of every open source package.
152 results
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
using Hexagon.V2.Application;
namespace Hexagon.V2.Tests.Application;
[TestClass]
public sealed class CharacterNameSkeletonTests
{
[TestMethod]
public void WithinScriptLookAlikesCollapseOntoTheNameTheyImitate()
{
var alice = CharacterNameSkeleton.Of( "Alice" );
// Digit-for-letter and the i/l/1 shape collision.
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A1ice" ) );
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "Alice" ) );
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "AIice" ) );
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "4lice" ) );
// Separators and punctuation carry no identity.
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A l i c e" ) );
Assert.AreEqual( alice, CharacterNameSkeleton.Of( "A-l-i-c-e" ) );
// Accents are decoration, not identity.
Assert.AreEqual( CharacterNameSkeleton.Of( "Jose" ), CharacterNameSkeleton.Of( "Jos\u00E9" ) );
// The classic digraph pair.
Assert.AreEqual( CharacterNameSkeleton.Of( "Bemard" ), CharacterNameSkeleton.Of( "Bernard" ) );
Assert.AreEqual( CharacterNameSkeleton.Of( "Walker" ), CharacterNameSkeleton.Of( "VValker" ) );
}
[TestMethod]
public void WholeNameWrittenInAnotherScriptCollapsesOntoTheLatinNameItImitates()
{
// The script profile permits these - they are single-script names. Only the skeleton
// sees that they are drawn to read as "Alice" and "Petro".
// Cyrillic es-o-o-er-ie-ghe, which draws "Cooper" without one Latin letter in it.
Assert.AreEqual(
CharacterNameSkeleton.Of( "Cooper" ),
CharacterNameSkeleton.Of( "\u0441\u043E\u043E\u0440\u0435\u0433" ) );
// Greek rho-omicron-rho-omicron-nu, which draws "Popov".
Assert.AreEqual(
CharacterNameSkeleton.Of( "Popov" ),
CharacterNameSkeleton.Of( "\u03C1\u03BF\u03C1\u03BF\u03BD" ) );
}
[TestMethod]
public void FoldingFollowsUnicodeRatherThanLocalIntuition()
{
// A documented residual, asserted so a table bump surfaces it rather than hiding it:
// UTS #39 judges Cyrillic em confusable with a turned w, NOT with Latin m, so a Cyrillic
// name using it does not collapse onto the Latin name it arguably resembles. The script
// profile still blocks the mixed-script form, which is the reachable attack.
Assert.AreNotEqual(
CharacterNameSkeleton.Of( "Maxwell" ),
CharacterNameSkeleton.Of( "\u043C\u0430\u0445\u051D\u0435\u0456\u0456" ) );
// Latin i and l stay distinct - the standard folds the digit 1, capital I and the bar
// onto l, but never merges lowercase i with it.
Assert.AreEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "A1ice" ) );
Assert.AreNotEqual( CharacterNameSkeleton.Of( "Bili" ), CharacterNameSkeleton.Of( "Bill" ) );
}
[TestMethod]
public void DistinctNamesKeepDistinctSkeletons()
{
Assert.AreNotEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "Alicia" ) );
Assert.AreNotEqual( CharacterNameSkeleton.Of( "Alice" ), CharacterNameSkeleton.Of( "Alison" ) );
Assert.AreNotEqual( CharacterNameSkeleton.Of( "Barney" ), CharacterNameSkeleton.Of( "Gordon" ) );
Assert.AreNotEqual( CharacterNameSkeleton.Of( "Judith Mossman" ), CharacterNameSkeleton.Of( "Eli Vance" ) );
// A name in a script with no Latin look-alikes is left alone rather than flattened.
Assert.AreNotEqual(
CharacterNameSkeleton.Of( "\u0410\u043B\u0438\u0441\u0430" ), // Cyrillic "Alisa"
CharacterNameSkeleton.Of( "\u0411\u043E\u0440\u0438\u0441" ) ); // Cyrillic "Boris"
}
[TestMethod]
public void NameWithNoIdentityBearingCharactersHasAnEmptySkeleton()
{
Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( "..." ) );
Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( "---" ) );
Assert.AreEqual( string.Empty, CharacterNameSkeleton.Of( string.Empty ) );
}
}using System.Text.Json;
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hexagon.V2.Tests.Application;
[TestClass]
public sealed class CurrencyAndCharacterRulesTests
{
[TestMethod]
public void CurrencyRejectsUnderflowAndOverflow()
{
var character = CreateCharacter( 10 );
Assert.IsTrue( CurrencyService.Debit( character, 11 ).Failed );
Assert.IsTrue( CurrencyService.Credit( character with { Balance = long.MaxValue }, 1 ).Failed );
}
[TestMethod]
public void SlotAllocatorUsesLowestGap()
{
var characters = new[] { CreateCharacter( 0 ) with { Slot = 0 }, CreateCharacter( 0 ) with { Slot = 2 } };
Assert.AreEqual( 1, CharacterRules.FindLowestFreeSlot( characters ) );
}
[TestMethod]
public void SlotAllocatorReturnsMinusOneWhenEverySlotIsOccupied()
{
var characters = Enumerable.Range( 0, CharacterRules.MaximumSlots )
.Select( slot => CreateCharacter( 0 ) with { Slot = slot } )
.ToArray();
Assert.AreEqual( -1, CharacterRules.FindLowestFreeSlot( characters ) );
var oneFree = characters.Where( character => character.Slot != CharacterRules.MaximumSlots - 1 ).ToArray();
Assert.AreEqual( CharacterRules.MaximumSlots - 1, CharacterRules.FindLowestFreeSlot( oneFree ) );
}
[TestMethod]
public void CreationRejectsUnregisteredFields()
{
var request = new CharacterCreationRequest
{
Name = "Valid Name",
Description = "A sufficiently long character description.",
Model = new DefinitionId( "test.model" ),
Faction = new FactionId( "test.faction" ),
Fields = new Dictionary<string, CreationValue> { ["money"] = CreationValue.Integer( long.MaxValue ) }
};
Assert.IsTrue( CharacterRules.ValidateCreationRequest( request, new HashSet<string>() ).Failed );
}
[TestMethod]
public void IdentityTextRejectsControlAndFormatCharacters()
{
// A name reaches other players verbatim through recognition records and nameplates, so
// spoofing payloads must not survive the creation boundary. These are written as escapes
// on purpose: as literals they are invisible in an editor and lost by copy/paste.
foreach ( var name in new[]
{
"Bob\nCombine Overwatch", // U+000A interior line break
"Bob\u0007Smith", // C0 control
"Bob\u202Eelbat", // right-to-left override
"Bo\u200Bb Smith", // zero-width space
"Bob\u200DSmith", // zero-width joiner
"Bob\u2066Smith\u2069", // bidirectional isolates
"Bob\uFEFFSmith" // zero-width no-break space
} )
{
Assert.IsTrue(
CharacterRules.NormalizeName( name ).Failed,
$"Name '{name.Replace( "\n", "\\n" )}' should have been rejected." );
Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request( name: name ), NoFields ).Failed );
}
// Zero-width characters are not whitespace, so they survive Trim: a length check
// alone can never catch a name built entirely from them.
Assert.IsTrue( CharacterRules.NormalizeName( "\u200B\u200B\u200B\u200B" ).Failed );
}
[TestMethod]
public void DescriptionKeepsLineBreaksButRejectsOtherControlCharacters()
{
var prose = "A tall citizen in a worn jacket.\nHe keeps his hands in his pockets.";
Assert.AreEqual( prose, CharacterRules.NormalizeDescription( prose ).Value );
Assert.IsTrue( CharacterRules.NormalizeDescription( "A citizen\u202Ewith a reversed tail." ).Failed );
Assert.IsTrue( CharacterRules.NormalizeDescription( "A citizen with a\u0000 null byte." ).Failed );
}
[TestMethod]
public void IdentityTextIsCanonicalizedSoValidationAndStorageAgree()
{
// Combining and precomposed acutes render identically, so they must not be
// storable as two distinct names.
var decomposed = CharacterRules.NormalizeName( " Ame\u0301lie " );
Assert.IsTrue( decomposed.Succeeded );
Assert.AreEqual( "Am\u00E9lie", decomposed.Value );
Assert.AreEqual( decomposed.Value, CharacterRules.NormalizeName( "Am\u00E9lie" ).Value );
// An unpaired surrogate is rejected rather than thrown out of Normalize.
Assert.IsTrue( CharacterRules.NormalizeName( "Bob\uD800Smith" ).Failed );
// Ordinary names still pass.
Assert.IsTrue( CharacterRules.NormalizeName( "Valid Name" ).Succeeded );
Assert.IsTrue( CharacterRules.NormalizeName( "Baßmann-O'Neill" ).Succeeded );
Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request(), NoFields ).Succeeded );
}
[TestMethod]
public void NameRejectsMixedScriptImpersonation()
{
// Each of these renders as ordinary Latin text but smuggles one letter from another
// script, which is exactly how a name is made to look like someone else's.
Assert.IsTrue( CharacterRules.NormalizeName( "\u0410lice" ).Failed ); // Cyrillic A
Assert.IsTrue( CharacterRules.NormalizeName( "Alic\u0435" ).Failed ); // Cyrillic e
Assert.IsTrue( CharacterRules.NormalizeName( "\u03A1eter" ).Failed ); // Greek Rho
Assert.IsTrue( CharacterRules.NormalizeName( "\u13AAlice" ).Failed ); // Cherokee
Assert.IsTrue( CharacterRules.ValidateCreationRequest( Request( name: "\u0410lice" ), NoFields ).Failed );
// A wholly Cyrillic or Greek name is a real name, not an impersonation.
Assert.IsTrue( CharacterRules.NormalizeName( "Алиса" ).Succeeded );
Assert.IsTrue( CharacterRules.NormalizeName( "Γεωργος" ).Succeeded );
}
[TestMethod]
public void NameAllowsTheScriptCombinationsRealNamesNeed()
{
// Japanese mixes Han, Hiragana and Katakana by nature, and may carry Latin.
Assert.IsTrue( CharacterRules.NormalizeName( "田中 ひろし" ).Succeeded );
Assert.IsTrue( CharacterRules.NormalizeName( "タナカ Tanaka" ).Succeeded );
// Korean mixes Hangul with Latin.
Assert.IsTrue( CharacterRules.NormalizeName( "김철수 Kim" ).Succeeded );
// Digits, spaces and punctuation are script-neutral and never constrain a name.
Assert.IsTrue( CharacterRules.NormalizeName( "Dr. Judith Mossman II" ).Succeeded );
// Greek alongside Latin is not a combination any real name needs.
Assert.IsTrue( CharacterRules.NormalizeName( "Alice Γεω" ).Failed );
}
[TestMethod]
public void NameFoldsCompatibilityLookAlikesOntoWhatTheyImitate()
{
// Fullwidth Latin is Latin script, so mixed-script detection cannot see it. NFKC folding
// is what stops it being a second, visually identical spelling of an existing name.
var fullwidth = CharacterRules.NormalizeName( "\uFF21lice" );
Assert.IsTrue( fullwidth.Succeeded );
Assert.AreEqual( "Alice", fullwidth.Value );
// A description is prose, not identity: it keeps compatibility characters and may mix
// scripts freely. Only its control- and format-character rules apply.
Assert.IsTrue( CharacterRules.NormalizeDescription(
"A citizen who mutters Алиса under their breath." ).Succeeded );
Assert.IsTrue( CharacterRules.NormalizeDescription(
"A citizen wearing a \uFF21-series jumpsuit, collar upturned." ).Succeeded );
}
[TestMethod]
public void TwoUnenumeratedScriptsAreStillTwoScripts()
{
// Neither Coptic nor Runic is in the named script table. They must not share one "other"
// bucket, or a name could mix two writing systems the profile never actually looked at.
Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83\u2C85" ).Succeeded ); // Coptic alone
Assert.IsTrue( CharacterRules.NormalizeName( "\u16A0\u16A2\u16A6" ).Succeeded ); // Runic alone
Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83\u16A0" ).Failed ); // the two mixed
Assert.IsTrue( CharacterRules.NormalizeName( "\u2C81\u2C83Ab" ).Failed ); // one with Latin
}
private static IReadOnlySet<string> NoFields => new HashSet<string>();
private static CharacterCreationRequest Request( string name = "Valid Name" ) =>
new()
{
Name = name,
Description = "A sufficiently long character description.",
Model = new DefinitionId( "test.model" ),
Faction = new FactionId( "test.faction" ),
Fields = new Dictionary<string, CreationValue>()
};
private static CharacterRecord CreateCharacter( long balance )
{
using var document = JsonDocument.Parse( "{}" );
return new CharacterRecord
{
Id = CharacterId.New(),
AccountId = new AccountId( 1 ),
Slot = 0,
Name = "Test",
Description = "A sufficiently long description.",
Model = new DefinitionId( "test.model" ),
Faction = new FactionId( "test.faction" ),
Balance = balance,
CreatedAt = DateTimeOffset.UtcNow,
LastPlayedAt = DateTimeOffset.UtcNow,
SchemaState = new TypedPayload
{
TypeId = new PersistedTypeId( "test.character" ),
TypeVersion = 1,
Data = document.RootElement.Clone()
}
};
}
}
#nullable enable
using Hexagon.V2.Client;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Tests.Client;
[TestClass]
public sealed class ClientControllerTests
{
[TestMethod]
public async Task ControllerMapsEveryUiIntentToAnExplicitCommand()
{
var transport = new RecordingTransport();
var controller = new HexClientController(transport);
var characterId = CharacterId.New();
var sourceId = InventoryId.New();
var targetId = InventoryId.New();
var itemId = ItemId.New();
var actionId = new ActionId("use");
var actionInstance = Guid.NewGuid();
var actionArguments = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal )
{
["amount"] = SnapshotValue.Integer( 3 )
};
var creation = new CharacterCreationInput(
"Alyx", "Description", new DefinitionId("citizen_model"),
new FactionId("citizen"), null);
await controller.RequestCharactersAsync();
await controller.CreateCharacterAsync(creation);
await controller.LoadCharacterAsync(characterId);
await controller.DeleteCharacterAsync(characterId);
await controller.UnloadCharacterAsync();
await controller.MoveItemAsync(sourceId, targetId, itemId, 2, 3);
await controller.RunItemActionAsync(sourceId, itemId, actionId, actionArguments);
actionArguments["amount"] = SnapshotValue.Integer( 99 );
await controller.DropItemAsync(sourceId, itemId);
await controller.PickUpItemAsync(itemId, targetId);
await controller.SendChatAsync("ic", "Hello");
await controller.CancelActionAsync(actionInstance);
Assert.HasCount(11, transport.Commands);
Assert.IsInstanceOfType<RequestCharacterListCommand>(transport.Commands[0]);
Assert.AreSame(creation, ((CreateCharacterCommand)transport.Commands[1]).Input);
Assert.AreEqual(characterId, ((LoadCharacterCommand)transport.Commands[2]).CharacterId);
Assert.IsInstanceOfType<DeleteCharacterCommand>(transport.Commands[3]);
Assert.IsInstanceOfType<UnloadCharacterCommand>(transport.Commands[4]);
Assert.AreEqual((2, 3),
(((MoveInventoryItemCommand)transport.Commands[5]).X,
((MoveInventoryItemCommand)transport.Commands[5]).Y));
Assert.AreEqual(actionId, ((RunItemActionCommand)transport.Commands[6]).ActionId);
Assert.AreEqual( 3L, ((RunItemActionCommand)transport.Commands[6]).Arguments["amount"].IntegerValue );
Assert.IsInstanceOfType<DropItemCommand>(transport.Commands[7]);
Assert.IsInstanceOfType<PickUpItemCommand>(transport.Commands[8]);
Assert.AreEqual("Hello", ((SendChatCommand)transport.Commands[9]).Text);
Assert.AreEqual(actionInstance, ((CancelActionCommand)transport.Commands[10]).InstanceId);
}
[TestMethod]
public async Task TransportFailureAndCancellationTokenArePropagated()
{
var expected = OperationResult.Failure(ErrorCode.Unauthorized, "Denied.");
var transport = new RecordingTransport(expected);
var controller = new HexClientController(transport);
using var source = new CancellationTokenSource();
var result = await controller.SendChatAsync("ic", "Hello", source.Token);
Assert.AreEqual(ErrorCode.Unauthorized, result.Error!.Code);
Assert.AreEqual(source.Token, transport.LastCancellationToken);
}
private sealed class RecordingTransport : IClientCommandTransport
{
private readonly OperationResult _result;
public RecordingTransport(OperationResult? result = null) =>
_result = result ?? OperationResult.Success();
public List<ClientCommand> Commands { get; } = new();
public CancellationToken LastCancellationToken { get; private set; }
public ValueTask<OperationResult> SendAsync(
ClientCommand command,
CancellationToken cancellationToken = default)
{
Commands.Add(command);
LastCancellationToken = cancellationToken;
return ValueTask.FromResult(_result);
}
}
}
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize( Scope = ExecutionScope.MethodLevel )]
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
[AssemblyInitialize]
public static void ClassInitialize( TestContext context )
{
Sandbox.Application.InitUnitTest();
}
}
using System.Numerics;
using System.Text;
using HumanoidRetargeter.Cleanup;
using HumanoidRetargeter.Formats.Bvh;
using HumanoidRetargeter.Formats.Fbx;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using Xunit;
namespace HumanoidRetargeter.Tests.Solve;
public class PosedLocomotionTests
{
[Theory]
[InlineData(1, 500)]
[InlineData(1, -500)]
[InlineData(2, 500)]
[InlineData(2, -500)]
public void InPlaceCentersClipsWhoseStaticReferenceIsFarFromTheTake(int upAxis, float restTravel)
{
var target = Target(upAxis);
var frames = Convert(Fixture(upAxis, restTravel: restTravel), target, RootMotionMode.InPlace);
var hips = target.Rig.BoneForRole(HumanoidRetargeter.Mapping.BoneRole.Hips)!.Value;
var center = frames.Aggregate(Vector3.Zero, (sum, frame) => sum + new Pose(frame).ToWorld(target.Rig.Skeleton)[hips].Pos) / frames.Count;
var delta = center - target.Rig.Skeleton.RestWorld[hips].Pos;
if (upAxis == 1) delta.Y = 0; else delta.Z = 0;
Assert.True(delta.Length() < .01f, $"In-place clip is displaced from the target bind by {delta}.");
}
[Theory]
[InlineData(1, false, 0)]
[InlineData(1, true, -90)]
[InlineData(2, false, 90)]
[InlineData(2, true, 90)]
public void SerializedRootYawMatchesTargetImportConvention(int upAxis, bool embedsMesh, float degrees)
{
var original = Target(upAxis);
var target = new RetargetTargetSpec
{
Rig = original.Rig, UpAxis = original.UpAxis, VmdlScale = 1,
MeshFilePath = embedsMesh ? "test.fbx" : "",
};
var frame = target.Rig.Skeleton.Bones.Select(b => b.RestLocal).ToArray();
var before = frame.ToArray();
var serialized = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(new[] { frame }, target).Single();
var yaw = Quaternion.CreateFromAxisAngle(upAxis == 1 ? Vector3.UnitY : Vector3.UnitZ, degrees * MathF.PI / 180);
for (var i = 0; i < frame.Length; i++)
{
var root = target.Rig.Skeleton[i].ParentIndex < 0;
var expectedPosition = root ? Vector3.Transform(before[i].Pos, yaw) : before[i].Pos;
var expectedRotation = root ? Quaternion.Normalize(yaw * before[i].Rot) : before[i].Rot;
Assert.True(Vector3.Distance(expectedPosition, serialized[i].Pos) < .0001f);
Assert.True(MathF.Abs(Quaternion.Dot(expectedRotation, serialized[i].Rot)) > .99999f);
Assert.Equal(before[i], frame[i]); // Serialization must not mutate solved poses.
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void LeaningSourceRestDoesNotTurnForwardTravelIntoVerticalMotion(int upAxis)
{
var target = Target(upAxis);
var result = Convert(Fixture(upAxis, torsoLean: 70), target, RootMotionMode.InPlace);
var hip = target.Rig.BoneForRole(HumanoidRetargeter.Mapping.BoneRole.Hips)!.Value;
var heights = result.Select(frame => Vertical(new Pose(frame).ToWorld(target.Rig.Skeleton)[hip].Pos, upAxis)).ToArray();
Assert.True(heights.Max() - heights.Min() < .05f,
$"Horizontal travel changed pelvis height by {heights.Max() - heights.Min()}.");
}
[Theory]
[InlineData(1)]
[InlineData(2)]
public void RemovingRootTravelPreservesGroundedHeightsOnLeaningTarget(int upAxis)
{
var target = Target(upAxis, torsoLean: 20);
var bytes = Fixture(upAxis);
var moving = Convert(bytes, target, RootMotionMode.Off);
var inPlace = Convert(bytes, target, RootMotionMode.InPlace);
for (var f = 0; f < moving.Count; f++)
{
var before = new Pose(moving[f]).ToWorld(target.Rig.Skeleton);
var after = new Pose(inPlace[f]).ToWorld(target.Rig.Skeleton);
for (var b = 0; b < before.Length; b++)
Assert.True(MathF.Abs(Vertical(before[b].Pos - after[b].Pos, upAxis)) < .005f,
$"Frame {f}, bone {b}: root-motion removal changed ground clearance.");
}
}
[Fact]
public void LiftedFootInExportedRestDoesNotPushPlantedFootBelowGround()
{
var target = Target(1);
var result = Convert(Fixture(1, liftedRest: 35, travel: 0), target, RootMotionMode.Off);
var toe = target.Rig.BoneForRole(HumanoidRetargeter.Mapping.BoneRole.ToeR)!.Value;
var floor = target.Rig.Skeleton.RestWorld[toe].Pos.Y;
foreach (var frame in result)
Assert.True(new Pose(frame).ToWorld(target.Rig.Skeleton)[toe].Pos.Y >= floor - .1f,
"A mid-step rest was incorrectly used as the planted-foot height reference.");
}
static float Vertical(Vector3 value, int axis) => axis == 1 ? value.Y : value.Z;
static RetargetTargetSpec Target(int upAxis, float torsoLean = 0)
{
var skeleton = FbxImporter.Import(Fixture(upAxis, torsoLean)).Skeleton;
var (map, _) = Retargeter.ResolveMapping(skeleton);
return new RetargetTargetSpec
{
Rig = TargetRig.FromSkeleton(skeleton, map), VmdlScale = 1,
UpAxis = upAxis == 1 ? TargetUpAxis.YUpCm : TargetUpAxis.ZUpEngine,
};
}
static List<HumanoidRetargeter.Maths.XForm[]> Convert(byte[] bytes, RetargetTargetSpec target, RootMotionMode mode)
{
var result = Retargeter.Convert(new RetargetRequest
{
SourceData = bytes, SourceFileName = "posed-locomotion.fbx",
RootMotion = mode, FootPlantCleanup = true,
}, target);
Assert.True(result.Success, string.Join("; ", result.Clips.Select(c => c.Error)));
return result.Clips[0].SolvedFrames!;
}
// A tiny animation-only FBX: no proprietary pack files or optional corpus needed.
// The static rig can be bent over or caught mid-step; the take moves horizontally.
static byte[] Fixture(int upAxis, float torsoLean = 0, float liftedRest = 0, float travel = 100, float restTravel = 0)
{
var skeleton = BvhImporter.Import(Encoding.UTF8.GetBytes(PosedSkeletonFixture.SyntheticWalkBvh())).Skeleton;
var objects = new StringBuilder();
var connections = new StringBuilder();
foreach (var bone in skeleton.Bones)
{
var p = bone.RestLocal.Pos;
if (bone.ParentIndex < 0) { p.Y = 93; p.Z = restTravel; }
if (upAxis == 2) p = new Vector3(p.X, -p.Z, p.Y);
var angle = bone.Name == "mixamorig:Spine" ? torsoLean
: bone.Name == "mixamorig:RightUpLeg" ? liftedRest : 0;
objects.AppendLine(FormattableString.Invariant($$"""
Model: {{bone.Index + 1}}, "Model::{{bone.Name}}", "LimbNode" {
Properties70: {
P: "Lcl Translation", "Lcl Translation", "", "A",{{p.X}},{{p.Y}},{{p.Z}}
P: "Lcl Rotation", "Lcl Rotation", "", "A",{{angle}},0,0
}
}
"""));
connections.AppendLine($"C: \"OO\",{bone.Index + 1},{bone.ParentIndex + 1}");
}
void Curve(int id, int bone, string property, string axis, float end)
{
objects.AppendLine(FormattableString.Invariant($$"""
AnimationCurveNode: {{id}}, "AnimCurveNode::curve", "" { }
AnimationCurve: {{id + 1}}, "AnimCurve::value", "" {
KeyTime: *2 { a: 0,46186158000 }
KeyValueFloat: *2 { a: 0,{{end}} }
}
"""));
connections.AppendLine($"C: \"OO\",{id},10001\nC: \"OP\",{id},{bone},\"{property}\"\nC: \"OP\",{id + 1},{id},\"d|{axis}\"");
}
Curve(10002, 1, "Lcl Translation", upAxis == 1 ? "Z" : "Y", upAxis == 1 ? travel : -travel);
Curve(10004, skeleton.IndexOf("mixamorig:RightUpLeg") + 1, "Lcl Rotation", "X", 0);
return Encoding.UTF8.GetBytes(FormattableString.Invariant($$"""
GlobalSettings: { Properties70: {
P: "UpAxis", "int", "Integer", "",{{upAxis}}
P: "UpAxisSign", "int", "Integer", "",1
P: "UnitScaleFactor", "double", "Number", "",1
} }
Objects: {
{{objects}}
AnimationStack: 10000, "AnimStack::walk", "" { }
AnimationLayer: 10001, "AnimLayer::base", "" { }
}
Connections: {
{{connections}}
C: "OO",10001,10000
}
"""));
}
}
using System.Numerics;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Tests.Target;
public class SmartPortAdditiveIkTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void RecoilDoesNotInventGoalTranslationFromDifferentArmBindPoses(bool authoredGoalMotion)
{
var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
const string goal = "support_goal";
var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
definitions.Add(new(goal, "hand_R", XForm.ToLocal(original.RestWorld[original.IndexOf("hand_R")],
original.RestWorld[original.IndexOf("hand_L")])));
var source = SkeletonModel.Create(definitions);
var target = SkeletonModel.Create(source.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : source[b.ParentIndex].Name,
new XForm(b.RestLocal.Pos * .8f, b.RestLocal.Rot *
(b.Name.StartsWith("arm_upper_") ? Quaternion.CreateFromAxisAngle(Vector3.UnitY, .6f) : Quaternion.Identity)))).ToArray());
var rig = new SmartPortRig(source, target, new Dictionary<string, string> { [goal] = "hand_L" });
var ordinary = new SmartPortRig(source, target);
var pose = Enumerable.Repeat(XForm.Identity, source.Count).ToArray();
pose[source.IndexOf("arm_upper_R")].Rot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .2f);
pose[source.IndexOf("arm_lower_L")].Rot = Quaternion.CreateFromAxisAngle(Vector3.UnitX, -.3f);
pose[source.IndexOf(goal)] = new XForm(authoredGoalMotion ? new Vector3(.2f, -.1f, .3f) : Vector3.Zero,
Quaternion.CreateFromAxisAngle(Vector3.UnitY, .1f));
var actual = rig.Transfer(pose, true)[rig.Target.IndexOf(goal)];
var expected = ordinary.Transfer(pose, true)[ordinary.Target.IndexOf(goal)];
// Additive goals are local deltas on the already fitted grip, not absolute
// effector positions computed against a reconstructed rest-pose animation.
Assert.True(Vector3.Distance(expected.Pos, actual.Pos) < .0001f, $"Expected {expected.Pos}, got {actual.Pos}");
Assert.True(MathQ.AngleBetween(expected.Rot, actual.Rot) < .001f);
if (!authoredGoalMotion) Assert.True(actual.Pos.Length() < .0001f);
else Assert.True(actual.Pos.Length() > .1f); // Preserve authored motion; do not freeze the goal.
}
}
using System.Numerics;
using HumanoidRetargeter.Cleanup;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Tests.Target;
public class SmartPortGripReachTests
{
[Fact]
public void SharedGripFitsShorterArmsWithoutStretching()
{
var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
foreach (var side in new[] { "L", "R" }) definitions.Add(new("grip_" + side, "hand_" + side, XForm.Identity));
definitions.Add(new("support_goal", "hand_R", XForm.Identity));
var source = SkeletonModel.Create(definitions);
var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
new XForm(b.RestLocal.Pos * (b.Name is "arm_lower_L" or "hand_L" ? .55f : 1), b.RestLocal.Rot))).ToArray());
var pose = source.Bones.Select(b => b.RestLocal).ToArray();
var left = source.IndexOf("arm_upper_L"); var right = source.IndexOf("arm_upper_R");
var center = (source.RestWorld[left].Pos + source.RestWorld[right].Pos) * .5f;
var up = Vector3.Normalize(source.RestWorld[source.IndexOf("head")].Pos - source.RestWorld[source.IndexOf("pelvis")].Pos);
var lateral = Vector3.Normalize(source.RestWorld[right].Pos - source.RestWorld[left].Pos);
var forward = Vector3.Normalize(Vector3.Cross(up, lateral));
var reach = source[source.IndexOf("arm_lower_R")].RestLocal.Pos.Length() + source[source.IndexOf("hand_R")].RestLocal.Pos.Length();
foreach (var side in new[] { "L", "R" })
{
var chain = new LimbChain { Upper = source.IndexOf("arm_upper_" + side), Lower = source.IndexOf("arm_lower_" + side), End = source.IndexOf("hand_" + side) };
var goal = center + forward * reach * (side == "L" ? .75f : .5f) + lateral * (side == "L" ? -4 : 4) - up * reach * .1f;
EffectorIk.ApplyGoals(new() { pose }, source, chain, new[] { goal }, lateral, soften: 0);
}
var input = new Pose(pose).ToWorld(source);
pose[source.IndexOf("support_goal")] = XForm.ToLocal(input[source.IndexOf("hand_R")], input[source.IndexOf("hand_L")]);
var targets = new Dictionary<string, string> { ["support_goal"] = "hand_L" };
var rig = new SmartPortRig(source, target, targets, new[] { "grip_L", "grip_R" });
var result = rig.Transfer(pose);
var world = new Pose(result).ToWorld(rig.Target);
var error = Vector3.Distance(world[rig.Target.IndexOf("hand_L")].Pos, world[rig.Target.IndexOf("support_goal")].Pos);
Assert.True(error < .01f, $"Support hand missed its goal by {error}");
foreach (var side in new[] { "L", "R" })
foreach (var name in new[] { "arm_upper_", "arm_lower_", "hand_" })
{
var index = rig.Target.IndexOf(name + side);
Assert.True(Vector3.Distance(rig.Target[index].RestLocal.Pos, result[index].Pos) < .0001f);
}
var unfitted = new SmartPortRig(source, target, targets);
var before = new Pose(unfitted.Transfer(pose)).ToWorld(unfitted.Target);
Assert.True(Vector3.Distance(before[unfitted.Target.IndexOf("hand_L")].Pos, before[unfitted.Target.IndexOf("support_goal")].Pos) > .1f);
Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
{
Assert.True(delta.Pos.Length() < .0001f);
Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
});
}
}
using System.Numerics;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Tests.Target;
public class SmartPortHandSocketTests
{
[Theory]
[InlineData(0)]
[InlineData(2)]
public void CopiedSocketFitsTheTargetHandNotItsLegProportions(int helperDepth)
{
var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
var names = new[] { "finger_index_0_R", "finger_middle_0_R", "finger_ring_0_R" };
Vector3 Center(SkeletonModel sk, XForm[] world) => names.Select(n => world[sk.IndexOf(n)].Pos).Aggregate(Vector3.Zero, (a,b) => a+b) / names.Length;
const string socket = "new_hand_socket";
var bind = original.RestWorld.ToArray();
var hand = original.IndexOf("hand_R");
var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
definitions.Add(new(socket, "hand_R", new XForm(bind[hand].Inverse().TransformPoint(Center(original, bind)), Quaternion.Identity)));
var attachment = socket;
for (var i = 0; i < helperDepth; i++)
{
var child = "nested_socket_" + i;
definitions.Add(new(child, attachment, XForm.Identity));
attachment = child;
}
var source = SkeletonModel.Create(definitions);
var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
new XForm(b.RestLocal.Pos * (b.Name.StartsWith("finger_") ? .5f : 1f), b.RestLocal.Rot))).ToArray());
var rig = new SmartPortRig(source, target, attachmentBones: new[] { attachment, socket });
var pose = source.Bones.Select(b => b.RestLocal).ToArray();
var result = rig.Transfer(pose);
var world = new Pose(result).ToWorld(rig.Target);
var unfitted = new SmartPortRig(source, target);
var before = new Pose(unfitted.Transfer(pose)).ToWorld(unfitted.Target);
Assert.True(Vector3.Distance(Center(unfitted.Target, before), before[unfitted.Target.IndexOf(socket)].Pos) > .1f);
Assert.True(Vector3.Distance(Center(rig.Target, world), world[rig.Target.IndexOf(socket)].Pos) < .001f,
$"Expected {Center(rig.Target, world)}, got {world[rig.Target.IndexOf(socket)].Pos}");
Assert.True(Vector3.Distance(Center(rig.Target, world), world[rig.Target.IndexOf(attachment)].Pos) < .001f);
foreach (var bone in target.Bones)
Assert.Equal(bone.RestLocal, rig.Target[rig.Target.IndexOf(bone.Name)].RestLocal);
Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
{
Assert.True(delta.Pos.Length() < .0001f);
Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
});
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ExistingFittedSocketIsNotReplacedByHandGeometry(bool nestedSource)
{
var original = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
var definitions = original.Bones.Select(b => new BoneDefinition(b.Name,
nestedSource && b.Name == "hold_R" ? "weapon_pivot" : b.ParentIndex < 0 ? null : original[b.ParentIndex].Name, b.RestLocal)).ToList();
if (nestedSource) definitions.Add(new("weapon_pivot", "hand_R", XForm.Identity));
var source = SkeletonModel.Create(definitions);
var target = SkeletonModel.Create(original.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : original[b.ParentIndex].Name,
new XForm(b.RestLocal.Pos * (b.Name.StartsWith("finger_") ? .5f : 1f)
+ (b.Name == "hold_R" ? new Vector3(.2f, .1f, .3f) : Vector3.Zero), b.RestLocal.Rot))).ToArray());
var fitted = new SmartPortRig(source, target, attachmentBones: new[] { "hold_R" });
var ordinary = new SmartPortRig(source, target);
var pose = source.Bones.Select(b => b.RestLocal).ToArray();
Assert.Equal(ordinary.Transfer(pose), fitted.Transfer(pose));
}
}
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;
// A custom rig without stock attachments must receive calibrated copied sockets.
// Run only in an explicitly opted-in isolated project, never a user's active editor.
public static class SmartPortCopiedSocketEngineTest
{
static bool started;
[EditorEvent.Frame]
public static void Tick()
{
var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_COPIED_SOCKET_PROJECT");
if (started || Project.Current is null || string.IsNullOrEmpty(root)
|| !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'), Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
|| !AssetSystem.All.Any()) return;
started = true;
_ = Run(root);
}
static async Task Run(string root)
{
var data = new Dictionary<string, object>();
void Save() => File.WriteAllText(Path.Combine(root, "copied-socket-result.json"),
JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
try
{
data["started"] = DateTime.UtcNow; Save();
var source = AssetSystem.FindByPath("models/citizen/citizen.vmdl") ?? throw new Exception("Missing Citizen source");
var target = AssetSystem.FindByPath("humanoid_retargeter_smoke/customfbx/catgirl_2_preview_bind_9e13ad1f.vmdl") ?? throw new Exception("Missing Catgirl target fixture");
var type = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetType("HumanoidRetargeter.Editor.SmartPortModels")).First(t => t != null);
var name = "copied_socket_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
Action<string> progress = s => { data["stage"] = s; Save(); };
var task = (Task)type.GetMethod("CreateAsync", BindingFlags.Static | BindingFlags.NonPublic)
.Invoke(null, new object[] { source, target, "smart_port", name, progress, CancellationToken.None });
await task;
var result = task.GetType().GetProperty("Result").GetValue(task);
data["compiled"] = result.GetType().GetProperty("Compiled").GetValue(result);
data["errors"] = result.GetType().GetProperty("Errors").GetValue(result);
data["model"] = "smart_port/" + name + ".vmdl"; Save();
if (!(bool)data["compiled"]) throw new Exception("Compile failed");
var session = SceneEditorSession.CreateDefault(); session.MakeActive();
using (session.Scene.Push())
foreach (var path in new[] { source.Path, (string)data["model"] })
{
var go = session.Scene.CreateObject(); go.Name = path;
var actor = go.Components.Create<SkinnedModelRenderer>();
actor.Model = Model.Load(path); actor.UseAnimGraph = true;
}
EditorScene.Play(false, session); await Task.Delay(1500);
if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
var observations = new List<object>();
const int hold = 2; // Source graph's rifle hold type.
foreach (var weight in new[] { 0f, 1f })
foreach (var pitch in new[] { 0f, -25f, 25f })
{
for (var settle = 0; settle < 50; settle++)
{
foreach (var actor in actors)
{
actor.Set("b_grounded", true); actor.Set("holdtype", hold); actor.Set("weapon_pose", 0);
actor.Set("aim_body", Rotation.From(pitch, 0, 0).Forward); actor.Set("aim_body_weight", weight);
actor.Set("aim_head", Rotation.From(pitch, 0, 0).Forward); actor.Set("aim_head_weight", 1f);
}
await Task.Delay(100);
}
var sourceActor = actors.Single(a => a.Model.Name == source.Path);
var targetActor = actors.Single(a => a.Model.Name == (string)data["model"]);
var sourceGrip = sourceActor.SceneModel.GetAttachment("hold_R", true).Value;
var targetGrip = targetActor.SceneModel.GetAttachment("hold_R", true).Value;
var a = sourceGrip.Rotation; var b = targetGrip.Rotation;
var error = 2 * MathF.Acos(Math.Clamp(MathF.Abs(a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w), 0, 1)) * 180 / MathF.PI;
if (!float.IsFinite(error) || error > 3) throw new Exception($"Rifle attachment diverged {error} degrees (aim weight {weight}, pitch {pitch})");
foreach (var actor in actors)
{
var att = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing grip");
observations.Add(new { model = actor.Model.Name, hold, weight, pitch, error,
rotation = att.Rotation.Angles().ToString(), forward = att.Rotation.Forward.ToString(), position = att.Position.ToString() });
}
}
data["observations"] = observations; data["passed"] = true;
}
catch (Exception e) { data["error"] = e.ToString(); data["passed"] = false; }
finally
{
data["completed"] = true; Save();
if (Game.IsPlaying) EditorScene.Stop();
await Task.Delay(1000); EditorUtility.Quit(true);
}
}
}
using HumanoidRetargeter.Editor;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.ResourceTypes;
using Xunit;
namespace SmartPort.Parser.Tests;
public class SmartPortHandSocketMetadataTests
{
[CompiledFixtureFact]
public void AttachmentFittingUsesCompiledParentBones()
{
var path = Path.Combine(Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!, "models/player/human/frank_mp.vmdl_c");
var names = SmartPortAttachments.BoneNames(path);
using var resource = new Resource();
resource.Read(path);
var model = Assert.IsType<Model>(resource.DataBlock);
Assert.Contains("hold_R", names);
Assert.Contains("hold_L", names);
Assert.Equal(names.Length, names.Distinct().Count());
Assert.All(names.Where(n => !string.IsNullOrEmpty(n)), n => Assert.Contains(model.Skeleton.Bones, b => b.Name == n));
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace WackyLib.Tests;
[TestClass]
public class TestInit
{
private static Sandbox.TestAppSystem s_appSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
s_appSystem = new Sandbox.TestAppSystem();
s_appSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
s_appSystem.Shutdown();
}
}
using Sandbox;
[TestClass]
public class SyncToolYamlRendererTests
{
[TestMethod]
public void EmptyInputProducesEmptyOutput()
{
Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( null ) );
Assert.AreEqual( "", SyncToolYamlRenderer.RenderFromJson( "" ) );
}
[TestMethod]
public void EmptyObjectAndArrayRenderAsFlowStyle()
{
Assert.AreEqual( "{}\n", SyncToolYamlRenderer.RenderFromJson( "{}" ) );
Assert.AreEqual( "[]\n", SyncToolYamlRenderer.RenderFromJson( "[]" ) );
}
[TestMethod]
public void InvalidJsonReturnsInputUnchanged()
{
var notJson = "not: real: json: ::";
Assert.AreEqual( notJson, SyncToolYamlRenderer.RenderFromJson( notJson ) );
}
[TestMethod]
public void TopLevelKeysAreSortedAlphabetically()
{
var json = "{\"zeta\":1,\"alpha\":2,\"mu\":3}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "alpha: 2\nmu: 3\nzeta: 1\n", yaml );
}
[TestMethod]
public void NestedObjectsAreSortedRecursively()
{
var json = "{\"outer\":{\"zeta\":1,\"alpha\":2}}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "outer:\n alpha: 2\n zeta: 1\n", yaml );
}
[TestMethod]
public void DifferentKeyOrdersProduceIdenticalOutput()
{
var a = "{\"slug\":\"hello\",\"method\":\"POST\",\"enabled\":true}";
var b = "{\"enabled\":true,\"method\":\"POST\",\"slug\":\"hello\"}";
Assert.AreEqual(
SyncToolYamlRenderer.RenderFromJson( a ),
SyncToolYamlRenderer.RenderFromJson( b )
);
}
[TestMethod]
public void OutputUsesYamlSyntaxNotJsonSyntax()
{
var json = "{\"name\":\"hooked\",\"enabled\":true,\"max\":42}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
// Smoke check: the rendered text must look like YAML, not JSON.
// This is the regression we're guarding: prior to the fix the diff
// view rendered structured data as JSON.
StringAssert.DoesNotMatch( yaml, new System.Text.RegularExpressions.Regex( @"^\s*\{" ) );
StringAssert.Contains( yaml, "enabled: true" );
StringAssert.Contains( yaml, "max: 42" );
StringAssert.Contains( yaml, "name: \"hooked\"" );
}
[TestMethod]
public void StringValuesAreQuotedAndEscapedSafely()
{
var json = "{\"text\":\"a:b\\nc\"}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
// Strings go through JSON quoting which is also valid YAML.
// The colon/newline must not leak as YAML structure.
Assert.AreEqual( "text: \"a:b\\nc\"\n", yaml );
}
[TestMethod]
public void BooleanAndNullAreUnquoted()
{
var json = "{\"a\":true,\"b\":false,\"c\":null}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "a: true\nb: false\nc: null\n", yaml );
}
[TestMethod]
public void IntegerAndDoubleAreUnquoted()
{
var json = "{\"i\":7,\"d\":1.5}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "d: 1.5\ni: 7\n", yaml );
}
[TestMethod]
public void ArraysOfObjectsRenderAsBlockSequence()
{
var json = "{\"steps\":[{\"name\":\"first\",\"id\":1},{\"name\":\"second\",\"id\":2}]}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
var expected =
"steps:\n" +
" -\n" +
" id: 1\n" +
" name: \"first\"\n" +
" -\n" +
" id: 2\n" +
" name: \"second\"\n";
Assert.AreEqual( expected, yaml );
}
[TestMethod]
public void ArraysOfScalarsRenderAsBlockSequence()
{
var json = "{\"tags\":[\"a\",\"b\",\"c\"]}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "tags:\n - \"a\"\n - \"b\"\n - \"c\"\n", yaml );
}
[TestMethod]
public void TopLevelArrayRenders()
{
var json = "[1,2,3]";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "- 1\n- 2\n- 3\n", yaml );
}
[TestMethod]
public void KeysWithNonIdentifierCharactersAreQuoted()
{
var json = "{\"weird key\":1,\"x:y\":2}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
// Bare YAML keys must not contain spaces or colons, so the renderer
// quotes them. Sort order is by raw key (Ordinal).
StringAssert.Contains( yaml, "\"weird key\": 1" );
StringAssert.Contains( yaml, "\"x:y\": 2" );
}
[TestMethod]
public void EmptyNestedObjectAndArrayUseFlowStyle()
{
var json = "{\"obj\":{},\"arr\":[]}";
var yaml = SyncToolYamlRenderer.RenderFromJson( json );
Assert.AreEqual( "arr: []\nobj: {}\n", yaml );
}
}
#nullable enable
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Kernel.Events;
using Hexagon.V2.Persistence;
namespace Hexagon.V2.Tests.Application;
[TestClass]
public sealed class SceneAndAggregateMutationTests
{
[TestMethod]
public async Task SceneStateUsesRegisteredTypesAndPublishesOnlyCommittedReplacement()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var service = new SceneEntityStateService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<SceneEntityStateMutationContext>());
var sceneEntityId = SceneEntityId.New();
var actor = new AccountId(9101);
var initial = ApplicationServiceTestEnvironment.StatePayload("closed");
var ensured = await service.EnsureAsync(sceneEntityId, "door", initial);
var replaced = await service.ReplaceStateAsync(
actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("open"));
var incompatible = await service.ReplaceStateAsync(
actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("future", version: 2));
environment.Provider.FailNextCommit();
var failedCommit = await service.ReplaceStateAsync(
actor, null, sceneEntityId, ApplicationServiceTestEnvironment.StatePayload("unpublished"));
Assert.IsTrue(ensured.Succeeded, ensured.Error?.Message);
Assert.IsTrue(replaced.Succeeded, replaced.Error?.Message);
Assert.AreEqual(ErrorCode.PersistedTypeInvalid, incompatible.Error!.Code);
Assert.AreEqual(ErrorCode.InternalError, failedCommit.Error!.Code);
var stored = service.Find(sceneEntityId)!;
Assert.AreEqual("open", stored.State.Data.GetProperty("name").GetString());
Assert.AreEqual(ApplicationServiceTestEnvironment.StateTypeId, stored.State.TypeId.Value);
Assert.AreEqual(1, stored.State.TypeVersion);
}
[TestMethod]
public async Task TouchLastPlayedReturnsOnlyProviderIssuedDurableReceipt()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var account = new AccountId( 9103 );
var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
{
LastPlayedAt = DateTimeOffset.UnixEpoch
};
await environment.SeedAsync( unitOfWork =>
unitOfWork.Create( environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
var service = new AggregateMutationService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>() );
var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
var touched = await service.TouchLastPlayedAsync( account, character.Id, timestamp );
Assert.IsTrue( touched.Succeeded, touched.Error?.Message );
Assert.AreEqual( CharacterMutationKind.TouchLastPlayed, touched.Value.Kind );
Assert.AreEqual( character.LastPlayedAt, touched.Value.Before.LastPlayedAt );
Assert.AreEqual( timestamp, touched.Value.After.LastPlayedAt );
Assert.IsGreaterThan( 0L, touched.Value.Commit.Sequence );
Assert.IsTrue( touched.Value.Commit.Documents.Any( document =>
document.Address.Collection == DomainCollections.Characters &&
document.Address.Key == DomainKeys.Character( character.Id ) ) );
environment.Provider.FailNextCommit();
var failed = await service.TouchLastPlayedAsync(
account, character.Id, timestamp.AddMinutes( 1 ) );
Assert.IsTrue( failed.Failed );
Assert.AreEqual( ErrorCode.InternalError, failed.Error!.Code );
Assert.AreEqual( timestamp, environment.Repositories.Characters.Find(
DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );
}
[TestMethod]
public async Task PreparedTouchStagesIntoCombinedCommitAndPublishesOnlyOnMatchingCompletion()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var account = new AccountId( 9104 );
var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
{
LastPlayedAt = DateTimeOffset.UnixEpoch
};
var item = ApplicationServiceTestEnvironment.Item();
await environment.SeedAsync( unitOfWork =>
{
unitOfWork.Create(
environment.Repositories.Characters, DomainKeys.Character( character.Id ), character );
unitOfWork.Create( environment.Repositories.Items, DomainKeys.Item( item.Id ), item );
} );
var events = new RecordingCharacterChangedHandler();
var service = new AggregateMutationService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
new PostCommitEventBus<CharacterChangedEvent>( new[]
{
new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
} ) );
var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
var itemDocument = environment.Repositories.Items.Find( DomainKeys.Item( item.Id ) )!;
var itemEditor = unitOfWork.Edit( environment.Repositories.Items, itemDocument )!;
itemEditor.Replace( itemEditor.Value with { Revision = 1 } );
unitOfWork.Save( itemEditor );
var committed = await unitOfWork.CommitAsync();
await unitOfWork.DisposeAsync();
Assert.IsTrue( committed.Succeeded, committed.Error?.Message );
Assert.HasCount( 2, committed.Value!.Documents );
Assert.IsEmpty( events.Events );
var premature = service.CompleteTouchLastPlayed(
prepared.Value, new CommitReceipt( committed.Value.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
Assert.AreEqual( ErrorCode.InvariantViolation, premature.Error!.Code );
Assert.IsEmpty( events.Events );
var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value );
Assert.IsTrue( completed.Succeeded, completed.Error?.Message );
Assert.AreSame( committed.Value, completed.Value.Commit );
Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
Assert.HasCount( 1, events.Events );
Assert.AreEqual( committed.Value.Sequence, events.Events[0].CommitSequence );
}
[TestMethod]
public async Task CompletionSucceedsFromTheReceiptAloneAfterAnInterleavedCommit()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var account = new AccountId( 9106 );
var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
{
LastPlayedAt = DateTimeOffset.UnixEpoch
};
await environment.SeedAsync( unitOfWork =>
unitOfWork.Create(
environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
var events = new RecordingCharacterChangedHandler();
var service = new AggregateMutationService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
new PostCommitEventBus<CharacterChangedEvent>( new[]
{
new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
} ) );
var timestamp = DateTimeOffset.UnixEpoch.AddMinutes( 1 );
var prepared = service.PrepareTouchLastPlayed( account, character.Id, timestamp );
Assert.IsTrue( prepared.Succeeded, prepared.Error?.Message );
var unitOfWork = environment.Repositories.Provider.BeginUnitOfWork();
Assert.IsTrue( service.StageTouchLastPlayed( unitOfWork, prepared.Value ).Succeeded );
var committed = await unitOfWork.CommitAsync();
await unitOfWork.DisposeAsync();
Assert.IsTrue( committed.Succeeded, committed.Error?.Message );
var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
competingEditor.Replace( competingEditor.Value with
{
LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 2 )
} );
competingUnit.Save( competingEditor );
Assert.IsTrue( (await competingUnit.CommitAsync()).Succeeded );
await competingUnit.DisposeAsync();
var completed = service.CompleteTouchLastPlayed( prepared.Value, committed.Value! );
Assert.IsTrue( completed.Succeeded,
"A durably committed mutation must complete from its receipt even after an interleaved commit." );
Assert.AreEqual( timestamp, completed.Value.After.LastPlayedAt );
Assert.HasCount( 1, events.Events );
}
[TestMethod]
public async Task PreparedTouchCommitFailureAndRevisionConflictEmitNoEvent()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var account = new AccountId( 9105 );
var character = ApplicationServiceTestEnvironment.Character( account, 0 ) with
{
LastPlayedAt = DateTimeOffset.UnixEpoch
};
await environment.SeedAsync( unitOfWork =>
unitOfWork.Create(
environment.Repositories.Characters, DomainKeys.Character( character.Id ), character ) );
var events = new RecordingCharacterChangedHandler();
var service = new AggregateMutationService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>(),
new PostCommitEventBus<CharacterChangedEvent>( new[]
{
new EventHandlerRegistration<CharacterChangedEvent>( "record", events )
} ) );
var failedPlan = service.PrepareTouchLastPlayed(
account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 1 ) );
var failedUnit = environment.Repositories.Provider.BeginUnitOfWork();
Assert.IsTrue( service.StageTouchLastPlayed( failedUnit, failedPlan.Value ).Succeeded );
environment.Provider.FailNextCommit();
var failedCommit = await failedUnit.CommitAsync();
await failedUnit.DisposeAsync();
Assert.IsFalse( failedCommit.Succeeded );
Assert.IsEmpty( events.Events );
Assert.AreEqual( DateTimeOffset.UnixEpoch, environment.Repositories.Characters.Find(
DomainKeys.Character( character.Id ) )!.Value.LastPlayedAt );
var conflictedPlan = service.PrepareTouchLastPlayed(
account, character.Id, DateTimeOffset.UnixEpoch.AddMinutes( 2 ) );
var staleUnit = environment.Repositories.Provider.BeginUnitOfWork();
Assert.IsTrue( service.StageTouchLastPlayed( staleUnit, conflictedPlan.Value ).Succeeded );
var current = environment.Repositories.Characters.Find( DomainKeys.Character( character.Id ) )!;
var competingUnit = environment.Repositories.Provider.BeginUnitOfWork();
var competingEditor = competingUnit.Edit( environment.Repositories.Characters, current )!;
competingEditor.Replace( competingEditor.Value with
{
LastPlayedAt = DateTimeOffset.UnixEpoch.AddMinutes( 3 )
} );
competingUnit.Save( competingEditor );
var competingCommit = await competingUnit.CommitAsync();
await competingUnit.DisposeAsync();
Assert.IsTrue( competingCommit.Succeeded, competingCommit.Error?.Message );
var conflict = await staleUnit.CommitAsync();
await staleUnit.DisposeAsync();
Assert.IsFalse( conflict.Succeeded );
Assert.AreEqual( PersistenceErrorCode.RevisionConflict, conflict.Error!.Code );
Assert.IsEmpty( events.Events );
// Completion validates from the receipt alone; a receipt that does not contain the
// prepared revision is rejected without publishing.
var wrongCompletion = service.CompleteTouchLastPlayed(
conflictedPlan.Value,
new CommitReceipt( competingCommit.Value!.Sequence, Array.Empty<CommittedDocumentVersion>() ) );
Assert.AreEqual( ErrorCode.InvariantViolation, wrongCompletion.Error!.Code );
Assert.IsEmpty( events.Events );
}
[TestMethod]
public async Task AggregateMutationFailuresLeaveTimestampBanBalanceStateAndTraitsUntouched()
{
await using var environment = await ApplicationServiceTestEnvironment.CreateAsync();
var account = new AccountId(9102);
var character = ApplicationServiceTestEnvironment.Character(account, 0) with
{
Balance = 10,
LastPlayedAt = DateTimeOffset.UnixEpoch.AddHours(2)
};
var item = ApplicationServiceTestEnvironment.Item() with
{
Traits = new Dictionary<string, TypedPayload>(StringComparer.Ordinal)
{
["state"] = ApplicationServiceTestEnvironment.StatePayload("old")
}
};
await environment.SeedAsync(unitOfWork =>
{
unitOfWork.Create(environment.Repositories.Characters, DomainKeys.Character(character.Id), character);
unitOfWork.Create(environment.Repositories.Items, DomainKeys.Item(item.Id), item);
});
var service = new AggregateMutationService(
environment.Repositories,
environment.Schema,
ApplicationServiceTestEnvironment.AllowPolicy<CharacterMutationContext>(),
ApplicationServiceTestEnvironment.AllowPolicy<ItemTraitMutationContext>());
var oldTimestamp = await service.TouchLastPlayedAsync(
account, character.Id, DateTimeOffset.UnixEpoch);
var nonUtcBan = await service.SetBanAsync(
account,
character.Id,
true,
new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.FromHours(1)));
var insufficientFunds = await service.DebitAsync(account, character.Id, 11);
var unknownState = await service.ReplaceSchemaStateAsync(
account,
character.Id,
ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.state"));
var unknownTrait = await service.ReplaceTraitAsync(
account,
character.Id,
item.Id,
"state",
ApplicationServiceTestEnvironment.StatePayload(typeId: "unknown.trait"));
environment.Provider.FailNextCommit();
var failedCredit = await service.CreditAsync(account, character.Id, 1);
environment.Provider.FailNextCommit();
var failedTraitCommit = await service.ReplaceTraitAsync(
account,
character.Id,
item.Id,
"state",
ApplicationServiceTestEnvironment.StatePayload("new"));
Assert.AreEqual(ErrorCode.InvalidArgument, oldTimestamp.Error!.Code);
Assert.AreEqual(ErrorCode.InvalidArgument, nonUtcBan.Error!.Code);
Assert.AreEqual(ErrorCode.Conflict, insufficientFunds.Error!.Code);
Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownState.Error!.Code);
Assert.AreEqual(ErrorCode.PersistedTypeInvalid, unknownTrait.Error!.Code);
Assert.AreEqual(ErrorCode.InternalError, failedCredit.Error!.Code);
Assert.AreEqual(ErrorCode.InternalError, failedTraitCommit.Error!.Code);
var storedCharacter = environment.Repositories.Characters.Find(DomainKeys.Character(character.Id))!.Value;
var storedItem = environment.Repositories.Items.Find(DomainKeys.Item(item.Id))!.Value;
Assert.AreEqual(character.LastPlayedAt, storedCharacter.LastPlayedAt);
Assert.IsFalse(storedCharacter.IsBanned);
Assert.IsNull(storedCharacter.BanExpiresAt);
Assert.AreEqual(10L, storedCharacter.Balance);
Assert.AreEqual("citizen", storedCharacter.SchemaState.Data.GetProperty("name").GetString());
Assert.AreEqual("old", storedItem.Traits["state"].Data.GetProperty("name").GetString());
}
private sealed class RecordingCharacterChangedHandler : IEventHandler<CharacterChangedEvent>
{
public List<CharacterChangedEvent> Events { get; } = new();
public void Handle( CharacterChangedEvent @event ) => Events.Add( @event );
}
}
using Hexagon.V2.Application;
using Hexagon.V2.Domain;
namespace Hexagon.V2.Tests.Application;
[TestClass]
public sealed class SceneIdentityValidatorTests
{
[TestMethod]
public void ProvenanceClassifier_TrustsOnlyCapturedNonNetworkComponents()
{
var authored = Guid.NewGuid();
var laterRuntime = Guid.NewGuid();
var classifier = new SceneIdentityProvenanceClassifier();
Assert.AreEqual(
SceneIdentityProvenance.RuntimeOrNetwork,
classifier.Classify( authored, hasActiveNetworkRoot: false ) );
classifier.CaptureAuthoredSnapshot( new[] { authored } );
Assert.IsTrue( classifier.HasAuthoredSnapshot );
Assert.AreEqual( 1, classifier.AuthoredComponentCount );
Assert.AreEqual(
SceneIdentityProvenance.EditorAuthored,
classifier.Classify( authored, hasActiveNetworkRoot: false ) );
Assert.AreEqual(
SceneIdentityProvenance.RuntimeOrNetwork,
classifier.Classify( authored, hasActiveNetworkRoot: true ) );
Assert.AreEqual(
SceneIdentityProvenance.RuntimeOrNetwork,
classifier.Classify( laterRuntime, hasActiveNetworkRoot: false ) );
}
[TestMethod]
public void ProvenanceClassifier_NewSceneSnapshotReplacesPriorAuthority()
{
var firstScene = Guid.NewGuid();
var secondScene = Guid.NewGuid();
var classifier = new SceneIdentityProvenanceClassifier();
classifier.CaptureAuthoredSnapshot( new[] { firstScene } );
classifier.CaptureAuthoredSnapshot( new[] { secondScene } );
Assert.AreEqual(
SceneIdentityProvenance.RuntimeOrNetwork,
classifier.Classify( firstScene, hasActiveNetworkRoot: false ) );
Assert.AreEqual(
SceneIdentityProvenance.EditorAuthored,
classifier.Classify( secondScene, hasActiveNetworkRoot: false ) );
}
[TestMethod]
public void EditorRepair_FillsMissingAndRepairsOnlyLaterDuplicate()
{
var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
var generated = new Queue<SceneEntityId>( new[]
{
new SceneEntityId( Guid.Parse( "22222222-2222-2222-2222-222222222222" ) ),
new SceneEntityId( Guid.Parse( "33333333-3333-3333-3333-333333333333" ) )
} );
var result = SceneIdentityValidator.RepairForEditor( new[]
{
new SceneIdentityCandidate( "a", duplicate ),
new SceneIdentityCandidate( "b", duplicate ),
new SceneIdentityCandidate( "c", null )
}, () => generated.Dequeue() );
Assert.IsFalse( result[0].Repaired );
Assert.IsTrue( result[1].Repaired );
Assert.IsTrue( result[2].Repaired );
Assert.AreNotEqual( result[0].EffectiveId, result[1].EffectiveId );
}
[TestMethod]
public void RuntimeValidation_DisablesEveryAmbiguousOrBlankEntity()
{
var duplicate = new SceneEntityId( Guid.Parse( "11111111-1111-1111-1111-111111111111" ) );
var result = SceneIdentityValidator.ValidateRuntime( new[]
{
new SceneIdentityCandidate( "a", duplicate ),
new SceneIdentityCandidate( "b", duplicate ),
new SceneIdentityCandidate( "c", null )
} );
Assert.IsTrue( result.All( value => !value.Enabled && value.FatalDiagnostic is not null ) );
}
[TestMethod]
public void PersistentIndexEnumeratesTenThousandCandidatesOnceAndProvidesConstantTimeLookups()
{
var ids = Enumerable.Range( 0, 10_000 ).Select( _ => SceneEntityId.New() ).ToArray();
var enumerated = 0;
IEnumerable<SceneIdentityCandidate> Candidates()
{
for ( var index = 0; index < ids.Length; index++ )
{
enumerated++;
yield return new SceneIdentityCandidate( $"entity/{index}", ids[index] );
}
}
var index = PersistentSceneIdentityIndex.Build( Candidates() );
Assert.AreEqual( 10_000, enumerated );
Assert.AreEqual( 10_000, index.Count );
for ( var candidate = 0; candidate < ids.Length; candidate++ )
{
Assert.IsTrue( index.TryResolveId( ids[candidate], out var byId ) );
Assert.IsTrue( index.TryResolvePath( $"entity/{candidate}", out var byPath ) );
Assert.AreSame( byId, byPath );
}
}
[TestMethod]
public void RuntimeNetworkDuplicateCannotPoisonEditorAuthoredIdentity()
{
var duplicate = SceneEntityId.New();
var initial = PersistentSceneIdentityIndex.Build( new[]
{
new SceneIdentityCandidate( "first", duplicate )
} );
Assert.IsTrue( initial.TryResolveId( duplicate, out _ ) );
var rebuilt = PersistentSceneIdentityIndex.Build( new[]
{
new SceneIdentityCandidate( "first", duplicate ),
new SceneIdentityCandidate(
"first", duplicate, SceneIdentityProvenance.RuntimeOrNetwork )
} );
Assert.IsTrue( rebuilt.TryResolveId( duplicate, out var resolved ) );
Assert.AreEqual( "first", resolved.StablePath );
Assert.IsTrue( rebuilt.Resolutions[0].Enabled );
Assert.IsFalse( rebuilt.Resolutions[1].Enabled );
Assert.AreEqual( SceneIdentityProvenance.RuntimeOrNetwork, rebuilt.Resolutions[1].Provenance );
Assert.IsNull( rebuilt.Resolutions[1].EffectiveId );
StringAssert.Contains( rebuilt.Resolutions[1].FatalDiagnostic, "runtime/network root" );
}
[TestMethod]
public void TwoEditorAuthoredDuplicatesStillFailClosed()
{
var duplicate = SceneEntityId.New();
var rebuilt = PersistentSceneIdentityIndex.Build( new[]
{
new SceneIdentityCandidate( "first", duplicate ),
new SceneIdentityCandidate( "second", duplicate )
} );
Assert.IsFalse( rebuilt.TryResolveId( duplicate, out _ ) );
Assert.IsTrue( rebuilt.Resolutions.All( resolution => !resolution.Enabled ) );
}
}
using System.IO;
namespace Hexagon.V2.Tests.Foundation;
[TestClass]
public sealed class SandboxCompatibilityTests
{
private static readonly string[] ForbiddenSourceTokens =
[
".ConfigureAwait(",
"Volatile.",
"ReaderWriterLockSlim",
"CryptographicOperations.FixedTimeEquals",
".IsInterface",
".IsByRef",
".IsPointer",
".IsInstanceOfType",
"ExceptionDispatchInfo",
"System.Reflection",
"RuntimeHelpers.",
"Activator.CreateInstance",
".GetProperties(",
".GetFields(",
".MakeGenericType(",
".GetGenericArguments(",
"await using",
"Task.WhenAll(",
"TaskScheduler",
"Environment.ProcessId"
];
private static readonly (string Name, string Pattern)[] ForbiddenRawIoSignatures =
[
("File static API", @"\bFile\s*\."),
("Directory static API", @"\bDirectory\s*\."),
("Path.GetFullPath", @"\bPath\s*\.\s*GetFullPath\s*\("),
("FileStream", @"\bFileStream\b"),
("FileInfo", @"\bFileInfo\b"),
("DirectoryInfo", @"\bDirectoryInfo\b"),
("FileMode", @"\bFileMode\b"),
("FileAccess", @"\bFileAccess\b"),
("FileShare", @"\bFileShare\b"),
("FileOptions", @"\bFileOptions\b"),
("SearchOption", @"\bSearchOption\b")
];
[TestMethod]
public void SandboxIndependentV2CodeAvoidsKnownWhitelistViolations()
{
var root = FindHexagonRoot();
var sourceRoots = new[] { Path.Combine( root, "Code", "V2" ) }
.Where( Directory.Exists )
.ToArray();
var violations = new List<string>();
foreach ( var path in sourceRoots.SelectMany( sourceRoot =>
Directory.GetFiles( sourceRoot, "*.cs", SearchOption.AllDirectories ) )
.Where( path => !path.Contains(
$"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}",
StringComparison.OrdinalIgnoreCase ) ) )
{
var source = File.ReadAllText( path );
if ( System.Text.RegularExpressions.Regex.IsMatch(
source,
@"^\s*(?:private|protected|internal|public)\s+(?:static\s+)?volatile\s+",
System.Text.RegularExpressions.RegexOptions.Multiline ) )
violations.Add(
$"{Path.GetRelativePath( root, path )} declares a volatile field (lowers to forbidden IsVolatile)" );
if ( System.Text.RegularExpressions.Regex.IsMatch(
source,
@"finally\s*\{[^{}]*\bawait\b",
System.Text.RegularExpressions.RegexOptions.Singleline ) )
violations.Add(
$"{Path.GetRelativePath( root, path )} awaits inside a finally block (lowers to forbidden ExceptionDispatchInfo)" );
if ( System.Text.RegularExpressions.Regex.IsMatch(
source,
@"catch(?:\s*\([^)]*\))?\s*\{[^{}]*\bawait\b[^{}]*\bthrow\s*;",
System.Text.RegularExpressions.RegexOptions.Singleline ) )
violations.Add(
$"{Path.GetRelativePath( root, path )} awaits before a bare catch rethrow (lowers to forbidden ExceptionDispatchInfo)" );
var lines = File.ReadAllLines( path );
for ( var index = 0; index < lines.Length; index++ )
{
foreach ( var token in ForbiddenSourceTokens )
{
if ( lines[index].Contains( token, StringComparison.Ordinal ) )
violations.Add( $"{Path.GetRelativePath( root, path )}:{index + 1} contains '{token}'" );
}
foreach ( var signature in FindForbiddenRawIoSignatures( lines[index] ) )
violations.Add(
$"{Path.GetRelativePath( root, path )}:{index + 1} uses forbidden raw I/O signature '{signature}'" );
}
}
Assert.HasCount(
0,
violations,
"Known s&box whitelist violations were found:" + Environment.NewLine + string.Join( Environment.NewLine, violations ) );
}
[TestMethod]
public void RawIoSignatureGuardCoversOriginalSb1000SurfaceWithoutFalsePositives()
{
// Keep fixtures split so the production-source scanner can never match this test file if its roots expand.
var separator = string.Empty;
var forbiddenFixtures = new[]
{
"File" + separator + ".Exists( path )",
"Path" + separator + ".GetFullPath( path )",
"File" + separator + "Stream? lease",
"File" + separator + "Info info",
"Directory" + separator + "Info directory",
"File" + separator + "Mode.OpenOrCreate",
"File" + separator + "Access.ReadWrite",
"File" + separator + "Share.None",
"File" + separator + "Options.WriteThrough",
"Search" + separator + "Option.AllDirectories",
"Directory" + separator + " . EnumerateFiles( root )"
};
foreach ( var fixture in forbiddenFixtures )
Assert.IsNotEmpty(
FindForbiddenRawIoSignatures( fixture ),
$"Expected raw I/O fixture to be rejected: {fixture}" );
var allowedFixtures = new[]
{
"var path = issue.Path;",
"var fileName = record.FileName;",
"IReadOnlyList<string> directories",
"storage.ExistsAsync( path, cancellationToken )"
};
foreach ( var fixture in allowedFixtures )
Assert.IsEmpty(
FindForbiddenRawIoSignatures( fixture ),
$"Expected non-I/O fixture to remain allowed: {fixture}" );
}
private static string[] FindForbiddenRawIoSignatures( string sourceLine ) => ForbiddenRawIoSignatures
.Where( signature => System.Text.RegularExpressions.Regex.IsMatch(
sourceLine,
signature.Pattern,
System.Text.RegularExpressions.RegexOptions.CultureInvariant ) )
.Select( signature => signature.Name )
.ToArray();
private static string FindHexagonRoot()
{
var directory = new DirectoryInfo( AppContext.BaseDirectory );
while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
directory = directory.Parent;
Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
return directory.FullName;
}
}
#nullable enable
using System;
using System.IO;
using System.Text.RegularExpressions;
using Hexagon.V2.Networking;
using Hexagon.V2.Tests.Foundation;
namespace Hexagon.V2.Tests.Networking;
/// <summary>
/// Sustained-run guards for the per-connection command budget, and a pin tying the numbers
/// <c>docs/security.md</c> publishes to the constants that enforce them.
/// <para>
/// The movement validator's failure was that a per-tick allowance was never checked as a rate, so prose
/// describing a bound and code granting one every tick could both read as true while disagreeing by
/// fifty times. Every other rate-limited surface therefore gets the same treatment: the published number
/// is read out of the document and asserted against the constant, and the constant is asserted against
/// what a client can actually extract over a run.
/// </para>
/// </summary>
[TestClass]
public sealed class CommandAdmissionSustainedTests
{
private const long Frequency = 1000; // timestamps in milliseconds
private const int Hz = 50;
private const int TickMilliseconds = 1000 / Hz;
private const int Ticks = 10 * Hz; // ten seconds
private const double ElapsedSeconds = 10.0;
[TestMethod]
public void TheSecurityDocumentPublishesTheBudgetThatIsActuallyEnforced()
{
// docs/security.md: "a weighted per-connection token bucket before payload construction:
// 16-unit burst, 8 units/second refill, and at most 16 active requests."
// Reading the numbers out of the document makes the prose a claim this suite owns: editing
// either side without the other fails here rather than shipping a document that describes a
// system nobody built.
var document = File.ReadAllText( Path.Combine( RepositoryRoot(), "docs", "security.md" ) );
var match = Regex.Match( document,
@"(?<burst>\d+)-unit burst,\s*(?<refill>\d+)\s*units?/second refill,\s*and at most\s*(?<active>\d+)\s*active requests" );
Assert.IsTrue( match.Success,
"docs/security.md no longer states the command budget in the form this guard reads. " +
"Update the guard deliberately rather than letting the published numbers go unchecked." );
Assert.AreEqual( CommandAdmissionController.BurstUnits, int.Parse( match.Groups["burst"].Value ),
"Documented burst does not match the enforced constant." );
Assert.AreEqual( CommandAdmissionController.RefillUnitsPerSecond, int.Parse( match.Groups["refill"].Value ),
"Documented refill rate does not match the enforced constant." );
Assert.AreEqual( CommandAdmissionController.MaximumActiveRequests, int.Parse( match.Groups["active"].Value ),
"Documented active-request cap does not match the enforced constant." );
}
[TestMethod]
public void SustainedAdmissionConvergesOnTheRefillRateNotTheBurst()
{
// The burst is a one-off; the refill rate is the ceiling. A bucket that re-granted its burst
// the way the movement validator re-granted its skin would admit 800 units here, not 96.
var controller = new CommandAdmissionController( Frequency );
var admittedCount = 0;
var run = SustainedEnvelope.Measure( Ticks, 1.0 / Hz, tick =>
{
var timestamp = (long)tick * TickMilliseconds;
var requestId = new CommandRequestId( Guid.NewGuid() );
var result = controller.TryBegin( requestId, cost: 1, timestamp );
if ( !result.Accepted ) return SustainedEnvelope.StepOutcome.Rejected;
controller.Finish( requestId ); // keep the active-request bound out of the way
admittedCount++;
return SustainedEnvelope.StepOutcome.Allowed( 1 );
} );
var bound = (CommandAdmissionController.RefillUnitsPerSecond * ElapsedSeconds)
+ CommandAdmissionController.BurstUnits;
Assert.IsLessThanOrEqualTo( bound, run.TotalWork, SustainedEnvelope.Describe(
$"Sustained command admission over 10s ({run.WorkPerSecond:F1}/s)",
run.TotalWork, bound, " units" ) );
Assert.IsGreaterThan( 0, admittedCount,
"The run admitted nothing, so the bound above proves nothing about the bucket." );
}
[TestMethod]
public void RejectedAttemptsAreNotRefundedAcrossASustainedRun()
{
// docs/security.md: "Rejected, duplicate, and malformed attempts are not refunded." A refund on
// rejection would make the bucket a no-op under exactly the load it exists to bound.
var controller = new CommandAdmissionController( Frequency );
var duplicate = new CommandRequestId( Guid.NewGuid() );
// Drain the burst with duplicates, which are charged and then refused.
for ( var i = 0; i < CommandAdmissionController.BurstUnits; i++ )
controller.TryBegin( duplicate, cost: 1, 0 );
Assert.IsLessThanOrEqualTo( 0.001, controller.AvailableUnits,
$"Duplicates were refunded: {controller.AvailableUnits:F2} units remain after draining the burst." );
}
[TestMethod]
public void ACostlierCommandDrainsTheBudgetProportionally()
{
// The bucket is weighted, so an expensive command must consume its weight rather than one slot.
var controller = new CommandAdmissionController( Frequency );
var admitted = 0;
for ( var i = 0; i < 100; i++ )
{
var requestId = new CommandRequestId( Guid.NewGuid() );
if ( !controller.TryBegin( requestId, CommandAdmissionController.BurstUnits, 0 ).Accepted ) break;
controller.Finish( requestId );
admitted++;
}
Assert.AreEqual( 1, admitted,
"A full-burst-cost command should be admissible exactly once from a full bucket." );
}
private static string RepositoryRoot()
{
var directory = new DirectoryInfo( AppContext.BaseDirectory );
while ( directory is not null && !File.Exists( Path.Combine( directory.FullName, "hexagon.sbproj" ) ) )
directory = directory.Parent;
Assert.IsNotNull( directory, "Could not locate the Hexagon repository root." );
return directory!.FullName;
}
}
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Tests.Networking;
[TestClass]
public sealed class CommandDispatchOrchestratorTests
{
[TestMethod]
public void UnauthenticatedCallerIsRejectedBeforeAnyChargeOrResolution()
{
var host = new FakeDispatchHost { Authenticated = false };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.IsEmpty( host.Calls, "Unauthenticated traffic must never reach the per-connection bucket." );
Assert.HasCount( 1, host.Sent );
Assert.AreEqual( "caller", host.Sent[0].Target );
Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void ChargeHappensBeforeResolutionAndCarriesTheDeclaredCost()
{
var host = new FakeDispatchHost();
CommandDispatchOrchestrator.Dispatch( host, Header(), 3, static () => new RequestCharacterListCommand() );
Assert.AreEqual( "charge:3", host.Calls[0], "The caller is charged before its scope is inspected." );
Assert.AreEqual( "resolve:initial", host.Calls[1] );
}
[TestMethod]
public void RateLimitedAdmissionCarriesRetryAfterAndSkipsResolution()
{
var host = new FakeDispatchHost
{
Admission = CommandAdmissionResult.Reject(
CommandAdmissionFailure.RateLimited, TimeSpan.FromSeconds( 2 ) )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 4, static () => new RequestCharacterListCommand() );
CollectionAssert.AreEqual( new[] { "charge:4" }, host.Calls );
Assert.HasCount( 1, host.Sent );
Assert.AreEqual( ErrorCode.RateLimited, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void DuplicateRequestIdsConflict()
{
var host = new FakeDispatchHost
{
Admission = CommandAdmissionResult.Reject( CommandAdmissionFailure.Duplicate )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void ResolutionFailureIsForwardedAndTheAcceptedChargeIsClosed()
{
var host = new FakeDispatchHost
{
Resolution = OperationResult<string>.Failure( ErrorCode.Unauthorized, "scope mismatch" )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
CollectionAssert.AreEqual(
new[] { "charge:1", "resolve:initial", "finish-rejected" },
host.Calls );
Assert.AreEqual( "scope mismatch", host.Sent[0].Result.Error!.Message );
}
[TestMethod]
public void EmptyRequestIdCompletesWithInvalidArgument()
{
var host = new FakeDispatchHost();
// Wire deserialization can materialize a default header; the constructor itself
// rejects empty request ids, so the guard is only reachable through default structs.
var header = default( ClientCommandHeader );
CommandDispatchOrchestrator.Dispatch( host, header, 1, static () => new RequestCharacterListCommand() );
CollectionAssert.Contains( host.Calls, "complete" );
Assert.HasCount( 1, host.Sent );
Assert.AreEqual( "actor", host.Sent[0].Target );
Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void ThrowingCommandFactoryFailsClosedWithoutStartingAHostOperation()
{
var host = new FakeDispatchHost();
CommandDispatchOrchestrator.Dispatch( host, Header(), 1,
static () => throw new FormatException( "bad wire payload" ) );
Assert.HasCount( 1, host.MalformedPayloads );
CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
Assert.AreEqual( ErrorCode.InvalidArgument, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void OversizedPayloadIsRejectedBeforeStableResolutionAndExecution()
{
var host = new FakeDispatchHost();
CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
static () => new SendChatCommand( "ic", new string( 'a', 1_000_000 ) ) );
CollectionAssert.DoesNotContain( host.Calls, "resolve:stable" );
CollectionAssert.DoesNotContain( host.Calls, "host-operation" );
Assert.HasCount( 1, host.Sent );
Assert.IsFalse( host.Sent[0].Result.Succeeded );
}
[TestMethod]
public void StableCharacterCommandsAreReResolvedAfterPayloadValidation()
{
var host = new FakeDispatchHost();
CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
static () => new SendChatCommand( "ic", "hello" ) );
CollectionAssert.AreEqual(
new[]
{
"charge:2", "resolve:initial", "resolve:stable",
"host-operation", "lease-check", "execute", "complete"
},
host.Calls );
Assert.AreEqual( "stable-actor", host.Sent[0].Target,
"Execution and completion must use the re-resolved stable actor." );
}
[TestMethod]
public void StableResolutionFailureCompletesWithoutExecuting()
{
var host = new FakeDispatchHost
{
StableResolution = OperationResult<string>.Failure(
ErrorCode.Unauthorized, "character changed" )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 2,
static () => new SendChatCommand( "ic", "hello" ) );
CollectionAssert.DoesNotContain( host.Calls, "execute" );
Assert.AreEqual( "character changed", host.Sent[0].Result.Error!.Message );
}
[TestMethod]
public void CharacterLifecycleCommandsSkipTheStableCharacterResolution()
{
foreach ( var factory in new Func<ClientCommand>[]
{
static () => new RequestCharacterListCommand(),
static () => new LoadCharacterCommand( CharacterId.New() ),
static () => new DeleteCharacterCommand( CharacterId.New() ),
static () => new UnloadCharacterCommand()
} )
{
var host = new FakeDispatchHost();
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, factory );
CollectionAssert.DoesNotContain( host.Calls, "resolve:stable",
$"{factory().GetType().Name} must dispatch without an active character." );
Assert.AreEqual( "actor", host.Sent[0].Target );
}
}
[TestMethod]
public void HostOperationRefusalCompletesWithDrainingConflict()
{
var host = new FakeDispatchHost { AcceptHostOperation = false };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.AreEqual( ErrorCode.Conflict, host.Sent[0].Result.Error!.Code );
StringAssert.Contains( host.StartedOperationName!, "command:" );
}
[TestMethod]
public void StaleLeaseInsideTheHostOperationRejectsBeforeExecution()
{
var host = new FakeDispatchHost { LeaseCurrent = false };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
CollectionAssert.DoesNotContain( host.Calls, "execute" );
Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void MissingHostApplicationCompletesWithInternalError()
{
var host = new FakeDispatchHost { ExecutionAvailable = false };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
CollectionAssert.DoesNotContain( host.Calls, "execute" );
Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void ExecutionFaultBecomesInternalError()
{
var host = new FakeDispatchHost
{
Execution = static () => throw new InvalidOperationException( "handler exploded" )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.HasCount( 1, host.ExecutionFaults );
Assert.AreEqual( ErrorCode.InternalError, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void ExecutionFaultUnderCancellationBecomesUnauthorized()
{
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var host = new FakeDispatchHost
{
Cancellation = cancellation.Token,
Execution = static () => throw new OperationCanceledException()
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.AreEqual( ErrorCode.Unauthorized, host.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void DisconnectedCompletionSendsNothing()
{
var host = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Disconnected };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.IsEmpty( host.Sent );
}
[TestMethod]
public void StaleCompletionKeepsASuccessfulExecutionResultButRejectsAFailedOne()
{
var success = new FakeDispatchHost { CompletionStatus = CommandCompletionStatus.Stale };
CommandDispatchOrchestrator.Dispatch( success, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.IsTrue( success.Sent[0].Result.Succeeded,
"A successful application result stays authoritative even under a stale lease." );
var failure = new FakeDispatchHost
{
CompletionStatus = CommandCompletionStatus.Stale,
Execution = static () => ValueTask.FromResult(
OperationResult.Failure( ErrorCode.NotFound, "gone" ) )
};
CommandDispatchOrchestrator.Dispatch( failure, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.AreEqual( ErrorCode.Unauthorized, failure.Sent[0].Result.Error!.Code );
}
[TestMethod]
public void SuccessfulExecutionForwardsTheApplicationResult()
{
var host = new FakeDispatchHost
{
Execution = static () => ValueTask.FromResult(
OperationResult.Failure( ErrorCode.PolicyDenied, "application said no" ) )
};
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.AreEqual( ErrorCode.PolicyDenied, host.Sent[0].Result.Error!.Code );
Assert.AreEqual( "application said no", host.Sent[0].Result.Error!.Message );
}
[TestMethod]
public void ThrowingCompletionHookIsObservedWithoutEscaping()
{
var host = new FakeDispatchHost { CompleteThrows = true };
CommandDispatchOrchestrator.Dispatch( host, Header(), 1, static () => new RequestCharacterListCommand() );
Assert.HasCount( 1, host.CompletionFaults );
Assert.IsEmpty( host.Sent );
}
[TestMethod]
public void CommandCostTableIsPinned()
{
var table = new Dictionary<string, int>( StringComparer.Ordinal )
{
[nameof( ClientCommandCosts.CharacterList )] = ClientCommandCosts.CharacterList,
[nameof( ClientCommandCosts.CreateCharacter )] = ClientCommandCosts.CreateCharacter,
[nameof( ClientCommandCosts.LoadCharacter )] = ClientCommandCosts.LoadCharacter,
[nameof( ClientCommandCosts.DeleteCharacter )] = ClientCommandCosts.DeleteCharacter,
[nameof( ClientCommandCosts.UnloadCharacter )] = ClientCommandCosts.UnloadCharacter,
[nameof( ClientCommandCosts.MoveItem )] = ClientCommandCosts.MoveItem,
[nameof( ClientCommandCosts.ItemAction )] = ClientCommandCosts.ItemAction,
[nameof( ClientCommandCosts.DropItem )] = ClientCommandCosts.DropItem,
[nameof( ClientCommandCosts.PickupItem )] = ClientCommandCosts.PickupItem,
[nameof( ClientCommandCosts.Chat )] = ClientCommandCosts.Chat,
[nameof( ClientCommandCosts.CancelAction )] = ClientCommandCosts.CancelAction,
[nameof( ClientCommandCosts.BeginInteraction )] = ClientCommandCosts.BeginInteraction,
[nameof( ClientCommandCosts.ContinueInteraction )] = ClientCommandCosts.ContinueInteraction,
[nameof( ClientCommandCosts.CloseInteraction )] = ClientCommandCosts.CloseInteraction,
[nameof( ClientCommandCosts.SchemaCommandFallback )] = ClientCommandCosts.SchemaCommandFallback
};
var expected = new Dictionary<string, int>( StringComparer.Ordinal )
{
["CharacterList"] = 1,
["CreateCharacter"] = 4,
["LoadCharacter"] = 4,
["DeleteCharacter"] = 4,
["UnloadCharacter"] = 1,
["MoveItem"] = 4,
["ItemAction"] = 4,
["DropItem"] = 4,
["PickupItem"] = 4,
["Chat"] = 2,
["CancelAction"] = 1,
["BeginInteraction"] = 2,
["ContinueInteraction"] = 1,
["CloseInteraction"] = 1,
["SchemaCommandFallback"] = 8
};
CollectionAssert.AreEquivalent( expected, table );
}
[TestMethod]
public void OnlyCharacterLifecycleCommandsAreExemptFromTheStableCharacterRequirement()
{
Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new RequestCharacterListCommand() ) );
Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
new LoadCharacterCommand( CharacterId.New() ) ) );
Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter(
new DeleteCharacterCommand( CharacterId.New() ) ) );
Assert.IsFalse( CommandDispatchOrchestrator.RequiresStableCharacter( new UnloadCharacterCommand() ) );
Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter( new SendChatCommand( "ic", "hi" ) ) );
Assert.IsTrue( CommandDispatchOrchestrator.RequiresStableCharacter(
new CloseInteractionCommand( InteractionSessionId.New() ) ) );
}
private static ClientSessionScope Scope() =>
new( ClientSessionNonce.New(), ConnectionEpoch.New() );
private static ClientCommandHeader Header() =>
new( Scope(), new CommandRequestId( Guid.NewGuid() ) );
private sealed class FakeDispatchHost : ICommandDispatchHost<string>
{
public List<string> Calls { get; } = new();
public bool Authenticated { get; set; } = true;
public bool ExecutionAvailable { get; set; } = true;
public CommandAdmissionResult Admission { get; set; } = CommandAdmissionResult.Success();
public OperationResult<string> Resolution { get; set; } = OperationResult<string>.Success( "actor" );
public OperationResult<string> StableResolution { get; set; } = OperationResult<string>.Success( "stable-actor" );
public bool LeaseCurrent { get; set; } = true;
public bool AcceptHostOperation { get; set; } = true;
public bool CompleteThrows { get; set; }
public Func<ValueTask<OperationResult>>? Execution { get; set; }
public CancellationToken Cancellation { get; set; }
public CommandCompletionStatus CompletionStatus { get; set; } = CommandCompletionStatus.Current;
public List<(string Target, OperationResult Result)> Sent { get; } = new();
public List<Exception> MalformedPayloads { get; } = new();
public List<Exception> ExecutionFaults { get; } = new();
public List<Exception> CompletionFaults { get; } = new();
public string? StartedOperationName { get; private set; }
public bool CallerIsAuthenticated => Authenticated;
public bool IsExecutionAvailable => ExecutionAvailable;
public CommandAdmissionResult TryBeginCommand( CommandRequestId requestId, int cost )
{
Calls.Add( $"charge:{cost}" );
return Admission;
}
public OperationResult<string> ResolveActor( ClientSessionScope scope, bool requireStableCharacter )
{
Calls.Add( requireStableCharacter ? "resolve:stable" : "resolve:initial" );
return requireStableCharacter ? StableResolution : Resolution;
}
public void FinishRejectedCommand( CommandRequestId requestId ) => Calls.Add( "finish-rejected" );
public bool IsCommandLeaseCurrent( string actor )
{
Calls.Add( "lease-check" );
return LeaseCurrent;
}
public bool TryStartHostOperation( string name, Func<Task> operation )
{
Calls.Add( "host-operation" );
StartedOperationName = name;
if ( !AcceptHostOperation ) return false;
operation().GetAwaiter().GetResult();
return true;
}
public CancellationToken CommandCancellation( string actor ) => Cancellation;
public ValueTask<OperationResult> ExecuteAsync(
string actor,
ClientCommand command,
CancellationToken cancellationToken )
{
Calls.Add( "execute" );
return Execution?.Invoke() ?? ValueTask.FromResult( OperationResult.Success() );
}
public CommandCompletionStatus CompleteCommand( string actor, CommandRequestId requestId )
{
Calls.Add( "complete" );
if ( CompleteThrows ) throw new InvalidOperationException( "completion registry exploded" );
return CompletionStatus;
}
public void SendResultToCaller( ClientSessionScope scope, CommandRequestId requestId, OperationResult result ) =>
Sent.Add( ("caller", result) );
public void SendResult( string actor, CommandRequestId requestId, OperationResult result ) =>
Sent.Add( (actor, result) );
public void OnMalformedPayload( Exception exception ) => MalformedPayloads.Add( exception );
public void OnExecutionFault( CommandRequestId requestId, Exception exception ) => ExecutionFaults.Add( exception );
public void OnCompletionFault( CommandRequestId requestId, Exception exception ) => CompletionFaults.Add( exception );
}
}
#nullable enable
using System;
using Hexagon.V2.Runtime;
using static Hexagon.V2.Tests.Foundation.SustainedEnvelope.EngineDefaults;
namespace Hexagon.V2.Tests.Runtime;
/// <summary>
/// Guards for the windowed movement audit.
/// <para>
/// The host observes an INTERPOLATED proxy transform, not what the client reported, so its per-tick
/// deltas are artifacts of network buffering. The audit's soundness rests on one property —
/// <b>summing deltas over a window is invariant to how the travel is chunked</b> — and
/// <see cref="AnIdenticalPathIsMeasuredTheSameHoweverItIsChunked"/> is the test that pins it. Everything
/// else here is a bound stated over a window; nothing is stated per tick, because per tick the signal
/// carries no truth.
/// </para>
/// </summary>
[TestClass]
public sealed class HexMovementValidatorTests
{
private static readonly HexMovementEnvelope Envelope = HexMovementEnvelope.Default;
private static MovementSample At( float x, float y, float z ) => new( x, y, z );
/// <summary>
/// Walks a straight horizontal path of <paramref name="totalDistance"/> over
/// <paramref name="seconds"/>, delivered in <paramref name="samples"/> chunks, and returns the
/// decision of the window that closes.
/// </summary>
private static HexMovementValidator.Decision WalkHorizontally(
float totalDistance, double seconds, int samples, bool frozen = false )
{
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var last = default( HexMovementValidator.Decision );
for ( var i = 1; i <= samples; i++ )
{
var t = seconds * i / samples;
var x = totalDistance * i / samples;
(last, audit) = HexMovementValidator.Observe(
audit, At( (float)x, 0, 0 ), t, RunSpeed, JumpSpeed, frozen, Envelope );
}
return last;
}
[TestMethod]
public void AnIdenticalPathIsMeasuredTheSameHoweverItIsChunked()
{
// THE property the whole design rests on. The interpolation buffer decides how travel is sliced
// — smoothly, in bursts of three ticks, or irregularly — and the audit must be blind to that.
// Per-tick checking is not: measured live, an honest sprint arrived as 19.2-unit bursts against
// a ~15-unit per-tick budget and was refused twelve times.
var smooth = WalkHorizontally( 300f, 1.0, samples: 60 );
var bursty = WalkHorizontally( 300f, 1.0, samples: 20 );
var coarse = WalkHorizontally( 300f, 1.0, samples: 5 );
Assert.AreEqual( smooth.HorizontalPath, bursty.HorizontalPath, 0.01f,
"Chunking changed the measured path; the audit is not sampling-invariant." );
Assert.AreEqual( smooth.HorizontalPath, coarse.HorizontalPath, 0.01f,
"Chunking changed the measured path; the audit is not sampling-invariant." );
}
[TestMethod]
public void AnHonestSprintIsAcceptedHoweverItIsSampled()
{
// A player at exactly run speed for a full window, delivered every way the network might.
foreach ( var samples in new[] { 60, 20, 12, 5 } )
{
var decision = WalkHorizontally( RunSpeed, 1.0, samples );
Assert.IsTrue( decision.WindowClosed, $"Window did not close for {samples} samples." );
Assert.IsFalse( decision.Corrected,
$"Honest sprint flagged at {samples} samples/window: " +
$"path {decision.HorizontalPath:F1} vs budget {decision.HorizontalBudget:F1}." );
}
}
[TestMethod]
public void ASpeedCheatIsFlagged()
{
// Twice run speed for a window: 640 units against a 496-unit budget.
var decision = WalkHorizontally( RunSpeed * 2f, 1.0, samples: 60 );
Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, decision.Verdict,
$"path {decision.HorizontalPath:F1} vs budget {decision.HorizontalBudget:F1}" );
}
[TestMethod]
public void SustainedClimbBeyondTheGroundAngleIsFlagged()
{
// Rising far faster than the steepest standable surface would permit for the distance walked.
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var decision = default( HexMovementValidator.Decision );
for ( var i = 1; i <= 60; i++ )
{
// 100 units of horizontal travel, 900 units of rise, in one window.
(decision, audit) = HexMovementValidator.Observe(
audit, At( 100f * i / 60f, 0, 900f * i / 60f ), i / 60.0,
RunSpeed, JumpSpeed, false, Envelope );
}
Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, decision.Verdict,
$"rise {decision.NetRise:F1} vs budget {decision.RiseBudget:F1}" );
}
[TestMethod]
public void AJumpAndLandIsNotAClimb()
{
// Net rise over a window is what "climbing" means. Jumping repeatedly nets nothing, and must
// not accumulate into a violation the way summed positive rise would.
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var decision = default( HexMovementValidator.Decision );
for ( var i = 1; i <= 60; i++ )
{
var t = i / 60.0;
var phase = Math.Sin( t * Math.PI * 4 ); // two full jump arcs in the window
var z = (float)(Math.Max( phase, 0 ) * 56.0); // apex ~ one jump
(decision, audit) = HexMovementValidator.Observe(
audit, At( 200f * i / 60f, 0, z ), t, RunSpeed, JumpSpeed, false, Envelope );
}
Assert.IsFalse( decision.Corrected,
$"Jumping while running was flagged: rise {decision.NetRise:F1} vs {decision.RiseBudget:F1}" );
}
[TestMethod]
public void ATeleportIsRefusedImmediatelyWithoutWaitingForTheWindow()
{
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var (decision, _) = HexMovementValidator.Observe(
audit, At( 5000, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );
Assert.AreEqual( HexMovementValidator.Verdict.Teleport, decision.Verdict );
Assert.IsFalse( decision.WindowClosed, "A teleport is caught on the sample, not at window end." );
}
[TestMethod]
public void AnInterpolationSizedBurstIsNotMistakenForATeleport()
{
// The bursts that broke per-tick checking (~19 units) must be nowhere near the teleport guard.
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var (decision, _) = HexMovementValidator.Observe(
audit, At( 19.2f, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );
Assert.AreEqual( HexMovementValidator.Verdict.Accepted, decision.Verdict );
}
[TestMethod]
public void FrozenPlayersAreHeldToTheDriftAllowance()
{
var moved = WalkHorizontally( 200f, 1.0, samples: 60, frozen: true );
Assert.AreEqual( HexMovementValidator.Verdict.WindowExceeded, moved.Verdict );
var jitter = WalkHorizontally( Envelope.FrozenDriftAllowance * 0.5f, 1.0, samples: 60, frozen: true );
Assert.IsFalse( jitter.Corrected, "Jitter within the frozen allowance must not be flagged." );
}
[TestMethod]
public void CorrectsNonFinitePosition()
{
var audit = MovementAudit.OpenAt( At( 0, 0, 0 ), 0 );
var (decision, _) = HexMovementValidator.Observe(
audit, At( float.NaN, 0, 0 ), 0.016, RunSpeed, JumpSpeed, false, Envelope );
Assert.AreEqual( HexMovementValidator.Verdict.Teleport, decision.Verdict );
}
[TestMethod]
public void AnUnprimedAuditAdoptsTheFirstSampleWithoutFlagging()
{
var (decision, audit) = HexMovementValidator.Observe(
MovementAudit.Unprimed, At( 500, 500, 500 ), 0, RunSpeed, JumpSpeed, false, Envelope );
Assert.IsFalse( decision.Corrected );
Assert.IsTrue( audit.Primed );
Assert.AreEqual( 500f, audit.Anchor.X );
}
[TestMethod]
public void AMalformedEnvelopeIsRejectedRatherThanAuditingNothing()
{
Assert.IsTrue( HexMovementEnvelope.Default.IsWellFormed( out _ ) );
var permissive = HexMovementEnvelope.Default with { HorizontalTolerance = 50f };
Assert.IsFalse( permissive.IsWellFormed( out var error ) );
Assert.IsNotEmpty( error );
var blind = HexMovementEnvelope.Default with { AuditWindowSeconds = 999f };
Assert.IsFalse( blind.IsWellFormed( out _ ) );
}
}
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
using System.Numerics;
using HumanoidRetargeter.Mapping;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Tests.Target;
public class StockBindPoseTests
{
private const string RigJson = """
{ "name": "stock", "bones": [
{ "name": "pelvis", "parent": null, "class": "Animated", "role": "Hips", "local_pos": [0, 100, 0], "local_rot_xyzw": [0, 0, 0, 1], "tail_world": [0, 110, 0] },
{ "name": "helper", "parent": "pelvis", "class": "ConstraintDriven", "local_pos": [1, 0, 0], "local_rot_xyzw": [0, 0, 0, 1] },
{ "name": "root_IK", "parent": null, "class": "IkBaked", "local_pos": [0, 0, 0], "local_rot_xyzw": [0, 0, 0, 1] }
] }
""";
[Fact]
public void RebindingPreservesCuratedHelpersAndAdoptsCompiledRest()
{
var stock = TargetRig.Load(RigJson);
var rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2);
var definitions = stock.Skeleton.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : stock.Skeleton[b.ParentIndex].Name,
b.ParentIndex < 0 ? new XForm(Vector3.Transform(b.RestLocal.Pos, rotation), rotation) : b.RestLocal)).ToList();
definitions.Add(new BoneDefinition("custom_extra", "pelvis", XForm.Identity));
var bind = SkeletonModel.Create(definitions);
var rig = stock.WithBindPose(bind);
Assert.Same(bind, rig.Skeleton);
Assert.True(rig.HelpersAreConstraintDriven);
Assert.Equal(BoneClass.ConstraintDriven, rig.ClassOf(bind.IndexOf("helper")));
Assert.Equal(BoneClass.IkBaked, rig.ClassOf(bind.IndexOf("root_IK")));
Assert.Equal(BoneClass.Animated, rig.ClassOf(bind.IndexOf("custom_extra")));
Assert.Equal(bind.IndexOf("pelvis"), rig.BoneForRole(BoneRole.Hips));
Assert.True(Vector3.Distance(new Vector3(-110, 0, 0), rig.TailWorldOf(bind.IndexOf("pelvis"))!.Value) < .001f);
Assert.Equal(new Vector3(0, 100, 0), stock.Skeleton.RestWorld[stock.Skeleton.IndexOf("pelvis")].Pos);
}
[Fact]
public void MissingMappedBonesFailRatherThanDroppingMotion()
=> Assert.Throws<ArgumentException>(() => TargetRig.Load(RigJson).WithBindPose(
SkeletonModel.Create(new[] { new BoneDefinition("unrelated", null, XForm.Identity) })));
[Fact]
public void PrefabMeshUsesTheSameDmxRootCorrectionAsAnExplicitMesh()
{
var rig = TargetRig.Load(RigJson);
var frames = new[] { rig.Skeleton.Bones.Select(b => b.RestLocal).ToArray() };
var prefab = new RetargetTargetSpec { Rig = rig, VmdlScale = .3937f, CompensateDmxRootYaw = true };
var explicitMesh = new RetargetTargetSpec { Rig = rig, VmdlScale = .3937f, MeshFilePath = "body.fbx" };
var expected = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(frames, explicitMesh);
var actual = Retargeter.TestHook_CompensateEmbeddedMeshRootYaw(frames, prefab);
Assert.Equal(expected[0], actual[0]);
Assert.NotEqual(frames[0][0].Rot, actual[0][0].Rot);
Assert.Equal(rig.Skeleton[0].RestLocal, frames[0][0]);
}
}
using HumanoidRetargeter.Target;
using Xunit;
namespace HumanoidRetargeter.Tests;
public class Kv3ModelDocNumberTests
{
[Theory]
[InlineData(1.0380962e-05)]
[InlineData(-1.2e-12)]
[InlineData(1.25e25)]
[InlineData(double.Epsilon)]
[InlineData(double.MaxValue)]
public void ModelDocNumbersHaveNoExponentAndRoundTripExactly(double value)
{
var doc = Kv3.Parse(VmdlWriter.Kv3Header + "\n{ value = 0.0 }");
((KvObject)doc.Root)["value"] = new KvDouble(value);
var text = Kv3.Serialize(doc);
var number = text[(text.IndexOf("value = ", StringComparison.Ordinal) + 8)..].Trim().TrimEnd('}').Trim();
Assert.DoesNotContain("E", number);
Assert.DoesNotContain("e", number);
Assert.Equal(value, ((KvDouble)((KvObject)Kv3.Parse(text).Root)["value"]).Value);
}
}
using System.Reflection;
using System.Text;
using HumanoidRetargeter.Editor;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.IO;
using Xunit;
namespace SmartPort.Parser.Tests;
public class OptionalMaterialTests
{
const string Outfit = "models/human_clothes/default_outfit/default_outfit_male_lower.vmat";
sealed class FailingLoader(Exception error) : IFileLoader
{
public Resource LoadFile(string file) => throw error;
public Resource LoadFileCompiled(string file) => LoadFile(file);
}
[Fact]
public void MissingOutfitMetadataUsesVertexBufferSemantics()
{
var loader = new FailingLoader(new FileNotFoundException("Missing material", Outfit));
Assert.Empty(ModelExtract.ReadMaterialInputSignature(loader, Outfit).Elements);
}
[Fact]
public void CorruptMetadataAndCancellationAreNotHidden()
{
Assert.Throws<InvalidDataException>(() => ModelExtract.ReadMaterialInputSignature(
new FailingLoader(new InvalidDataException("Corrupt material")), Outfit));
Assert.Throws<OperationCanceledException>(() => ModelExtract.ReadMaterialInputSignature(
new FailingLoader(new OperationCanceledException()), Outfit));
}
[Theory]
[InlineData("required.vmesh")]
[InlineData("required.vphys")]
[InlineData("required.vanim")]
[InlineData("required.vagrp")]
[InlineData("required.vmdl")]
public void RequiredDependenciesStillFailWithTheirFilename(string path)
{
var type = typeof(CompiledAssetRecovery).GetNestedType("Loader", BindingFlags.NonPublic)!;
using var loader = (IDisposable)Activator.CreateInstance(type,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
new object[] { new Dictionary<string, string>(), CancellationToken.None }, null)!;
var error = Assert.Throws<FileNotFoundException>(() => ((IFileLoader)loader).LoadFileCompiled(path));
Assert.Equal(path, error.FileName);
Assert.Contains(path, error.Message);
}
[CompiledFixtureFact]
public void MissingMaterialFilesDoNotBlockRecoveryOrEraseMaterialAssignments()
{
var fixture = Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!;
var paths = Directory.GetFiles(fixture, "*_c", SearchOption.AllDirectories)
.Where(p => !p.EndsWith(".vmat_c", StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => Path.GetRelativePath(fixture, p)[..^2].Replace('\\', '/'), p => p, StringComparer.OrdinalIgnoreCase);
const string model = "models/player/human/frank_mp.vmdl";
var output = Path.Combine(Path.GetTempPath(), "hr-smart-port-no-materials-" + Guid.NewGuid().ToString("N"));
var text = CompiledAssetRecovery.Recover(paths[model], model, output, "recovered", paths, default);
using var resource = new Resource();
resource.Read(paths[model]);
Assert.NotNull(resource.ExternalReferences);
var materials = resource.ExternalReferences.ResourceRefInfoList.Where(r => r.Name.EndsWith(".vmat")).Select(r => r.Name).ToArray();
Assert.NotEmpty(materials);
var files = Directory.GetFiles(output, "*.dmx", SearchOption.AllDirectories);
Assert.Equal(398, files.Length); // Seven meshes and all 391 animation sources.
var meshText = string.Join("\n", files.Where(f => Path.GetFileName(f).StartsWith("frank_mp_")).Select(f => Encoding.UTF8.GetString(File.ReadAllBytes(f))));
foreach (var material in materials) Assert.Contains(material, text + meshText);
}
}
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Sandbox.States.Test;
[TestClass]
public class TestInit
{
[AssemblyInitialize]
public static void ClassInitialize( TestContext context )
{
Sandbox.Application.InitUnitTest();
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}
#nullable enable
using System;
using System.Linq;
using Hexagon.V2.Infrastructure;
using Hexagon.V2.Persistence;
namespace Hexagon.V2.Tests.Infrastructure;
[TestClass]
public sealed class PrefixedPersistenceStorageTests
{
[TestMethod]
public async Task EveryOperationIsMappedBelowTheIsolatedPrefix()
{
var physical = new InMemoryPersistenceStorage();
var isolated = new PrefixedPersistenceStorage( physical, "verification/run-42" );
Assert.IsTrue( await isolated.TryWriteImmutableAsync( "hexagon/v2/schema/format.json", new byte[] { 1, 2 } ) );
Assert.IsTrue( await physical.ExistsAsync( "verification/run-42/hexagon/v2/schema/format.json" ) );
Assert.IsFalse( await physical.ExistsAsync( "hexagon/v2/schema/format.json" ) );
var listed = await isolated.ListAsync( "hexagon/v2/schema" );
Assert.HasCount( 1, listed );
Assert.AreEqual( "hexagon/v2/schema/format.json", listed.Single() );
}
[TestMethod]
public void PrefixRejectsParentAndCurrentDirectorySegments()
{
var storage = new InMemoryPersistenceStorage();
Assert.ThrowsExactly<ArgumentException>( () => new PrefixedPersistenceStorage( storage, "../escape" ) );
Assert.ThrowsExactly<ArgumentException>( () => new PrefixedPersistenceStorage( storage, "safe/./escape" ) );
}
}
#nullable enable
using Hexagon.V2.Domain;
using Hexagon.V2.Kernel;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Tests.Networking;
[TestClass]
public sealed class ClientPayloadLimitsTests
{
[TestMethod]
public void NullReferencePayloadsFailClosedInsteadOfEscapingAdmissionCleanup()
{
Assert.AreEqual(
ErrorCode.InvalidArgument,
ClientPayloadLimits.Validate( new CreateCharacterCommand( null! ) ).Error!.Code );
Assert.AreEqual(
ErrorCode.InvalidArgument,
ClientPayloadLimits.Validate( new BeginInteractionCommand( null! ) ).Error!.Code );
}
[TestMethod]
public void MapEntryAndPerStringBoundariesAreEnforced()
{
var accepted = Enumerable.Range( 0, ClientPayloadLimits.MaximumMapEntries )
.ToDictionary( index => $"k{index}", _ => SnapshotValue.String( "x" ), StringComparer.Ordinal );
var rejected = new Dictionary<string, SnapshotValue>( accepted, StringComparer.Ordinal )
{
["overflow"] = SnapshotValue.String( "x" )
};
Assert.IsTrue( ClientPayloadLimits.Validate( new RunSchemaCommandCommand( "test", accepted ) ).Succeeded );
Assert.AreEqual( ErrorCode.InvalidArgument,
ClientPayloadLimits.Validate( new RunSchemaCommandCommand( "test", rejected ) ).Error!.Code );
Assert.IsTrue( ClientPayloadLimits.Validate(
new SendChatCommand( "ic", new string( 'a', ClientPayloadLimits.MaximumStringCharacters ) ) ).Succeeded );
Assert.IsTrue( ClientPayloadLimits.Validate(
new SendChatCommand( "ic", new string( 'a', ClientPayloadLimits.MaximumStringCharacters + 1 ) ) ).Failed );
}
[TestMethod]
public void InvalidUnicodeAndAggregateUtf8OverflowAreRejected()
{
var invalidUnicode = new SendChatCommand( "ic", "\ud800" );
var fields = new Dictionary<string, SnapshotValue>( StringComparer.Ordinal );
for ( var index = 0; index < 5; index++ )
fields[$"key{index}"] = SnapshotValue.String( new string( '\u0800', 4096 ) );
Assert.IsTrue( ClientPayloadLimits.Validate( invalidUnicode ).Failed );
Assert.IsTrue( ClientPayloadLimits.Validate(
new RunSchemaCommandCommand( "expensive", fields ) ).Failed );
}
[TestMethod]
public void CreationIdentifiersContributeToAggregateUtf8Budget()
{
var fields = Enumerable.Range( 0, 4 ).ToDictionary(
index => $"field{index}",
_ => SnapshotValue.String( new string( 'x', 4090 ) ),
StringComparer.Ordinal );
var input = new CharacterCreationInput(
"name",
"description",
new DefinitionId( new string( 'm', 96 ) ),
new FactionId( new string( 'f', 96 ) ),
new ClassId( new string( 'c', 96 ) ),
fields );
Assert.IsTrue( ClientPayloadLimits.Validate( new CreateCharacterCommand( input ) ).Failed );
}
}
using Hexagon.V2.Runtime;
namespace Hexagon.V2.Tests.Runtime;
[TestClass]
public sealed class SpawnCandidateSelectionTests
{
[TestMethod]
public void NamespacedSpawnsWinOutrightOverMapSpawns()
{
// A mounted map ships its own spawn points; a schema-authored one must take every slot,
// or players are distributed onto arbitrary map geometry.
var candidates = new[]
{
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a1" ), false ),
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a2" ), true ),
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a3" ), false )
};
CollectionAssert.AreEqual( new[] { 1 }, SpawnCandidateSelection.Prefer( candidates ) );
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 0, out var first ) );
Assert.AreEqual( 1, first.CandidateIndex );
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 7, out var later ) );
Assert.AreEqual( 1, later.CandidateIndex, "Every slot must land on the only namespaced spawn." );
}
[TestMethod]
public void WithoutNamespacedSpawnsEveryPointIsUsedInIdOrder()
{
// ID ordering, not scene order, so the same slot resolves to the same point across hosts
// and across restarts.
var candidates = new[]
{
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000c0" ), false ),
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a0" ), false ),
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000b0" ), false )
};
CollectionAssert.AreEqual( new[] { 1, 2, 0 }, SpawnCandidateSelection.Prefer( candidates ) );
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 0, out var zero ) );
Assert.AreEqual( 1, zero.CandidateIndex );
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 1, out var one ) );
Assert.AreEqual( 2, one.CandidateIndex );
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 2, out var two ) );
Assert.AreEqual( 0, two.CandidateIndex );
}
[TestMethod]
public void SlotsBeyondTheSpawnCountOverflowIntoRingsRatherThanStacking()
{
var candidates = new[]
{
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a1" ), true ),
new SpawnCandidate( Guid.Parse( "00000000-0000-0000-0000-0000000000a2" ), true )
};
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 0, out var first ) );
Assert.AreEqual( 0, first.Placement.Ring, "The first layer sits on the point itself." );
// Two points, so slot 2 wraps to the first point on the next layer out.
Assert.IsTrue( SpawnCandidateSelection.TrySelect( candidates, 2, out var wrapped ) );
Assert.AreEqual( 0, wrapped.CandidateIndex );
Assert.IsGreaterThan( 0, wrapped.Placement.Ring );
Assert.IsGreaterThan( 0.0, wrapped.Placement.Radius, "An overflow slot must be offset, not stacked." );
}
[TestMethod]
public void AnEmptySceneSelectsNothingRatherThanThrowing()
{
Assert.IsFalse( SpawnCandidateSelection.TrySelect( Array.Empty<SpawnCandidate>(), 0, out _ ) );
}
}using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
using HumanoidRetargeter.Target;
using Xunit;
namespace HumanoidRetargeter.Tests.Target;
public class AnimationMetadataTests
{
private static KvObject Root(string text) => (KvObject)((KvObject)Kv3.Parse(text).Root)["rootNode"];
private static KvObject Category(string text, string category)
=> ((KvArray)Root(text)["children"]).Items.OfType<KvObject>().Single(n => n.GetString("_class") == category);
[Fact]
public void CompleteNestedAnimationMetadataSurvivesSetupAndGraphCopy()
{
var stock = VmdlWriter.Kv3Header + """
{ rootNode = { anim_graph_name = "stock.vanmgrph" children = [
{ _class = "AnimationList" default_root_bone_name = "pelvis" children = [
{ _class = "Prefab" target_file = "complete_stock_animations.vmdl_prefab" },
{ _class = "Folder" name = "Locomotion" children = [
{ _class = "AnimFile" name = "Walk_N" source_filename = "walk.fbx" looping = true fps = 30 children = [
{ _class = "AnimEvent" event_class = "AE_FOOTSTEP" event_frame = 7 event_keys = { Foot = "0" Attachment = "foot_L" Volume = 0.7 } },
{ _class = "ExtractMotion" root_bone_name = "pelvis" },
{ _class = "AnimTag" name = "contact" frame = 7 },
{ _class = "FutureAnimationSetting" data = [ 1, 2, 3 ] }
] },
{ _class = "AnimFile" name = "idle_delta" source_filename = "idle.fbx" children = [
{ _class = "AnimSubtract" anim_name = "idle" frame = 12 },
{ _class = "AnimWeightList" name = "UpperBody" }
] },
{ _class = "2DBlend" name = "Walk" blend_anim_list = [ [ "Walk_N" ] ] }
] }
] },
{ _class = "AnimConstraintList" children = [ { _class = "Folder" name = "CopyPinky" children = [ { _class = "AnimConstraintOrient" weight = 1.0 } ] } ] },
{ _class = "BoneMarkupList" children = [ { _class = "BoneMarkup" name = "leg" } ] },
{ _class = "AttachmentList" children = [ { _class = "Attachment" name = "foot_L" bone = "ankle_L" } ] },
{ _class = "IKData" children = [ { _class = "IKChain" name = "leg_L" } ] },
{ _class = "PoseParamList" children = [ { _class = "PoseParameter" name = "move_x" } ] },
{ _class = "WeightListList" children = [ { _class = "WeightList" name = "UpperBody" } ] },
{ _class = "GameDataList" children = [ { _class = "Prefab" target_file = "game_data.vmdl_prefab" } ] }
] } }
""";
var custom = VmdlWriter.GenerateStandalone("", Array.Empty<AnimEntry>(), .3937f, "pelvis", meshFilePath: "custom.fbx",
materialRemaps: new Dictionary<string, string> { ["body"] = "custom.vmat" });
var setup = CitizenAnimationSetup.Apply(custom, stock);
setup = StockAnimationGraph.Attach(setup, "output/graphs/custom.vanmgrph");
foreach (var category in new[] { "AnimationList", "BoneMarkupList", "AttachmentList", "IKData", "PoseParamList", "WeightListList", "GameDataList" })
Assert.True(KvValue.DeepEquals(Category(stock, category), Category(setup, category)), category + " was not copied completely.");
var constraints = Category(setup, "AnimConstraintList");
var wrapper = Assert.IsType<KvObject>(Assert.Single(((KvArray)constraints["children"]).Items));
Assert.True(KvValue.DeepEquals(Category(stock, "AnimConstraintList")["children"], wrapper["children"]));
Assert.True(KvValue.DeepEquals(Category(custom, "RenderMeshList"), Category(setup, "RenderMeshList")));
Assert.True(KvValue.DeepEquals(Category(custom, "MaterialGroupList"), Category(setup, "MaterialGroupList")));
// Adding a replacement sequence must not flatten, rebuild or strip stock metadata.
var augmented = VmdlAugmenter.Augment(setup, new[] { new AnimEntry { Name = "replacement", SourceFilename = "replacement.dmx" } }, out _);
var originalEntries = ((KvArray)Category(setup, "AnimationList")["children"]).Items;
var augmentedEntries = ((KvArray)Category(augmented, "AnimationList")["children"]).Items;
foreach (var original in originalEntries) Assert.Contains(augmentedEntries, n => KvValue.DeepEquals(original, n));
}
}
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.ResourceTypes;
using HumanoidRetargeterVrf.ResourceTypes.ModelAnimation;
using HumanoidRetargeterVrf.Serialization.KeyValues;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace SmartPort.Parser.Tests;
public class SmartPortRetargetTests
{
[CompiledFixtureFact]
public void CompiledUnflaggedAdditiveDoesNotAddBindLengthDuringRetargeting()
{
var path = Path.Combine(Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!, "models/player/human/frank_mp.vmdl_c");
using var resource = new Resource();
resource.Read(path);
var model = Assert.IsType<Model>(resource.DataBlock);
var source = SkeletonModel.Create(model.Skeleton.Bones.Select(b => new BoneDefinition(b.Name, b.Parent?.Name, new XForm(b.Position, b.Angle))).ToArray());
var target = SkeletonModel.Create(source.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : source[b.ParentIndex].Name, new XForm(b.RestLocal.Pos * .7f, b.RestLocal.Rot))).ToArray());
var plan = new SmartPortRig(source, target);
var animation = model.GetEmbeddedAnimations().Single(a => a.Name == "bindPose_delta");
Assert.False(animation.Delta); // Modern ModelDoc stores already-subtracted channels without the legacy flag.
var frame = new Frame(model.Skeleton, model.FlexControllers) { FrameIndex = 0 };
animation.DecodeFrame(frame);
var locals = source.Bones.Select(b =>
{
var original = model.Skeleton.Bones.Single(x => x.Name == b.Name).Index;
return new XForm(frame.Bones[original].Position, frame.Bones[original].Angle);
}).ToArray();
Assert.True(plan.IsDeltaPose(locals));
Assert.All(plan.Transfer(locals, delta: true), p => Assert.True(p.Pos.Length() < .001f));
}
[Fact]
public void RecoveredParentConstraintRetainsItsDestinationBone()
{
const string bone = "test_parent_constraint_slave";
HumanoidRetargeterVrf.Utils.StringToken.Store(new[] { bone });
var hash = HumanoidRetargeterVrf.Utils.StringToken.InvertedTable.Single(p => p.Value == bone).Key;
var slave = new KVObject(null);
slave.AddProperty("m_nBoneHash", hash);
slave.AddProperty("m_flWeight", 1.0);
KVObject Array(params double[] values)
{
var a = new KVObject(null, isArray: true);
foreach (var value in values) a.AddItem(value);
return a;
}
slave.AddProperty("m_vBasePosition", Array(0, 0, 0));
slave.AddProperty("m_qBaseOrientation", Array(0, 0, 0, 1));
var slaves = new KVObject(null, isArray: true); slaves.AddItem(slave);
var constraint = new KVObject(null);
constraint.AddProperty("m_slaves", slaves);
constraint.AddProperty("m_targets", new KVObject(null, isArray: true));
var node = new KVObject(null); node.AddProperty("_class", "AnimConstraintParent");
typeof(HumanoidRetargeterVrf.IO.ModelExtract).GetMethod("ProcessBoneConstraintChildren",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.Invoke(null, new object[] { constraint, node });
Assert.Equal(bone, node.GetStringProperty("constrained_bone"));
}
}
using System.Numerics;
using HumanoidRetargeter.Editor;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeterVrf;
using HumanoidRetargeterVrf.ResourceTypes;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace SmartPort.Parser.Tests;
public class AttachmentAlignmentTests
{
static string Document(string bone, bool ignore = false) => VmdlWriter.Kv3Header + """
{ rootNode = { children = [{ _class = "AttachmentList" children = [{
_class = "Attachment" name = "tool_mount"
""" + $"parent_bone = \"{bone}\" ignore_rotation = {ignore.ToString().ToLowerInvariant()} " + """
relative_origin = [1, 2, 3] relative_angles = [0, 0, 0] weight = 1
}] }] } }
""";
static KvObject Attachment(string text) => (KvObject)((KvArray)((KvObject)((KvArray)((KvObject)((KvObject)
Kv3.Parse(text).Root)["rootNode"])["children"]).Items[0])["children"]).Items[0];
[CompiledFixtureFact]
public void SharedSocketAdoptsSourceAxesWithoutMovingItsFittedPositionOrSkin()
{
using var resource = new Resource();
resource.Read(Path.Combine(Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!, "models/player/human/frank_mp.vmdl_c"));
var model = Assert.IsType<Model>(resource.DataBlock);
var source = SkeletonModel.Create(model.Skeleton.Bones.Select(b => new BoneDefinition(b.Name, b.Parent?.Name, new(b.Position, b.Angle))).ToArray());
var target = SkeletonModel.Create(source.Bones.Select(b => new BoneDefinition(b.Name,
b.ParentIndex < 0 ? null : source[b.ParentIndex].Name,
b.Name == "hold_R" ? new XForm(b.RestLocal.Pos, Quaternion.CreateFromAxisAngle(Vector3.UnitY, .3f) * b.RestLocal.Rot) : b.RestLocal)).ToArray());
var rig = new SmartPortRig(source, target);
var sourceText = Document("hold_r"); // compiled attachment names can differ in case
var targetText = Document("hold_R");
var aligned = Attachment(SmartPortAttachments.Align(targetText, sourceText, rig));
Assert.True(KvValue.DeepEquals(Attachment(targetText)["relative_origin"], aligned["relative_origin"]));
var values = ((KvArray)aligned["relative_angles"]).Items.Select(v => v is KvDouble d ? (float)d.Value : ((KvLong)v).Value).ToArray();
var local = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, values[1] * MathF.PI / 180)
* Quaternion.CreateFromAxisAngle(Vector3.UnitY, values[0] * MathF.PI / 180)
* Quaternion.CreateFromAxisAngle(Vector3.UnitX, values[2] * MathF.PI / 180);
var pose = rig.Transfer(source.Bones.Select(b => b.RestLocal).ToArray());
var world = new XForm[rig.Target.Count];
foreach (var bone in rig.Target.Bones)
world[bone.Index] = bone.ParentIndex < 0 ? pose[bone.Index] : XForm.Compose(world[bone.ParentIndex], pose[bone.Index]);
Assert.True(MathQ.AngleBetween(world[rig.Target.IndexOf("hold_R")].Rot * local, source.RestWorld[source.IndexOf("hold_R")].Rot) < .001f);
foreach (var bone in target.Bones) Assert.Equal(bone.RestLocal, rig.Target[rig.Target.IndexOf(bone.Name)].RestLocal);
var ignored = Document("hold_R", true);
Assert.True(KvValue.DeepEquals(Attachment(ignored), Attachment(SmartPortAttachments.Align(ignored, sourceText, rig))));
}
[CompiledFixtureFact]
public void RecoveredGraphRetainsEveryNodePositionWhenCopied()
{
var path = Path.Combine(Environment.GetEnvironmentVariable("HR_SMART_PORT_FIXTURE")!, "models/player/mplayer/mplayer_animgraph.vanmgrph_c");
var recovered = CompiledAssetRecovery.Recover(path, "models/player/mplayer/mplayer_animgraph.vanmgrph",
Path.GetTempPath(), "unused", new Dictionary<string, string>(), CancellationToken.None);
using var resource = new Resource(); resource.Read(path);
var original = (KvObject)Kv3.Parse(resource.DataBlock!.ToString()!).Root;
var copied = (KvObject)Kv3.Parse(StockAnimationGraph.CopyForModel(recovered, "custom.vmdl")).Root;
Assert.True(KvValue.DeepEquals(original["m_nodeManager"], copied["m_nodeManager"]));
Assert.Contains("m_vecPosition", recovered);
}
}
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;
// Read-only playback of a fresh Smart Port output in an explicitly opted-in scratch project.
public static class SmartPortGripEngineTest
{
static bool started;
[EditorEvent.Frame]
public static void Tick()
{
var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_TEST_PROJECT");
if (started || Project.Current is null || string.IsNullOrEmpty(root)
|| !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'),
Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
|| !AssetSystem.All.Any()) return;
started = true;
_ = Run(root);
}
static async Task Run(string root)
{
var data = new Dictionary<string, object>();
void Save() => File.WriteAllText(Path.Combine(root, "grip-test-result.json"),
JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
try
{
data["started"] = DateTime.UtcNow; Save();
var target = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_MODEL");
if (string.IsNullOrWhiteSpace(target)) throw new Exception("Set HR_SMART_PORT_GRIP_MODEL to a freshly generated port's asset path");
data["model"] = target;
var session = SceneEditorSession.CreateDefault(); session.MakeActive();
using (session.Scene.Push())
foreach (var path in new[] { "models/player/human/frank_mp.vmdl", target })
{
var go = session.Scene.CreateObject(); go.Name = path;
var actor = go.Components.Create<SkinnedModelRenderer>();
actor.Model = Model.Load(path); actor.UseAnimGraph = true;
if (actor.Model.IsError) throw new Exception("Missing model " + path);
}
EditorScene.Play(false, session); await Task.Delay(1500);
if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
var hands = actors.ToDictionary(a => a, a => a.Model.Bones.AllBones
.Where(b => string.Equals(b.Name, "hand_L", StringComparison.OrdinalIgnoreCase)
|| string.Equals(b.Name, "hand_R", StringComparison.OrdinalIgnoreCase))
.OrderBy(b => b.Name, StringComparer.OrdinalIgnoreCase).Select(b => b.Name).ToArray());
if (hands.Values.Any(names => names.Length != 2)) throw new Exception("Missing hand bones");
var observations = new List<object>(); data["observations"] = observations;
var reference = new Dictionary<SkinnedModelRenderer, Vector3[]>();
foreach (var (style, x, y) in new[] { (0, 0f, 0f), (4, 120f, 0f), (5, 240f, 0f),
(4, 0f, 120f), (4, 0f, -120f), (4, -120f, 0f), (4, 85f, 85f),
(4, 85f, -85f), (4, -85f, 85f), (4, -85f, -85f) })
{
var samples = actors.ToDictionary(a => a, a => new List<Vector3[]>());
var feet = actors.ToDictionary(a => a, a => new List<Vector3>());
for (var frame = 0; frame < 180; frame++)
{
foreach (var actor in actors)
{
actor.Set("b_grounded", true); actor.Set("holdtype", 6); actor.Set("weapon_pose", 0); actor.Set("move_style", style);
actor.Set("aim_body", Vector3.Forward); actor.Set("aim_body_weight", 1f);
actor.Set("aim_head", Vector3.Forward); actor.Set("aim_head_weight", 1f);
var speed = MathF.Sqrt(x * x + y * y);
actor.Set("move_x", x); actor.Set("move_y", y); actor.Set("move_speed", speed); actor.Set("move_groundspeed", speed);
actor.Set("wish_x", x); actor.Set("wish_y", y); actor.Set("wish_groundspeed", speed);
}
await Task.Delay(20);
if (frame < 80) continue;
foreach (var actor in actors)
{
var grip = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing weapon attachment");
samples[actor].Add(hands[actor].Select(b => grip.Rotation.Inverse *
(actor.SceneModel.GetBoneWorldTransform(b).Position - grip.Position)).ToArray());
var ankle = actor.Model.Bones.AllBones.First(b => string.Equals(b.Name, "ankle_L", StringComparison.OrdinalIgnoreCase));
feet[actor].Add(actor.SceneModel.GetBoneWorldTransform(ankle.Name).Position - actor.WorldPosition);
}
}
foreach (var actor in actors)
{
if (style == 0) reference[actor] = Enumerable.Range(0, 2).Select(side =>
samples[actor].Aggregate(Vector3.Zero, (sum, v) => sum + v[side]) / samples[actor].Count).ToArray();
var footMotion = feet[actor].Max(v => v.Distance(feet[actor][0]));
if (style != 0 && footMotion < 1) throw new Exception("Locomotion did not animate: " + actor.Model.Name);
foreach (var side in new[] { 0, 1 })
{
var drift = samples[actor].Max(v => v[side].Distance(reference[actor][side]));
observations.Add(new { model = actor.Model.Name, style, x, y, side, drift, footMotion }); Save();
if (!float.IsFinite(drift) || drift > .15f)
throw new Exception($"Hand slipped relative to weapon: {actor.Model.Name}, side {side}, style {style}, velocity {x},{y}, drift {drift}");
}
}
}
data["passed"] = true;
}
catch (Exception e) { data["error"] = e.ToString(); data["passed"] = false; }
finally
{
data["completed"] = true; Save();
if (Game.IsPlaying) EditorScene.Stop();
await Task.Delay(1000); EditorUtility.Quit(true);
}
}
}
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;
// Opt-in playback in an isolated scratch project; never run against a user's editor.
public static class SmartPortFiringEngineTest
{
static bool started;
[EditorEvent.Frame]
public static void Tick()
{
var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_FIRING_PROJECT");
if (started || Project.Current is null || string.IsNullOrEmpty(root)
|| !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'),
Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
|| !AssetSystem.All.Any()) return;
started = true; _ = Run(root);
}
static async Task Run(string root)
{
var data = new Dictionary<string, object>();
void Save() => File.WriteAllText(Path.Combine(root, "firing-result.json"),
JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
try
{
data["started"] = DateTime.UtcNow;
var port = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_MODEL");
if (string.IsNullOrWhiteSpace(port)) throw new Exception("Set HR_SMART_PORT_GRIP_MODEL to the ported Human Citizen asset");
data["model"] = port; Save();
var session = SceneEditorSession.CreateDefault(); session.MakeActive();
using (session.Scene.Push())
{
foreach (var path in new[] { "models/player/human/frank_mp.vmdl", port })
{
var go = session.Scene.CreateObject(); go.Name = path;
go.WorldPosition = new Vector3(0, path == port ? 35 : -35, 0);
var actor = go.Components.Create<SkinnedModelRenderer>();
actor.Model = Model.Load(path); actor.UseAnimGraph = true;
if (actor.Model.IsError) throw new Exception("Missing model " + path);
}
var camera = session.Scene.CreateObject(); camera.Name = "Firing Camera";
camera.WorldPosition = new Vector3(120, 0, 80);
camera.WorldRotation = Rotation.LookAt(new Vector3(0, 0, 45) - camera.WorldPosition);
camera.Components.Create<CameraComponent>().FieldOfView = 60;
foreach (var yaw in new[] { 25f, 145f, 265f })
{
var light = session.Scene.CreateObject(); light.WorldRotation = Rotation.From(35, yaw, 0);
light.Components.Create<DirectionalLight>().LightColor = Color.White * .8f;
}
}
EditorScene.Play(false, session); await Task.Delay(1500);
if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
var observations = new List<object>(); data["observations"] = observations;
foreach (var hold in new[] { 6, 5, 7 })
foreach (var speed in new[] { 0f, 120f, 240f })
{
void Parameters(bool fire)
{
foreach (var actor in actors)
{
actor.Set("b_grounded", true); actor.Set("holdtype", hold); actor.Set("weapon_pose", 0);
actor.Set("move_style", speed == 0 ? 0 : speed < 200 ? 4 : 5);
actor.Set("aim_body", Vector3.Forward); actor.Set("aim_body_weight", 1f);
actor.Set("aim_head", Vector3.Forward); actor.Set("aim_head_weight", 1f);
actor.Set("move_x", speed); actor.Set("move_speed", speed); actor.Set("move_groundspeed", speed);
actor.Set("wish_x", speed); actor.Set("wish_groundspeed", speed);
if (fire) actor.Set("b_attack", true);
}
}
for (var frame = 0; frame < 100; frame++) { Parameters(false); await Task.Delay(20); }
var idle = actors.ToDictionary(a => a, a => RelativeHand(a));
var rotations = actors.ToDictionary(a => a, a => a.SceneModel.GetAttachment("hold_R", true).Value.Rotation);
var drift = actors.ToDictionary(a => a, a => 0f);
var recoil = actors.ToDictionary(a => a, a => 0f);
for (var frame = 0; frame < 150; frame++)
{
Parameters(frame % 25 == 0); await Task.Delay(20);
if (hold == 6 && speed == 0 && (frame == 6 || frame == 12 || frame == 18))
{
var camera = Game.ActiveScene.GetAllComponents<CameraComponent>().Single(c => c.GameObject.Name == "Firing Camera");
var pixmap = new Pixmap(1200, 900);
if (!camera.RenderToPixmap(pixmap)) throw new Exception("Render failed");
File.WriteAllBytes(Path.Combine(root, $"firing-{frame}.png"), pixmap.GetPng());
}
foreach (var actor in actors)
{
drift[actor] = MathF.Max(drift[actor], RelativeHand(actor).Distance(idle[actor]));
var direction = actor.SceneModel.GetAttachment("hold_R", true).Value.Rotation.Forward;
recoil[actor] = MathF.Max(recoil[actor], MathF.Acos(Math.Clamp(Vector3.Dot(rotations[actor].Forward, direction), -1, 1)) * 180 / MathF.PI);
}
}
foreach (var actor in actors)
observations.Add(new { model = actor.Model.Name, hold, speed, drift = drift[actor], recoil = recoil[actor] });
Save();
foreach (var actor in actors)
{
if (!float.IsFinite(drift[actor]) || drift[actor] > .15f)
throw new Exception($"Support hand slipped while firing: {actor.Model.Name}, hold {hold}, speed {speed}, drift {drift[actor]}");
if (!float.IsFinite(recoil[actor]) || recoil[actor] < .5f)
throw new Exception("Firing did not visibly recoil: " + actor.Model.Name);
}
}
data["passed"] = true;
}
catch (Exception e) { data["error"] = e.ToString(); data["passed"] = false; }
finally
{
data["completed"] = true; Save();
if (Game.IsPlaying) EditorScene.Stop();
await Task.Delay(1000); EditorUtility.Quit(true);
}
}
static Vector3 RelativeHand(SkinnedModelRenderer actor)
{
var grip = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing weapon attachment");
var bone = actor.Model.Bones.AllBones.Single(b => string.Equals(b.Name, "hand_L", StringComparison.OrdinalIgnoreCase));
return grip.Rotation.Inverse * (actor.SceneModel.GetBoneWorldTransform(bone.Name).Position - grip.Position);
}
}
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Editor;
using Sandbox;
// Visual evidence only: no corrective offsets, bone merging or scaling.
public static class SmartPortGunVisualTest
{
static bool started;
static readonly Dictionary<SkinnedModelRenderer, ModelRenderer> guns = new();
[EditorEvent.Frame]
public static void Tick()
{
foreach (var pair in guns)
if (pair.Key.IsValid() && pair.Value.IsValid() && pair.Key.SceneModel is not null)
pair.Value.WorldTransform = pair.Key.SceneModel.GetAttachment("hold_R", true)
?? throw new Exception("Missing weapon socket");
var root = Environment.GetEnvironmentVariable("HR_SMART_PORT_GUN_VISUAL_PROJECT");
if (started || Project.Current is null || string.IsNullOrEmpty(root)
|| !string.Equals(Path.GetFullPath(Project.Current.GetRootPath()).TrimEnd('/', '\\'),
Path.GetFullPath(root).TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)
|| !AssetSystem.All.Any()) return;
started = true; _ = Run(root);
}
static async Task Run(string root)
{
var data = new Dictionary<string, object>();
void Save() => File.WriteAllText(Path.Combine(root, "gun-visual-result.json"),
JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));
try
{
var port = Environment.GetEnvironmentVariable("HR_SMART_PORT_GRIP_MODEL");
if (string.IsNullOrWhiteSpace(port)) throw new Exception("Set HR_SMART_PORT_GRIP_MODEL to the custom Citizen Smart Port output");
const string source = "models/citizen/citizen.vmdl";
const string gun = "models/weapons/sbox_assault_m4a1/w_m4a1.vmdl";
data["started"] = DateTime.UtcNow; data["model"] = port; data["gun"] = gun;
data["attachment"] = "hold_R"; data["offset"] = "identity"; Save();
var session = SceneEditorSession.CreateDefault(); session.MakeActive();
using (session.Scene.Push())
{
foreach (var path in new[] { source, port })
{
var go = session.Scene.CreateObject(); go.Name = path;
var actor = go.Components.Create<SkinnedModelRenderer>();
actor.Model = Model.Load(path); actor.UseAnimGraph = true;
if (actor.Model.IsError) throw new Exception("Missing character " + path);
}
var camera = session.Scene.CreateObject(); camera.Name = "Gun Camera";
camera.WorldPosition = new Vector3(95, 85, 70);
camera.WorldRotation = Rotation.LookAt(new Vector3(8, 0, 45) - camera.WorldPosition);
camera.Components.Create<CameraComponent>().FieldOfView = 40;
foreach (var yaw in new[] { 25f, 145f, 265f })
{
var light = session.Scene.CreateObject(); light.WorldRotation = Rotation.From(35, yaw, 0);
light.Components.Create<DirectionalLight>().LightColor = Color.White * .8f;
}
}
EditorScene.Play(false, session); await Task.Delay(1500);
if (!Game.IsPlaying || Game.ActiveScene is null || Game.ActiveScene.IsEditor) throw new Exception("Did not enter play mode");
var actors = Game.ActiveScene.GetAllComponents<SkinnedModelRenderer>().ToArray();
using (Game.ActiveScene.Push())
foreach (var actor in actors)
{
var go = Game.ActiveScene.CreateObject(); go.Name = "M4A1 " + actor.Model.Name;
var renderer = go.Components.Create<ModelRenderer>(); renderer.Model = Model.Load(gun);
if (renderer.Model.IsError) throw new Exception("Missing M4A1 model/dependencies");
guns.Add(actor, renderer);
}
var captures = new List<string>(); data["screenshots"] = captures;
var grips = new List<object>(); data["grips"] = grips;
foreach (var speed in new[] { 0f, 120f, 240f })
foreach (var actor in actors)
{
foreach (var other in actors) other.WorldPosition = other == actor ? Vector3.Zero : new Vector3(0, 1000, 0);
for (var frame = 0; frame < 120; frame++)
{
actor.Set("b_grounded", true); actor.Set("holdtype", 2); actor.Set("weapon_pose", 0);
actor.Set("move_style", speed == 0 ? 0 : speed < 200 ? 4 : 5);
actor.Set("aim_body", Vector3.Forward); actor.Set("aim_body_weight", 1f);
actor.Set("aim_head", Vector3.Forward); actor.Set("aim_head_weight", 1f);
actor.Set("move_x", speed); actor.Set("move_speed", speed); actor.Set("move_groundspeed", speed);
actor.Set("wish_x", speed); actor.Set("wish_groundspeed", speed);
await Task.Delay(20);
}
foreach (var fire in new[] { false, true })
{
if (fire) { actor.Set("b_attack", true); await Task.Delay(100); }
var socket = actor.SceneModel.GetAttachment("hold_R", true) ?? throw new Exception("Missing grip");
guns[actor].WorldTransform = socket;
foreach (var hand in new[] { "hand_L", "hand_R" })
{
var bone = actor.Model.Bones.AllBones.Single(b => string.Equals(b.Name, hand, StringComparison.OrdinalIgnoreCase));
var relative = socket.Rotation.Inverse * (actor.SceneModel.GetBoneWorldTransform(bone.Name).Position - socket.Position);
grips.Add(new { model = actor.Model.Name, hand, speed, fire, relative = relative.ToString() });
}
var camera = Game.ActiveScene.GetAllComponents<CameraComponent>().Single(c => c.GameObject.Name == "Gun Camera");
var pixmap = new Pixmap(1200, 1200);
if (!camera.RenderToPixmap(pixmap)) throw new Exception("Render failed");
var name = $"gun-raw-{(actor.Model.Name == source ? "source" : "custom")}-{speed}-{(fire ? "fire" : "hold")}.png";
File.WriteAllBytes(Path.Combine(root, name), pixmap.GetPng()); captures.Add(name); Save();
actor.Set("b_attack", false);
}
}
data["captured"] = true;
}
catch (Exception e) { data["error"] = e.ToString(); }
finally
{
data["completed"] = true; Save(); guns.Clear();
if (Game.IsPlaying) EditorScene.Stop();
await Task.Delay(1000); EditorUtility.Quit(true);
}
}
}
using Sandbox;
[TestClass]
public partial class LibraryTests
{
[TestMethod]
public void SceneTest()
{
var scene = new Scene();
using ( scene.Push() )
{
var go = new GameObject();
Assert.AreEqual( 1, scene.Directory.GameObjectCount );
}
}
}
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using LichessNET.API;
using LichessNET.Entities.Board;
using LichessNET.Entities.Enumerations;
using LichessNET.Entities.OAuth;
using LichessNET.Gameplay;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public sealed class LichessGameplayTests
{
[TestMethod]
public async Task QueueSubscribesBeforeStartingAccountStream()
{
var stream = new FakeBoardStream();
var client = new FakeBoardClient(stream);
stream.OnStart = () => stream.Emit(
"{\"type\":\"gameStart\",\"game\":{\"gameId\":\"game-1\",\"color\":\"white\"}}");
LichessGameFoundEventArgs? found = null;
await using var queue = new LichessQueue(client);
queue.OnGameFound += (_, game) => found = game;
await queue.StartSeekAsync(new BoardSeekOptions());
Assert.IsTrue(stream.Started);
Assert.IsNotNull(found);
Assert.AreEqual("game-1", found.GameId);
Assert.AreEqual(LichessQueueState.GameFound, queue.State);
Assert.IsFalse(queue.IsSeeking);
}
[TestMethod]
public async Task QueueCancellationReleasesStreamAndReturnsToIdle()
{
var stream = new FakeBoardStream();
var client = new FakeBoardClient(stream);
using var cancellation = new CancellationTokenSource();
await using var queue = new LichessQueue(client);
await queue.StartSeekAsync(new BoardSeekOptions(), cancellation.Token);
Assert.AreEqual(LichessQueueState.Seeking, queue.State);
cancellation.Cancel();
stream.Complete();
Assert.AreEqual(LichessQueueState.Idle, queue.State);
Assert.IsFalse(queue.IsSeeking);
Assert.IsTrue(stream.Disposed);
}
[TestMethod]
public async Task SessionReconcilesPendingAndRebuildsDivergentHistory()
{
var first = new FakeBoardStream();
var second = new FakeBoardStream();
var client = new FakeBoardClient(new FakeBoardStream(), first, second);
first.OnStart = () => first.Emit(GameFull("e2e4 e7e5", true));
second.OnStart = () => second.Emit(GameFull("e2e4 e7e5 g1f3", false));
var adapter = new RecordingChessBoardAdapter();
await using var session = new LichessGameSession(
client, "game-1", "white", adapter,
new LichessGameSessionOptions { AutoReconnect = false });
await session.StartAsync();
Assert.AreEqual("standard", session.GameFull?.Variant);
Assert.AreEqual("Blitz", session.GameFull?.Perf);
Assert.IsTrue(session.WhiteOfferingDraw);
CollectionAssert.AreEqual(
new[] { "e2e4", "e7e5" }, new List<string>(session.MoveHistory));
Assert.IsTrue(await session.SubmitLocalMoveAsync("g1f3"));
Assert.AreEqual("g1f3", session.PendingLocalMove);
await session.ReconnectAsync();
Assert.IsNull(session.PendingLocalMove);
CollectionAssert.AreEqual(
new[] { "e2e4", "e7e5", "g1f3" },
new List<string>(session.MoveHistory));
second.Emit(
"{\"type\":\"gameState\",\"moves\":\"d2d4\",\"status\":\"started\"}");
CollectionAssert.AreEqual(
new[] { "d2d4" }, new List<string>(session.MoveHistory));
CollectionAssert.AreEqual(
new[] { "d2d4" }, new List<string>(adapter.Moves));
}
[TestMethod]
public async Task UnsupportedInitialFenBlocksLaterStateUpdates()
{
var stream = new FakeBoardStream();
var client = new FakeBoardClient(new FakeBoardStream(), stream);
stream.OnStart = () => stream.Emit(GameFull("e2e4", false, "8/8/8/8/8/8/8/8 w - - 0 1"));
var adapter = new RecordingChessBoardAdapter();
await using var session = new LichessGameSession(
client, "game-1", "white", adapter,
new LichessGameSessionOptions { AutoReconnect = false });
await session.StartAsync();
stream.Emit(
"{\"type\":\"gameState\",\"moves\":\"e2e4 e7e5\",\"status\":\"started\"}");
Assert.AreEqual(0, session.MoveHistory.Count);
Assert.AreEqual(0, adapter.Moves.Count);
}
[TestMethod]
public async Task AuthenticationFailureStopsAutomaticReconnect()
{
var first = new FakeBoardStream();
var second = new FakeBoardStream();
var client = new FakeBoardClient(new FakeBoardStream(), first, second);
first.OnStart = () => first.Emit(GameFull(string.Empty, false));
await using var session = new LichessGameSession(
client, "game-1", "white", new RecordingChessBoardAdapter(),
new LichessGameSessionOptions
{
AutoReconnect = true,
ReconnectDelays = new[] { TimeSpan.Zero }
});
await session.StartAsync();
first.Fail(new LichessApiException(HttpStatusCode.Unauthorized));
first.Complete();
Assert.IsFalse(second.Started);
Assert.AreEqual(LichessGameConnectionState.Disconnected, session.ConnectionState);
}
[TestMethod]
public async Task ZeroDelayReconnectStartsOnlyOneReplacementStream()
{
var first = new FakeBoardStream();
var second = new FakeBoardStream();
var client = new FakeBoardClient(new FakeBoardStream(), first, second);
first.OnStart = () => first.Emit(GameFull(string.Empty, false));
second.OnStart = () => second.Emit(GameFull(string.Empty, false));
await using var session = new LichessGameSession(
client, "game-1", "white", new RecordingChessBoardAdapter(),
new LichessGameSessionOptions
{
AutoReconnect = true,
ReconnectDelays = new[] { TimeSpan.Zero }
});
var unexpectedCompletions = 0;
session.OnUnexpectedCompletion += _ => unexpectedCompletions++;
await session.StartAsync();
first.Complete();
Assert.IsTrue(second.Started);
Assert.AreEqual(1, unexpectedCompletions);
Assert.AreEqual(LichessGameConnectionState.Connected, session.ConnectionState);
}
private static string GameFull(string moves, bool whiteDraw,
string initialFen = "startpos")
{
return "{" +
"\"type\":\"gameFull\"," +
"\"id\":\"game-1\"," +
"\"initialFen\":\"" + initialFen + "\"," +
"\"variant\":{\"key\":\"standard\",\"name\":\"Standard\"}," +
"\"speed\":\"blitz\"," +
"\"perf\":{\"name\":\"Blitz\"}," +
"\"state\":{" +
"\"type\":\"gameState\"," +
"\"moves\":\"" + moves + "\"," +
"\"status\":\"started\"," +
"\"wdraw\":" + whiteDraw.ToString().ToLowerInvariant() +
"}}";
}
private sealed class FakeBoardStream : ILichessBoardEventStream
{
public event Action<ILichessBoardEventStream, JsonElement>? LineReceived;
public event Action<ILichessBoardEventStream, Exception>? ErrorReceived;
public event Action<ILichessBoardEventStream>? Completed;
public Action? OnStart { get; set; }
public bool Started { get; private set; }
public bool Disposed { get; private set; }
public Task Completion => Task.CompletedTask;
public void Start()
{
Started = true;
OnStart?.Invoke();
}
public void Emit(string json)
{
var element = JsonSerializer.Deserialize<JsonElement>(json);
LineReceived?.Invoke(this, element);
}
public void Fail(Exception exception)
{
ErrorReceived?.Invoke(this, exception);
}
public void Complete()
{
Completed?.Invoke(this);
}
public ValueTask DisposeAsync()
{
Disposed = true;
return ValueTask.CompletedTask;
}
}
private sealed class FakeBoardClient : ILichessBoardClient
{
private readonly Queue<ILichessBoardEventStream> _gameStreams = new();
private readonly ILichessBoardEventStream _accountStream;
public FakeBoardClient(ILichessBoardEventStream accountStream,
params ILichessBoardEventStream[] gameStreams)
{
_accountStream = accountStream;
foreach (var stream in gameStreams)
_gameStreams.Enqueue(stream);
}
public string? GetToken() => "test-token";
public Task<Dictionary<string, TokenInfo?>> TestTokensAsync(
List<string> tokens, CancellationToken cancellationToken = default)
{
var info = new TokenInfo
{
Permissions = new List<TokenPermission> { TokenPermission.PlayGames }
};
return Task.FromResult(new Dictionary<string, TokenInfo?>
{
["test-token"] = info
});
}
public Task CreateBoardSeekAsync(BoardSeekOptions options,
CancellationToken cancellationToken = default)
{
return cancellationToken.IsCancellationRequested
? Task.FromCanceled(cancellationToken)
: Task.CompletedTask;
}
public Task<ILichessBoardEventStream> CreateBoardAccountEventStreamAsync(
CancellationToken cancellationToken = default)
{
return Task.FromResult(_accountStream);
}
public Task<ILichessBoardEventStream> CreateBoardGameStreamAsync(
string gameId, CancellationToken cancellationToken = default)
{
return Task.FromResult(_gameStreams.Dequeue());
}
public Task<bool> MakeBoardMoveAsync(string gameId, string uci,
bool offerDraw = false, CancellationToken cancellationToken = default)
{
return Task.FromResult(true);
}
public Task<bool> AbortBoardGameAsync(string gameId,
CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> ResignBoardGameAsync(string gameId,
CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> HandleDrawOfferAsync(string gameId, bool accept,
CancellationToken cancellationToken = default) => Task.FromResult(true);
public Task<bool> SendBoardChatAsync(string gameId, string text,
BoardChatRoom room = BoardChatRoom.Player,
CancellationToken cancellationToken = default) => Task.FromResult(true);
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
[AssemblyInitialize]
public static void ClassInitialize( TestContext context )
{
Sandbox.Application.InitUnitTest();
}
}
#nullable enable
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Tests.Architecture;
[TestClass]
public sealed class LayerBoundaryTests
{
private static readonly Regex BlockComments = new(@"/\*.*?\*/", RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex LineComments = new(@"//.*?$", RegexOptions.Multiline | RegexOptions.Compiled);
private static readonly Regex SyncAttribute = new(@"\[\s*Sync(?<body>[^\]]*)\]", RegexOptions.Compiled);
private static readonly Regex AuthorityApi = new(
@"\bRpc\.|\[\s*Rpc\b|\[\s*Sync\b|\bSyncFlags\.|\bFileSystem\.Data\b",
RegexOptions.Compiled);
public TestContext TestContext { get; set; } = null!;
[TestMethod]
public void DomainAndApplicationAreSandboxIndependent()
{
var violations = ProductFiles("Domain", "Application")
.Select(file => (File: file, Source: SourceWithoutComments(file)))
.Where(value => ContainsAny(value.Source, "using Sandbox", "Sandbox.") ||
AuthorityApi.IsMatch(value.Source))
.Select(value => Relative(value.File))
.ToArray();
Assert.IsEmpty(violations,
$"Domain/Application must remain Sandbox-independent: {string.Join(", ", violations)}");
}
[TestMethod]
public void SandboxAndAuthorityApisAreRestrictedToRuntimeAndInfrastructure()
{
var violations = ProductFiles()
.Select(file => (File: file, Source: SourceWithoutComments(file)))
.Where(value => ContainsAny(value.Source, "using Sandbox", "Sandbox.") ||
AuthorityApi.IsMatch(value.Source))
.Where(value => !IsAllowedSandboxLayer(value.File))
.Select(value => Relative(value.File))
.ToArray();
Assert.IsEmpty(violations,
$"Sandbox/authority APIs are restricted to Runtime and Infrastructure: {string.Join(", ", violations)}");
}
[TestMethod]
public void EveryV2SyncAttributeIsExplicitlyHostAuthored()
{
var violations = new List<string>();
foreach (var file in ProductFiles())
{
var source = SourceWithoutComments(file);
foreach (Match match in SyncAttribute.Matches(source))
{
if (!match.Groups["body"].Value.Contains("SyncFlags.FromHost", StringComparison.Ordinal))
violations.Add($"{Relative(file)}: {match.Value}");
}
}
Assert.IsEmpty(violations,
$"Every v2 synchronized field must be host-authored: {string.Join(" | ", violations)}");
}
[TestMethod]
public void ReplicatedPlayerBodyIsPresentationOnlyAndNeverWritesTheClientStore()
{
var playerBody = Path.Combine(V2Root(), "Runtime", "HexPlayerBody.cs");
var source = SourceWithoutComments(playerBody);
var forbidden = new[] { "HexClientStore", "ClientStore", "ApplyState(", "ReplacePlayer(" };
var violations = forbidden.Where(value => source.Contains(value, StringComparison.Ordinal)).ToArray();
Assert.IsEmpty(violations,
$"Replicated player state must remain presentation-only: {string.Join(", ", violations)}");
}
[TestMethod]
public void PlayerIsASingleConnectionOwnedObjectRunningANativeController()
{
var playerBody = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexPlayerBody.cs" ) );
// The player is one connection-owned object running a native, owner-simulated
// PlayerController. This is the engine idiom (ownership == authority) that keeps the
// body from being pinned to the world origin by host physics.
StringAssert.Contains( playerBody, "controller.UseInputControls = true" );
StringAssert.Contains( playerBody, "controller.UseLookControls = true" );
StringAssert.Contains( playerBody, "controller.EnablePressing = true" );
// No separate unowned body, and no revived custom client predictor.
Assert.IsFalse( playerBody.Contains( "Owner = null!", StringComparison.Ordinal ),
"The player body is the connection-owned object; no unowned host-simulated body may be spawned." );
Assert.IsFalse( playerBody.Contains( "NetworkMode = NetworkMode.Never", StringComparison.Ordinal ),
"The custom client predictor is removed in favour of the native controller's prediction." );
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
StringAssert.Contains( runtime, "playerObject.NetworkSpawn( connection )" );
}
[TestMethod]
public void ClientInputCannotBecomeSpatialAuthority()
{
var playerBody = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexPlayerBody.cs" ) );
// Movement is owner-simulated, but position AUTHORITY stays on the host. The host cannot see
// what the client reported — a proxy's transform is an interpolated reconstruction — so it
// AUDITS that transform over a window rather than validating it per tick. Enforcement is split
// deliberately: a teleport is corrected immediately through the host-authored channel, while a
// window whose travel exceeds the envelope escalates to a KICK rather than a snap-back, because
// per-tick correction fed itself and players experienced it as rubber-banding. Either way a
// client can never mint spatial authority: gameplay reads the host's accepted position.
StringAssert.Contains( playerBody, "Sandbox.Networking.IsHost && GameObject.Network.IsProxy && IsEmbodied" );
StringAssert.Contains( playerBody, "HostValidateMovement" );
StringAssert.Contains( playerBody, "HexMovementValidator.Observe" );
// The correction pulse is the only authority write to position, and it is host-authored.
StringAssert.Contains( playerBody, "[Sync( SyncFlags.FromHost )] public Vector3 AuthoritativePosition" );
StringAssert.Contains( playerBody, "[Sync( SyncFlags.FromHost )] public int CorrectionTick" );
// The owner APPLIES corrections; it does not author them.
StringAssert.Contains( playerBody, "GameObject.WorldPosition = AuthoritativePosition" );
StringAssert.Contains( playerBody, "AuthoritativeBody" );
// Gameplay resolves spatial checks against the host-validated position (not the raw client
// transform), and a client that keeps reporting out-of-envelope positions is enforced
// against — kicked, not merely nudged.
StringAssert.Contains( playerBody, "public Vector3 AuthoritativeWorldPosition" );
StringAssert.Contains( playerBody, "connection?.Kick(" );
// The deleted owner->host input pump must not return in any form.
Assert.IsFalse( playerBody.Contains( "SubmitInputFrame", StringComparison.Ordinal ),
"Movement is owner-simulated; there must be no owner->host input RPC." );
Assert.IsFalse( playerBody.Contains( "PlayableBody", StringComparison.Ordinal ) );
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
StringAssert.Contains( runtime, "connection.CanSpawnObjects = false" );
StringAssert.Contains( runtime, "connection.CanRefreshObjects = false" );
StringAssert.Contains( runtime, "connection.CanDestroyObjects = false" );
}
[TestMethod]
public void ProjectNetworkingAndPredictionCollisionDefaultsAreFailClosed()
{
var networking = File.ReadAllText( Path.Combine( ProductRoot(), "ProjectSettings", "Networking.config" ) );
foreach ( var permission in new[]
{
"\"ClientsCanSpawnObjects\": false",
"\"ClientsCanRefreshObjects\": false",
"\"ClientsCanDestroyObjects\": false",
// Host migration would hand authority to a machine with no host application,
// no domain services, and no persistence lease; both flags must stay closed.
"\"DestroyLobbyWhenHostLeaves\": true",
"\"AutoSwitchToBestHost\": false"
} ) StringAssert.Contains( networking, permission );
StringAssert.Contains( networking, "\"UpdateRate\": 30" );
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
StringAssert.Contains( runtime, "void Component.INetworkListener.OnBecameHost( Connection previousHost )" );
StringAssert.Contains( runtime, "HEXAGON_HOST_MIGRATION_REFUSED" );
StringAssert.Contains( runtime, "Networking.Disconnect()" );
var collision = File.ReadAllText( Path.Combine( ProductRoot(), "ProjectSettings", "Collision.config" ) );
StringAssert.Contains( collision, "\"b\": \"prediction\"" );
StringAssert.Contains( collision, "\"r\": \"Ignore\"" );
}
[TestMethod]
public void HostServicePublicationAndRpcShutdownAreFailClosed()
{
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
StringAssert.Contains( runtime, "OperationResult<HexHostServicesComponent> CreateHostServices()" );
StringAssert.Contains( runtime, "HostServicePublication.RequirePublished" );
StringAssert.Contains( runtime, "servicesObject.NetworkSpawn" );
StringAssert.Contains( runtime, "if ( servicesObject.IsValid() ) servicesObject.Destroy()" );
StringAssert.Contains( runtime, "if ( hostServices.Failed )" );
var shutdown = runtime.IndexOf( "private async Task<OperationResult> ShutdownHostAsync()", StringComparison.Ordinal );
var commandDrain = runtime.IndexOf( "_hostOperations.DrainAsync()", shutdown, StringComparison.Ordinal );
var pairedDisconnectLoop = runtime.IndexOf( "foreach ( var disconnect in disconnects )", shutdown, StringComparison.Ordinal );
var sessionDisconnect = runtime.IndexOf( "disconnect.Session.Disconnect()", pairedDisconnectLoop, StringComparison.Ordinal );
var disconnect = runtime.IndexOf( "application.Disconnected", shutdown, StringComparison.Ordinal );
var applicationDrain = runtime.IndexOf( "application.DisposeAsync", shutdown, StringComparison.Ordinal );
var persistenceDrain = runtime.IndexOf( "persistence.ShutdownAsync", shutdown, StringComparison.Ordinal );
var persistenceDispose = runtime.IndexOf( "persistence.DisposeAsync", shutdown, StringComparison.Ordinal );
var quiescedEvidence = runtime.IndexOf( "application.CompleteQuiescedShutdown", shutdown, StringComparison.Ordinal );
Assert.IsGreaterThanOrEqualTo( 0, shutdown );
Assert.IsLessThan( disconnect, sessionDisconnect,
"Each client session must be revoked immediately before its application disconnect callback." );
Assert.IsLessThan( commandDrain, disconnect, "Disconnect callbacks must revoke sessions before RPC dispatch drains." );
Assert.IsLessThan( applicationDrain, commandDrain, "RPC dispatch must finish before application disposal." );
Assert.IsLessThan( persistenceDrain, applicationDrain, "Application disposal must finish before persistence shutdown." );
Assert.IsLessThan( persistenceDispose, persistenceDrain, "Persistence must stop before it is disposed." );
Assert.IsLessThan( quiescedEvidence, persistenceDispose, "Quiesced evidence must follow persistence disposal." );
var services = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexHostServicesComponent.cs" ) );
StringAssert.Contains( services, "runtime.TryStartHostOperation" );
Assert.IsFalse( services.Contains( "_ = DispatchAsync", StringComparison.Ordinal ) );
}
[TestMethod]
public void ClientStateSyncShellAppliesOnlyAfterScopeCaptureAndEncodeAborts()
{
var services = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexHostServicesComponent.cs" ) );
var send = services.IndexOf( "public void SendClientState(", StringComparison.Ordinal );
Assert.IsGreaterThanOrEqualTo( 0, send );
var scopeCapture = services.IndexOf( "runtime.CaptureClientScope( recipient )", send, StringComparison.Ordinal );
var encodeAbort = services.IndexOf( "LogSnapshotWireFailure( \"ENCODE\", \"client-state\"", send, StringComparison.Ordinal );
var applyShell = services.IndexOf( "player.HostApplyPublicSnapshot( publicSnapshot )", send, StringComparison.Ordinal );
var wireSend = services.IndexOf( "ReceiveClientState( scope.Value, encoded.Value )", send, StringComparison.Ordinal );
Assert.IsGreaterThanOrEqualTo( 0, applyShell );
Assert.IsGreaterThanOrEqualTo( 0, wireSend );
Assert.IsLessThan( applyShell, scopeCapture,
"The scope-capture abort must run before the replicated [Sync] shell is mutated." );
Assert.IsLessThan( applyShell, encodeAbort,
"The wire-encode abort must run before the replicated [Sync] shell is mutated." );
Assert.IsLessThan( wireSend, applyShell,
"The [Sync] shell mutation must sit immediately before the send, after every abort exit." );
}
[TestMethod]
public void ClientLayerDoesNotReferenceServerAggregatesOrPersistence()
{
var forbidden = new[]
{
"Hexagon.V2.Persistence",
"CharacterRecord",
"InventoryRecord",
"ItemRecord",
"WorldItemRecord",
"TypedPayload",
"DocumentSnapshot",
"IPersistenceProvider"
};
var violations = ProductFiles("Client")
.Select(file => (File: file, Source: SourceWithoutComments(file)))
.Where(value => ContainsAny(value.Source, forbidden))
.Select(value => Relative(value.File))
.ToArray();
Assert.IsEmpty(violations,
$"Client may depend only on snapshots, commands, IDs, and kernel results: {string.Join(", ", violations)}");
}
[TestMethod]
public void SnapshotContractsExposeNoServerOnlyNamesOrTypes()
{
var forbiddenPropertyNames = new HashSet<string>(StringComparer.Ordinal)
{
"SteamId",
"AccountId",
"IsDirty",
"SchemaState",
"BanExpiresAt",
"Traits",
"RevisionToken"
};
var forbiddenTypeNames = new HashSet<string>(StringComparer.Ordinal)
{
"Hexagon.V2.Domain.CharacterRecord",
"Hexagon.V2.Domain.InventoryRecord",
"Hexagon.V2.Domain.ItemRecord",
"Hexagon.V2.Domain.WorldItemRecord",
"Hexagon.V2.Domain.TypedPayload"
};
var snapshotTypes = typeof(PlayerPublicSnapshot).Assembly.GetTypes()
.Where(type => type.Namespace == "Hexagon.V2.Networking" &&
(type.Name.EndsWith("Snapshot", StringComparison.Ordinal) || type == typeof(SnapshotValue)))
.ToArray();
var violations = new List<string>();
foreach (var type in snapshotTypes)
{
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
if (forbiddenPropertyNames.Contains(property.Name))
violations.Add($"{type.Name}.{property.Name}");
if (ContainsForbiddenType(property.PropertyType, forbiddenTypeNames))
violations.Add($"{type.Name}.{property.Name}: {property.PropertyType.FullName}");
}
}
Assert.IsNotEmpty(snapshotTypes);
Assert.IsEmpty(violations,
$"Snapshots expose server-only members: {string.Join(", ", violations)}");
}
[TestMethod]
public void LegacyNamespacesAreReportedWithoutGatingV2()
{
var codeRoot = Path.Combine(ProductRoot(), "Code");
var legacyFiles = Directory.EnumerateFiles(codeRoot, "*.cs", SearchOption.AllDirectories)
.Where(file => !file.StartsWith(Path.Combine(codeRoot, "V2") + Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase))
.Where(file => Regex.IsMatch(SourceWithoutComments(file), @"\bnamespace\s+Hexagon(?:\.|;)",
RegexOptions.CultureInvariant))
.Select(file => Path.GetRelativePath(ProductRoot(), file))
.OrderBy(file => file, StringComparer.Ordinal)
.ToArray();
TestContext.WriteLine($"Legacy Hexagon namespace files (non-gating): {legacyFiles.Length}");
foreach (var file in legacyFiles.Take(25))
TestContext.WriteLine(file);
}
[TestMethod]
public void PersistenceHandoffDefersToRecoveryAndQuarantineIsOperatorArmed()
{
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
// The scene-handoff gate awaits the previous owner (bounded) and then defers ownership and
// integrity to the exclusive lease and WAL recovery — it must never fail-closed on the
// predecessor's drain outcome again (that was the self-poisoning wedge).
StringAssert.Contains( runtime, "AwaitPredecessorSettlementAsync" );
StringAssert.Contains( runtime, "SceneHandoffPolicy.EvaluatePredecessor" );
Assert.IsFalse( runtime.Contains( "did not drain cleanly", StringComparison.Ordinal ),
"The barrier must not fail-closed on a predecessor drain; the lease and recovery are the authorities." );
// Corruption recovery is operator-armed and one-shot, never automatic.
// The arming is consumed per host-start (read into a local, then reset) so it cannot linger
// and silently quarantine a later store.
StringAssert.Contains( runtime, "var quarantineCorruptStore = HexagonRuntimeOverrides.QuarantineCorruptStore" );
StringAssert.Contains( runtime, "HexagonRuntimeOverrides.QuarantineCorruptStore = false" );
}
[TestMethod]
public void HostConstructionOccursOnlyAfterConfigurationAndRecoveredDomainValidation()
{
var runtime = SourceWithoutComments( Path.Combine( V2Root(), "Runtime", "HexagonRuntimeSystem.cs" ) );
var configuration = runtime.IndexOf( "configuration.InitializeAsync", StringComparison.Ordinal );
var validation = runtime.IndexOf( "new DomainInvariantValidator", StringComparison.Ordinal );
var hostConstruction = runtime.IndexOf( "descriptor.CreateHostApplication", StringComparison.Ordinal );
Assert.IsGreaterThanOrEqualTo( 0, configuration );
Assert.IsLessThan( configuration, validation );
Assert.IsLessThan( hostConstruction, configuration );
}
private static bool ContainsForbiddenType(Type type, IReadOnlySet<string> forbidden)
{
if (type.FullName is not null && forbidden.Contains(type.FullName))
return true;
if (type.IsArray)
return ContainsForbiddenType(type.GetElementType()!, forbidden);
return type.IsGenericType && type.GetGenericArguments().Any(argument => ContainsForbiddenType(argument, forbidden));
}
private static bool ContainsAny(string source, params string[] values) =>
values.Any(value => source.Contains(value, StringComparison.Ordinal));
private static bool IsAllowedSandboxLayer(string file)
{
var relative = Path.GetRelativePath(V2Root(), file);
var separator = relative.IndexOfAny(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
var layer = separator < 0 ? relative : relative[..separator];
return layer is "Runtime" or "Infrastructure";
}
private static IEnumerable<string> ProductFiles(params string[] layers)
{
var roots = layers.Length == 0
? Directory.EnumerateDirectories(V2Root())
: layers.Select(layer => Path.Combine(V2Root(), layer));
return roots.Where(Directory.Exists)
.SelectMany(root => Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories));
}
private static string SourceWithoutComments(string file)
{
var source = File.ReadAllText(file);
return LineComments.Replace(BlockComments.Replace(source, string.Empty), string.Empty);
}
private static string Relative(string file) => Path.GetRelativePath(ProductRoot(), file);
private static string V2Root() => Path.Combine(ProductRoot(), "Code", "V2");
private static string ProductRoot()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
if (Directory.Exists(Path.Combine(current.FullName, "Code", "V2")))
return current.FullName;
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate the Hexagon product root from the test output directory.");
}
}
using Hexagon.V2.Composition;
using Hexagon.V2.Kernel.Configuration;
using Hexagon.V2.Kernel.Persistence;
using Hexagon.V2.Kernel.Schema;
using Hexagon.V2.Persistence;
using KernelConfigDefinition = Hexagon.V2.Kernel.Configuration.ConfigDefinition<int>;
namespace Hexagon.V2.Tests.Composition;
[TestClass]
public sealed class SchemaPersistenceAdapterTests
{
[TestMethod]
public void BindValidatesAndRegistersSchemaCodecAndTypedConfigs()
{
var compiled = CompileSchema(2);
var registry = new PersistedTypeRegistry();
var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 2,
PersistedValuePublication.Immutable,
upgrades: new Dictionary<int, Func<System.Text.Json.JsonElement, System.Text.Json.JsonElement>>
{
[1] = value => value
});
var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });
Assert.IsTrue(result.Succeeded, result.Error?.Message);
Assert.AreSame(registry, result.Value.Types);
Assert.AreEqual(typeof(Payload), registry.Resolve(new PersistedTypeKey("schema.payload")).ClrType);
Assert.AreEqual(4, result.Value.Configs.Require<int>("slots").Value.DefaultValue);
Assert.HasCount(1, result.Value.PersistenceConfigs);
var persistedConfig = result.Value.PersistenceConfigs["slots"];
var encodedConfig = persistedConfig.SerializeObject(7);
Assert.AreEqual(7, persistedConfig.DeserializeObject(encodedConfig));
}
[TestMethod]
public void VersionMismatchFailsBeforeMutatingRegistry()
{
var compiled = CompileSchema(2);
var registry = new PersistedTypeRegistry();
var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 1,
PersistedValuePublication.Immutable);
var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });
Assert.IsTrue(result.Failed);
Assert.IsEmpty(registry.Codecs);
StringAssert.Contains(result.Error!.Message, "does not match schema version");
}
[TestMethod]
public void MissingAndUnknownCodecsFailClosed()
{
var compiled = CompileSchema(1);
var missingRegistry = new PersistedTypeRegistry();
var unknownRegistry = new PersistedTypeRegistry();
var missing = SchemaPersistenceAdapter.Bind(compiled, missingRegistry,
Array.Empty<IPersistedTypeCodec>());
var unknown = SchemaPersistenceAdapter.Bind(compiled, unknownRegistry,
new IPersistedTypeCodec[]
{
new JsonPersistedTypeCodec<OtherPayload>(new PersistedTypeKey("schema.other"), 1,
PersistedValuePublication.Immutable)
});
Assert.IsTrue(missing.Failed);
Assert.IsTrue(unknown.Failed);
Assert.IsEmpty(missingRegistry.Codecs);
Assert.IsEmpty(unknownRegistry.Codecs);
}
[TestMethod]
public void ExistingCollisionFailsBeforeAddingAnySchemaCodec()
{
var compiled = CompileSchema(1);
var registry = new PersistedTypeRegistry()
.Register<OtherPayload>(new PersistedTypeKey("schema.payload"), 1,
PersistedValuePublication.Immutable);
var codec = new JsonPersistedTypeCodec<Payload>(new PersistedTypeKey("schema.payload"), 1,
PersistedValuePublication.Immutable);
var result = SchemaPersistenceAdapter.Bind(compiled, registry, new[] { codec });
Assert.IsTrue(result.Failed);
Assert.HasCount(1, registry.Codecs);
Assert.AreEqual(typeof(OtherPayload), registry.Codecs.Single().ClrType);
}
[TestMethod]
public void ConfigurationWithoutStablePersistenceTypeFailsBinding()
{
var compiled = SchemaCompiler.Compile( new UnsupportedConfigurationSchema() );
Assert.IsTrue( compiled.Succeeded, compiled.Error?.Message );
var result = SchemaPersistenceAdapter.Bind(
compiled.Value,
new PersistedTypeRegistry(),
Array.Empty<IPersistedTypeCodec>() );
Assert.IsTrue( result.Failed );
Assert.AreEqual( Hexagon.V2.Kernel.ErrorCode.ConfigurationInvalid, result.Error!.Code );
StringAssert.Contains( result.Error.Message, "stable persistence identity" );
}
private static CompiledSchema CompileSchema(int version)
{
var result = SchemaCompiler.Compile(new TestSchema(version));
Assert.IsTrue(result.Succeeded, result.Error?.Message);
return result.Value;
}
private sealed class TestSchema : IHexSchema
{
private readonly int _version;
public TestSchema(int version) => _version = version;
public string Id => "test_schema";
public void Configure(SchemaBuilder builder)
{
builder.RegisterPersistedType(
new PersistedTypeRegistration("schema.payload", typeof(Payload), _version));
builder.RegisterConfig(new KernelConfigDefinition("slots", 4, ConfigCodecs.Int32));
}
}
private sealed record Payload(string Value);
private sealed record OtherPayload(string Value);
private sealed class UnsupportedConfigurationSchema : IHexSchema
{
public string Id => "unsupported_config";
public void Configure( SchemaBuilder builder ) => builder.RegisterConfig(
new Hexagon.V2.Kernel.Configuration.ConfigDefinition<DateTimeOffset>(
"unsupported",
DateTimeOffset.UnixEpoch,
new UnsupportedConfigurationCodec() ) );
}
private sealed class UnsupportedConfigurationCodec : IConfigCodec<DateTimeOffset>
{
public Hexagon.V2.Kernel.OperationResult<string> Encode( DateTimeOffset value ) =>
Hexagon.V2.Kernel.OperationResult<string>.Success( value.ToString( "O" ) );
public Hexagon.V2.Kernel.OperationResult<DateTimeOffset> Decode( string encoded ) =>
Hexagon.V2.Kernel.OperationResult<DateTimeOffset>.Success( DateTimeOffset.Parse( encoded ) );
}
}
using System.Threading;
using System.Threading.Tasks;
using Hexagon.V2.Kernel;
namespace Hexagon.V2.Tests.Kernel;
[TestClass]
public sealed class AsyncOperationCaptureTests
{
[TestMethod]
public async Task SynchronousThrowIsCapturedAsAFailureOutcome()
{
var thrown = new InvalidOperationException( "boom" );
var outcome = await AsyncOperation.Capture( () => throw thrown );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public async Task SynchronousThrowIsCapturedAsAFailureOutcomeWithValue()
{
var thrown = new InvalidOperationException( "boom" );
var outcome = await AsyncOperation.Capture<int>( () => throw thrown );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public void CompletedOperationTakesTheSynchronousFastPath()
{
var task = AsyncOperation.Capture( () => ValueTask.CompletedTask );
Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
Assert.IsTrue( task.Result.Succeeded );
}
[TestMethod]
public void CompletedOperationWithValueTakesTheSynchronousFastPath()
{
var task = AsyncOperation.Capture( () => ValueTask.FromResult( 42 ) );
Assert.IsTrue( task.IsCompletedSuccessfully, "A completed ValueTask must not allocate a continuation." );
Assert.IsTrue( task.Result.Succeeded );
Assert.AreEqual( 42, task.Result.Value );
}
[TestMethod]
public async Task AsynchronousFaultIsUnwrappedFromItsAggregateException()
{
var thrown = new InvalidOperationException( "inner" );
var outcome = await AsyncOperation.Capture(
() => new ValueTask( Task.FromException( thrown ) ) );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public async Task AsynchronousFaultWithValueIsUnwrappedFromItsAggregateException()
{
var thrown = new InvalidOperationException( "inner" );
var outcome = await AsyncOperation.Capture(
() => new ValueTask<int>( Task.FromException<int>( thrown ) ) );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public async Task ThrowAfterAnAwaitPointIsCaptured()
{
var thrown = new InvalidOperationException( "late" );
var outcome = await AsyncOperation.Capture( async () =>
{
await Task.Yield();
throw thrown;
} );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public async Task CancellationBecomesAFailureOutcomeInsteadOfAThrow()
{
var canceled = new CancellationToken( canceled: true );
var outcome = await AsyncOperation.Capture(
() => new ValueTask( Task.FromCanceled( canceled ) ) );
Assert.IsFalse( outcome.Succeeded );
Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
}
[TestMethod]
public async Task CancellationWithValueBecomesAFailureOutcomeInsteadOfAThrow()
{
var canceled = new CancellationToken( canceled: true );
var outcome = await AsyncOperation.Capture(
() => new ValueTask<int>( Task.FromCanceled<int>( canceled ) ) );
Assert.IsFalse( outcome.Succeeded );
Assert.IsInstanceOfType<OperationCanceledException>( outcome.Exception );
}
[TestMethod]
public void CaptureSynchronousConvertsAThrowingActionIntoAFailure()
{
var thrown = new InvalidOperationException( "boom" );
var outcome = AsyncOperation.CaptureSynchronous( () => throw thrown );
Assert.IsFalse( outcome.Succeeded );
Assert.AreSame( thrown, outcome.Exception );
}
[TestMethod]
public void CaptureSynchronousReturnsTheProducedValueOnSuccess()
{
var outcome = AsyncOperation.CaptureSynchronous( () => 42 );
Assert.IsTrue( outcome.Succeeded );
Assert.AreEqual( 42, outcome.Value );
}
}
#nullable enable
using Hexagon.V2.Domain;
using Hexagon.V2.Networking;
namespace Hexagon.V2.Tests.Networking;
[TestClass]
public sealed class ConnectionSessionBoundaryTests
{
[TestMethod]
public void CharacterTransitionCancelsStableLeaseButNotConnectionLease()
{
using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
var firstCharacter = CharacterId.New();
var stable = boundary.Capture( firstCharacter, true );
var connection = boundary.Capture( firstCharacter, false );
boundary.ObserveCharacter( CharacterId.New() );
Assert.IsTrue( stable.CancellationToken.IsCancellationRequested );
Assert.IsFalse( connection.CancellationToken.IsCancellationRequested );
Assert.IsFalse( boundary.IsCurrent( stable ) );
Assert.IsTrue( boundary.IsCurrent( connection ) );
}
[TestMethod]
public void DisconnectCancelsEveryLeaseAndRejectsCurrentChecks()
{
using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
var stable = boundary.Capture( CharacterId.New(), true );
var connection = boundary.Capture( boundary.CharacterId, false );
boundary.Disconnect();
Assert.IsTrue( stable.CancellationToken.IsCancellationRequested );
Assert.IsTrue( connection.CancellationToken.IsCancellationRequested );
Assert.IsFalse( boundary.IsCurrent( stable ) );
Assert.IsFalse( boundary.IsCurrent( connection ) );
}
[TestMethod]
public void PublishedEpochOrdersStateAndOnlyAdvancesCharacterOnIdentityChange()
{
using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New() );
var character = CharacterId.New();
var first = boundary.Publish( null );
var second = boundary.Publish( character );
var third = boundary.Publish( character );
var fourth = boundary.Publish( null );
Assert.AreEqual( 0L, first.Character );
Assert.AreEqual( 1L, second.Character );
Assert.AreEqual( 1L, third.Character );
Assert.AreEqual( 2L, fourth.Character );
Assert.AreEqual( 1L, first.Revision );
Assert.AreEqual( 4L, fourth.Revision );
Assert.AreEqual( first.Connection, fourth.Connection );
}
[TestMethod]
public void ThrowingCancellationCallbackCannotEscapeDisconnectBoundary()
{
var diagnostics = new List<Exception>();
using var boundary = new ConnectionSessionBoundary( ConnectionEpoch.New(), diagnostics.Add );
var lease = boundary.Capture( CharacterId.New(), true );
using var registration = lease.CancellationToken.Register( () => throw new InvalidOperationException( "callback" ) );
boundary.Disconnect();
Assert.IsTrue( lease.CancellationToken.IsCancellationRequested );
Assert.HasCount( 1, diagnostics );
}
}
using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ModelPro.Vmdl;
[TestClass]
public class VmdlGeneratorTests
{
[TestMethod]
public void GeneratesValidVmdl_HullCollision()
{
var options = new MeshToModelOptions
{
Collision = CollisionMode.Hull,
ImportScale = 0.3937f,
AlignOriginX = AlignOrigin.BoundsCenter,
AlignOriginY = AlignOrigin.BoundsCenter,
AlignOriginZ = AlignOrigin.BoundsMin
};
var source = VmdlGenerator.Generate( "models/foo.fbx", options );
// Parseable, finds all nodes.
var doc = Kv3Document.Parse( source );
Assert.AreEqual( 1, doc.FindObjects( "RenderMeshFile" ).Count );
Assert.AreEqual( 1, doc.FindObjects( "PhysicsHullFromRender" ).Count );
Assert.AreEqual( 1, doc.FindObjects( "RenderMeshList" ).Count );
Assert.AreEqual( 1, doc.FindObjects( "PhysicsShapeList" ).Count );
var mesh = doc.FindObjects( "RenderMeshFile" ).Single();
Assert.AreEqual( "models/foo.fbx", ((Kv3Scalar)mesh.FindField( "filename" ).Value).Value );
Assert.AreEqual( "0.3937", ((Kv3Scalar)mesh.FindField( "import_scale" ).Value).Value );
Assert.AreEqual( "BoundsCenter", ((Kv3Scalar)mesh.FindField( "align_origin_x_type" ).Value).Value );
Assert.AreEqual( "BoundsMin", ((Kv3Scalar)mesh.FindField( "align_origin_z_type" ).Value).Value );
// Collision must carry the same align origin.
var hull = doc.FindObjects( "PhysicsHullFromRender" ).Single();
Assert.AreEqual( "BoundsCenter", ((Kv3Scalar)hull.FindField( "align_origin_x_type" ).Value).Value );
Assert.AreEqual( "BoundsCenter", ((Kv3Scalar)hull.FindField( "align_origin_y_type" ).Value).Value );
Assert.AreEqual( "BoundsMin", ((Kv3Scalar)hull.FindField( "align_origin_z_type" ).Value).Value );
}
[TestMethod]
public void GeneratesMeshCollision()
{
var options = new MeshToModelOptions
{
Collision = CollisionMode.Mesh,
ImportScale = 1.0f
};
var source = VmdlGenerator.Generate( "models/foo.fbx", options );
var doc = Kv3Document.Parse( source );
Assert.AreEqual( 1, doc.FindObjects( "PhysicsMeshFromRender" ).Count );
Assert.AreEqual( 0, doc.FindObjects( "PhysicsHullFromRender" ).Count );
}
[TestMethod]
public void GeneratesFileCollision()
{
var options = new MeshToModelOptions
{
Collision = CollisionMode.File,
ImportScale = 0.3937f
};
var source = VmdlGenerator.Generate( "models/foo.fbx", options );
var doc = Kv3Document.Parse( source );
Assert.AreEqual( 1, doc.FindObjects( "PhysicsHullFile" ).Count );
var hull = doc.FindObjects( "PhysicsHullFile" ).Single();
Assert.AreEqual( "models/foo.fbx", ((Kv3Scalar)hull.FindField( "filename" ).Value).Value );
Assert.AreEqual( "0.3937", ((Kv3Scalar)hull.FindField( "import_scale" ).Value).Value );
}
[TestMethod]
public void NoCollisionWhenNone()
{
var options = new MeshToModelOptions
{
Collision = CollisionMode.None,
ImportScale = 1.0f
};
var source = VmdlGenerator.Generate( "models/foo.fbx", options );
var doc = Kv3Document.Parse( source );
Assert.AreEqual( 0, doc.FindObjects( "PhysicsShapeList" ).Count );
Assert.AreEqual( 1, doc.FindObjects( "RenderMeshFile" ).Count );
}
[TestMethod]
public void GeneratedVmdlIsEditableByBulkEditor()
{
var options = new MeshToModelOptions
{
Collision = CollisionMode.Hull,
ImportScale = 0.3937f,
AlignOriginX = AlignOrigin.BoundsCenter,
AlignOriginY = AlignOrigin.BoundsCenter,
AlignOriginZ = AlignOrigin.BoundsMin
};
var source = VmdlGenerator.Generate( "models/foo.fbx", options );
// The generated file should load in the bulk editor and find its mesh entry.
var editor = VmdlBulkEditor.Load( source );
Assert.AreEqual( 1, editor.MeshEntryCount );
// And a further bulk edit should work on it.
var props = new MeshEntryProperties { ImportScale = 2.0f };
Assert.IsTrue( editor.Apply( props ) );
var parsed = Kv3Document.Parse( editor.Source );
var mesh = parsed.FindObjects( "RenderMeshFile" ).Single();
Assert.AreEqual( "2.0", ((Kv3Scalar)mesh.FindField( "import_scale" ).Value).Value );
}
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class TestInit
{
public static Sandbox.TestAppSystem AppSystem;
[AssemblyInitialize]
public static void AssemblyInitialize( TestContext context )
{
AppSystem = new Sandbox.TestAppSystem();
AppSystem.Init();
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
AppSystem.Shutdown();
}
}