acdream/tests/AcDream.Runtime.Tests/RuntimeSimulationFixtureHostTests.cs
Erik cdee7a4b49 refactor(runtime): close simulation ownership
Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order.
2026-07-26 15:53:31 +02:00

384 lines
13 KiB
C#

using System.Numerics;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Spells;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests;
public sealed class RuntimeSimulationFixtureHostTests
{
[Fact]
public void RuntimeOnlyHostDrivesCompleteJ5SimulationWithoutPresentation()
{
var host = new RuntimeOnlySimulationFixtureHost();
host.Move();
host.Use(0x70000011u);
host.Attack();
host.Cast();
host.AdvanceProjectile(failProjection: false);
Assert.Equal(
[
"movement:run",
"use:70000011",
"attack:prepare",
"attack:Medium:0.5",
"cast:stop",
"cast:1",
"cast:busy",
"projectile:ack",
],
host.Trace);
string[] loaded = AppDomain.CurrentDomain.GetAssemblies()
.Select(static assembly =>
assembly.GetName().Name ?? string.Empty)
.ToArray();
Assert.DoesNotContain(loaded, static name =>
name.StartsWith(
"AcDream.App",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"AcDream.UI.",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"Silk.NET",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"OpenAL",
StringComparison.OrdinalIgnoreCase)
|| name.StartsWith(
"ImGui",
StringComparison.OrdinalIgnoreCase));
host.Dispose();
Assert.True(host.CaptureOwnership().IsConverged);
}
[Fact]
public void ProjectionAcknowledgementFailureStillAllowsTerminalConvergence()
{
var host = new RuntimeOnlySimulationFixtureHost();
Assert.Throws<InvalidOperationException>(() =>
host.AdvanceProjectile(failProjection: true));
host.Dispose();
RuntimeSimulationOwnershipSnapshot ownership =
host.CaptureOwnership();
Assert.True(ownership.IsConverged);
Assert.Equal(0, ownership.Physics.SpatialProjectileCount);
Assert.Equal(0, ownership.EntityObjects.ActiveEntityCount);
}
private sealed class RuntimeOnlySimulationFixtureHost :
IRuntimeInteractionTransport,
IRuntimeCombatAttackOperations,
IRuntimeCombatTargetOperations,
IRuntimeCombatModeOperations,
IRuntimeSpellCastOperations,
IDisposable
{
private const uint ProjectileGuid = 0x70000021u;
private double _now = 10d;
private uint _sequence;
private bool _disposed;
public RuntimeOnlySimulationFixtureHost()
{
EntityObjects = new RuntimeEntityObjectLifetime();
Inventory = new RuntimeInventoryState(EntityObjects);
Character = new RuntimeCharacterState();
Character.Spellbook.InstallMetadata(SpellMetadata());
Character.Spellbook.OnSpellLearned(1u);
Communication = new RuntimeCommunicationState();
Actions = new RuntimeActionState(
Inventory.Transactions,
Character.Spellbook,
this,
this,
this,
this,
now: () => _now);
Movement = new RuntimeLocalPlayerMovementState();
}
public RuntimeEntityObjectLifetime EntityObjects { get; }
public RuntimeInventoryState Inventory { get; }
public RuntimeCharacterState Character { get; }
public RuntimeCommunicationState Communication { get; }
public RuntimeActionState Actions { get; }
public RuntimeLocalPlayerMovementState Movement { get; }
public List<string> Trace { get; } = [];
public void Move()
{
Assert.True(Movement.Execute(
RuntimeMovementCommand.ToggleRunLock));
Assert.True(Movement.AutoRunActive);
Trace.Add("movement:run");
}
public void Use(uint serverGuid)
{
ItemUseRequestReservation reservation =
Actions.Transactions.BeginUseRequestReservation();
RuntimeInteractionDispatchResult result =
Actions.Transactions.TryDispatchUse(
serverGuid,
ownedByPlayer: false,
useable: true,
reservation,
this,
out _);
Assert.Equal(
RuntimeInteractionDispatchResult.Dispatched,
result);
Actions.Transactions.CompleteUse(0u);
}
public void Attack()
{
Actions.Combat.SetCombatMode(CombatMode.Melee);
Actions.CombatAttack.SetDesiredPower(0.5f);
Actions.CombatAttack.PressAttack(AttackHeight.Medium);
_now += 0.5d;
Actions.CombatAttack.ReleaseAttack();
}
public void Cast() =>
Assert.Equal(
CastRequestResult.Sent,
Actions.SpellCast.Cast(1u));
public void AdvanceProjectile(bool failProjection)
{
RuntimeEntityRecord record =
EntityObjects.RegisterEntity(
Spawn(ProjectileGuid, 1)).Canonical!;
PhysicsBody body =
EntityObjects.Physics.GetOrCreatePhysicsBody(
record,
static canonical =>
{
var created = new PhysicsBody
{
Position = new Vector3(10f, 20f, 5f),
Orientation = Quaternion.Identity,
InWorld = true,
TransientState =
TransientStateFlags.Active,
};
created.SnapToCell(
canonical.FullCellId,
created.Position,
created.Position);
return created;
});
_ = EntityObjects.Physics.BindProjectile(
record,
body,
new ProjectileCollisionSphere(
Vector3.Zero,
0.25f));
EntityObjects.Physics.AcknowledgeSpatialProjection(
record,
spatial: true);
var updater =
new RuntimeProjectilePhysicsUpdater(
EntityObjects.Physics);
Assert.True(updater.TryBegin(
record,
quantum: 0.05f,
record.ObjectClockEpoch,
externalOwnerValid: null,
out RuntimeProjectilePhysicsCommit commit));
_ = updater.Complete(
commit,
liveCenterX: 1,
liveCenterY: 1,
_ =>
{
if (failProjection)
{
throw new InvalidOperationException(
"projection acknowledgement");
}
Trace.Add("projectile:ack");
return true;
});
}
public RuntimeSimulationOwnershipSnapshot CaptureOwnership() =>
RuntimeSimulationOwnership.Capture(
EntityObjects,
Inventory,
Character,
Communication,
Actions,
Movement);
public bool IsInWorld => true;
public uint LocalPlayerId => 0x50000001u;
public bool CanSend => true;
public bool IsDualWield => false;
public bool PlayerReadyForAttack => true;
public bool AutoRepeatAttack => false;
public bool AutoTarget => false;
public bool TrySendUse(uint serverGuid, out uint sequence)
{
sequence = ++_sequence;
Trace.Add($"use:{serverGuid:X8}");
return true;
}
public bool TrySendPickup(
uint itemGuid,
uint destinationContainerId,
int placement,
out uint sequence)
{
sequence = ++_sequence;
return true;
}
public bool CanStartAttack() => true;
public void PrepareAttackRequest() =>
Trace.Add("attack:prepare");
public bool SendAttack(AttackHeight height, float power)
{
Trace.Add($"attack:{height}:{power:0.0}");
return true;
}
public void SendCancelAttack() { }
public uint? SelectClosestTarget() => null;
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
public void NotifyExplicitCombatModeRequest() { }
public void SendChangeCombatMode(CombatMode mode) { }
public bool HasRequiredComponents(uint spellId) => true;
public bool IsTargetCompatible(
uint targetId,
SpellMetadata spell,
bool showMessage) => true;
public void StopCompletely() =>
Trace.Add("cast:stop");
public void SendUntargeted(uint spellId) =>
Trace.Add($"cast:{spellId}");
public void SendTargeted(uint targetId, uint spellId) =>
Trace.Add($"cast:{targetId}:{spellId}");
public void DisplayMessage(string message) =>
Trace.Add($"message:{message}");
public void IncrementBusy()
{
Inventory.Transactions.IncrementBusyCount();
Trace.Add("cast:busy");
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
Movement.Dispose();
Actions.Dispose();
Communication.Dispose();
Character.Dispose();
Inventory.Dispose();
EntityObjects.Dispose();
}
private static SpellTable SpellMetadata()
{
const string header =
"Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]";
const string row = "1,Runtime Fixture,0x0,true,0x0";
return SpellTable.LoadFromReader(
new System.IO.StringReader($"{header}\n{row}"));
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance)
{
var position = new CreateObject.ServerPosition(
0x01010001u,
10f,
20f,
5f,
1f,
0f,
0f,
0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: (uint)(
PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile),
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: new Vector3(10f, 0f, 0f),
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"runtime projectile",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}
}