acdream/tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs
Erik 6dcb94ac1b test(runtime): restore the world-frame precondition across first-entry fixtures
670f307c made remote first-entry placement resolve its landblock-local
CreateObject origin through Runtime's world frame
(RuntimeSetPositionState.PrepareMover:1526-1544) and return
RetrySetupUnavailable until that frame exists. Only the accepted local-player
Create publishes it (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
RuntimePhysicsState.ObserveLocalWorldFrame).

Fixtures that drive remote conductors in a world with no local player - a
state production never occupies, since the player's own Create always precedes
broadcast Creates - therefore parked forever on RetrySetupUnavailable. Their
initial-create residences never retired, which cascaded into rejected
appearance updates, missing canonical bodies, unconverged ownership ledgers,
and a GameRuntime teardown that could not complete stage 10.

The measured blast radius was far larger than the handoff recorded. It claimed
"six selected fixture failures"; a baseline run found 43. The App suite was
fully green at 01f4791e and 670f307c broke 28 tests at once; the Runtime suite
lost 13, twelve of them in RuntimeRemoteFirstEntryStateTests - the exact
conductor that commit gated. Both commits were verified on focused runs only.

The production gate is correct, so nothing here weakens it. It matches App's
own coordinate owner: LiveWorldOriginState is initialized once from the local
player's spawn (LiveEntityHydrationPorts.cs:226) and rebased only by
StreamingOriginRecenterCoordinator.Advance at a teleport boundary - exactly
ObserveLocalWorldFrame's semantics. Every fixture is repaired by supplying the
missing precondition beside the resident landblock it already models, and not
one expected value or assertion was changed.

The mechanism shipped with zero tests. RuntimeWorldFrameTests now pins its
contract: the local player publishes the frame, remotes never do, neighbouring
landblocks convert at 192 m per step, ordinary movement across a landblock
boundary must NOT rebase it, an accepted teleport must, and a zero cell id
neither publishes nor resolves. That "no rebase on ordinary movement" rule is
load-bearing - if it and LiveWorldOriginState ever disagree, remote objects
are placed a multiple of 192 m from where the world is streamed.

Runtime 1,009/1,009; App 4,048 passed / 3 skipped.

Refs #281.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 13:32:28 +02:00

792 lines
27 KiB
C#

using System.Net;
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.Session;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.Session;
public sealed class RuntimeLiveEntitySessionControllerTests
{
[Fact]
public void DirectSinkOwnsCanonicalCreateUpdateDeleteWithoutProjection()
{
// C3c: the direct sink's Create now enters the canonical initial
// residence; this test drives the remote first-entry conductor to
// completion (the direct-host pump's job) before the follow-up
// Position flows the ordinary post-residence path.
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
CommitLandblockCollision(runtime, 0x01010000u);
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
// C3c-R1 review R3: the residence route requires a drive-backed
// world projection — a content-less (projection-less) controller
// keeps the pre-flip legacy registration instead (see
// ContentLessDirectSink_KeepsPreFlipLegacyRegistration).
var controller = new RuntimeLiveEntitySessionController(
runtime,
session,
worldProjection: new FixtureWorldProjection());
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(0x70000001u, incarnation: 1);
sink.Spawned(spawn);
drive.DriveAll();
Assert.Equal(0, drive.PendingCount);
DrainPlacementFifo(runtime);
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
spawn.Guid,
spawn.Position!.Value with
{
PositionX = 20f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0));
Assert.Equal(1, runtime.Entities.Count);
Assert.Equal(1, runtime.Inventory.ObjectCount);
Assert.True(
runtime.EntityObjects.Entities.TryGetActive(
spawn.Guid,
out RuntimeEntityRecord canonical));
Assert.Equal(
20f,
canonical.Snapshot.Position!.Value.PositionX);
sink.Deleted(new DeleteObject.Parsed(spawn.Guid, 1));
Assert.Equal(0, runtime.Entities.Count);
Assert.Equal(0, runtime.Inventory.ObjectCount);
Assert.Equal(
0,
runtime.EntityObjects.Entities.PendingTeardownCount);
}
/// <summary>
/// C3c-R1 review R3: a CONTENT-LESS headless host (validated-legal
/// configuration: HeadlessConfigurationLoader accepts a null
/// process.content) builds no world projection and no first-entry
/// drive. Its Creates must keep the exact pre-flip legacy registration
/// — the accepted frame commits directly (FullCellId derives from the
/// wire cell at registration), no residence lease ever opens, and the
/// entity/residence/drive ledgers stay at zero — because a residence
/// with no drive to pump it would park every Create (and every packet
/// FIFO'd behind its pending residence) forever.
/// </summary>
[Fact]
public void ContentLessDirectSink_KeepsPreFlipLegacyRegistration()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
var controller = new RuntimeLiveEntitySessionController(
runtime,
session);
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(0x70000003u, incarnation: 1);
sink.Spawned(spawn);
Assert.True(
runtime.EntityObjects.Entities.TryGetActive(
spawn.Guid,
out RuntimeEntityRecord canonical));
Assert.Equal(
spawn.Position!.Value.LandblockId,
canonical.FullCellId);
RuntimeEntityObjectOwnershipSnapshot ownership =
runtime.EntityObjects.CaptureOwnership();
Assert.Equal(0, ownership.InitialCreateResidenceLeaseCount);
Assert.Equal(0, ownership.FirstEntryDrivePendingCount);
Assert.Equal(1, runtime.Entities.Count);
Assert.Equal(1, runtime.Inventory.ObjectCount);
// Position packets flow the ordinary immediate path — nothing is
// FIFO'd behind a pending residence.
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
spawn.Guid,
spawn.Position!.Value with
{
PositionX = 20f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0));
Assert.Equal(
20f,
canonical.Snapshot.Position!.Value.PositionX);
sink.Deleted(new DeleteObject.Parsed(spawn.Guid, 1));
Assert.Equal(0, runtime.Entities.Count);
Assert.Equal(
0,
runtime.EntityObjects.Entities.PendingTeardownCount);
}
[Fact]
public void DirectSinkCompletesExactPortalAndSendsLoginComplete()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000001u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
var gameActions = new List<byte[]>();
session.GameActionCapture = body => gameActions.Add(body);
var controller = new RuntimeLiveEntitySessionController(
runtime,
session);
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(playerGuid, incarnation: 1);
sink.Spawned(spawn);
Assert.Single(gameActions);
Assert.Equal(GameActionLoginComplete.Build(), gameActions[0]);
sink.Spawned(spawn);
Assert.Single(gameActions);
gameActions.Clear();
sink.TeleportStarted(1u);
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
playerGuid,
spawn.Position!.Value with
{
LandblockId = 0x01020001u,
PositionX = 30f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0));
Assert.Single(gameActions);
Assert.Equal(GameActionLoginComplete.Build(), gameActions[0]);
Assert.True(runtime.TransitOwner.CaptureOwnership().IsSessionIdle);
Assert.True(runtime.Portal.Snapshot.Completed);
Assert.Equal(0x01020001u, runtime.Portal.Snapshot.DestinationCell);
}
[Fact]
public void DirectSinkProjectsAcceptedLocalWorldStateThroughOneHostSeam()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000002u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
session.GameActionCapture = _ => { };
var projection = new FixtureWorldProjection();
var controller = new RuntimeLiveEntitySessionController(
runtime,
session,
worldProjection: projection);
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(playerGuid, incarnation: 1);
sink.Spawned(spawn);
sink.TeleportStarted(1u);
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
playerGuid,
spawn.Position!.Value with
{
LandblockId = 0x01020001u,
PositionX = 30f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0));
Assert.Equal(1, projection.SpawnCount);
Assert.Equal(1, projection.PositionCount);
Assert.Equal(1, projection.TeleportStartCount);
Assert.Equal(1, projection.PrepareCount);
Assert.True(projection.LastSpawnWasLocal);
Assert.True(projection.LastPositionWasLocal);
Assert.Equal(
PositionTimestampDisposition.Apply,
projection.LastPositionDisposition);
Assert.Equal(playerGuid, projection.LastRecord?.ServerGuid);
Assert.Equal(0x01020001u, projection.LastDestination.CellId);
Assert.True(runtime.TransitOwner.CaptureOwnership().IsSessionIdle);
Assert.True(runtime.Portal.Snapshot.Completed);
}
/// <summary>
/// C3c-R1 review F6: the drive controller outlives its session routes,
/// so "session reset precedes a new route" is an asserted latch, not a
/// silent assumption — a second route cannot attach before the prior
/// route detached, and a route that never owned the drive cannot clear
/// the live route's tracked entries.
/// </summary>
[Fact]
public void FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
var routeA = new object();
var routeB = new object();
drive.AttachRoute(routeA);
// Re-attaching the same owner is a no-op; a SECOND route asserts.
drive.AttachRoute(routeA);
Assert.Throws<InvalidOperationException>(
() => drive.AttachRoute(routeB));
_ = runtime.EntityObjects.RegisterEntityWithInitialResidence(
Spawn(0x70000004u, incarnation: 1),
isLocalPlayer: false);
Assert.Equal(1, drive.PendingCount);
// A non-owner detach (never-attached / displaced route rollback)
// must not clear the live route's entries.
drive.DetachRoute(routeB);
Assert.Equal(1, drive.PendingCount);
drive.DetachRoute(routeA);
Assert.Equal(0, drive.PendingCount);
// After the owner detached, a replacement route may attach.
drive.AttachRoute(routeB);
drive.DetachRoute(routeB);
}
[Fact]
public void FirstEntryDriveSignalsLocalCompletionOnceAfterCanonicalPlacement()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000003u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
CommitLandblockCollision(runtime, 0x01010000u);
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
var route = new object();
int completed = 0;
drive.AttachRoute(route, record =>
{
Assert.Equal(playerGuid, record.ServerGuid);
completed++;
});
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
var controller = new RuntimeLiveEntitySessionController(
runtime,
session,
worldProjection: new FixtureWorldProjection());
LiveEntitySessionSink sink = controller.CreateSink();
sink.Spawned(Spawn(playerGuid, incarnation: 1));
Assert.Equal(0, completed);
DrainFirstEntry(runtime, drive);
Assert.Equal(1, completed);
Assert.Equal(0, drive.PendingCount);
drive.DetachRoute(route);
}
/// <summary>
/// C3c: initial-residence admission requires a live session generation
/// (RuntimeInitialCreateResidenceState.CanAcceptCreate), so these direct
/// sink tests start one through the same fixture-session shape
/// DirectGameRuntimeCommandAdapterTests uses.
/// </summary>
private sealed class StartedRuntime : IDisposable
{
internal required GameRuntime Runtime { get; init; }
internal required LiveSessionHost Live { get; init; }
public void Dispose()
{
_ = Live.Stop(Runtime.Generation);
Runtime.Dispose();
}
}
private static StartedRuntime StartRuntime()
{
var operations = new FixtureGameplayOperations();
var sessionOperations = new FixtureSessionOperations();
var runtime = new GameRuntime(new GameRuntimeDependencies(
operations,
operations,
operations,
operations,
SessionOperations: sessionOperations));
operations.Bind(runtime);
var resetHost = new FixtureResetHost();
var options = new LiveSessionConnectOptions(
true,
"127.0.0.1",
9000,
"account",
"password");
var live = new LiveSessionHost(
runtime.Session,
new LiveSessionHostBindings(
new LiveSessionRoutingFactories(
_ => new FixtureEventRoute(),
_ => new FixtureCommandRoute()),
generation => runtime.ResetGeneration(generation, resetHost),
new LiveSessionSelectionBindings(
id => runtime.PlayerIdentity.ServerGuid = id,
_ => { },
runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
_ => { },
_ => { },
runtime.ActionOwner.Combat.Clear),
new LiveSessionEnteredWorldBindings(
_ => { },
() => { },
() => { },
_ => { },
() => { }),
(_, _, _) => { },
() => { }),
options);
LiveSessionStartResult startResult = live.Start(options);
Assert.Equal(LiveSessionStartStatus.Connected, startResult.Status);
Assert.NotEqual(0UL, runtime.Generation.Value);
return new StartedRuntime { Runtime = runtime, Live = live };
}
private static void CommitLandblockCollision(
GameRuntime runtime,
uint landblockId)
{
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
landblockId, 1UL);
runtime.EntityObjects.Physics.Engine.AddLandblock(
landblockId,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
landblockId, 1UL, ready: true);
// 670f307c: a remote conductor resolves its landblock-local Create
// origin through Runtime's world frame
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
// RetrySetupUnavailable until that frame exists. Production publishes
// it from the accepted local-player Create
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570), which
// always precedes broadcast Creates. Model it with the resident
// landblock it belongs to, so these direct-sink tests keep asserting
// exact single-entity ledgers.
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
landblockId | 0x0001u,
teleportAdvanced: false);
}
private static RuntimeFirstEntryDriveController CreateDrive(
GameRuntime runtime) =>
new(
runtime.EntityObjects,
runtime.Clock,
new UnusedCollisionSource(),
() => PlayerMovementConstructionOptions.Fallback,
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
private static void DrainFirstEntry(
GameRuntime runtime,
RuntimeFirstEntryDriveController drive)
{
for (int attempt = 0; attempt < 8 && drive.PendingCount != 0; attempt++)
{
drive.DriveAll();
DrainPlacementFifo(runtime);
}
Assert.Equal(0, drive.PendingCount);
}
/// <summary>
/// Drains/acknowledges every still-pending placement receipt (the
/// ExecutorCompleted correlation is reaped by its acknowledgement) the
/// way a host subscription would.
/// </summary>
private static void DrainPlacementFifo(GameRuntime runtime)
{
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
out AcDream.Runtime.Physics.RuntimePlacementProjectionSnapshot head))
{
if (!runtime.EntityObjects.Physics.SetPosition
.AcknowledgeProjection(head.Token))
{
break;
}
}
}
private sealed class UnusedCollisionSource
: AcDream.Content.IPreparedCollisionSource
{
public AcDream.Content.PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
AcDream.Content.PreparedAssetPresence.Available;
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision>.Missing;
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatGfxObjCollisionAsset> ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
default;
public void Dispose()
{
}
}
private sealed class FixtureSessionOperations : ILiveSessionOperations
{
public IPEndPoint ResolveEndpoint(string host, int port) =>
new(IPAddress.Loopback, port);
public WorldSession CreateSession(IPEndPoint endpoint) =>
new(endpoint, new FixtureTransport());
public void Connect(WorldSession session, string user, string password)
{
}
public CharacterList.Parsed GetCharacters(WorldSession session) =>
new(
0u,
[new CharacterList.Character(0x50000001u, "Direct", 0u)],
[],
11,
"account",
true,
true);
public void EnterWorld(WorldSession session, int activeCharacterIndex)
{
}
public void Tick(WorldSession session)
{
}
public void DisposeSession(WorldSession session) =>
session.Dispose();
}
private sealed class FixtureEventRoute : ILiveSessionEventRouting
{
public void Attach()
{
}
public void Dispose()
{
}
}
private sealed class FixtureCommandRoute : ILiveSessionCommandRouting
{
public void Activate()
{
}
public void Dispose()
{
}
}
private sealed class FixtureResetHost : IRuntimeGenerationResetHost
{
public void RetireEntityProjection(RuntimeEntityRecord entity)
{
}
public void DrainEntityProjectionBoundary()
{
}
public void CompleteEntityProjectionRetirement()
{
}
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort incarnation)
{
var position = new CreateObject.ServerPosition(
0x01010001u,
10f,
10f,
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: incarnation);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: null,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
null,
[],
[],
[],
null,
null,
"direct entity",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed class FixtureTransport : IWorldSessionTransport
{
public void Send(ReadOnlySpan<byte> datagram)
{
}
public void Send(
IPEndPoint remote,
ReadOnlySpan<byte> datagram)
{
}
public int Receive(
Span<byte> destination,
TimeSpan timeout,
out IPEndPoint? from)
{
from = null;
return -1;
}
public ValueTask<NetReceiveResult> ReceiveAsync(
Memory<byte> destination,
CancellationToken cancellationToken) =>
ValueTask.FromException<NetReceiveResult>(
new OperationCanceledException(cancellationToken));
public void Dispose()
{
}
}
private sealed class FixtureWorldProjection
: IRuntimeDirectWorldProjection
{
public int SpawnCount { get; private set; }
public int PositionCount { get; private set; }
public int TeleportStartCount { get; private set; }
public int PrepareCount { get; private set; }
public bool LastSpawnWasLocal { get; private set; }
public bool LastPositionWasLocal { get; private set; }
public PositionTimestampDisposition LastPositionDisposition
{
get;
private set;
}
public RuntimeEntityRecord? LastRecord { get; private set; }
public RuntimeTeleportDestination LastDestination { get; private set; }
public void ProjectSpawn(
RuntimeEntityRecord record,
bool isLocalPlayer)
{
SpawnCount++;
LastRecord = record;
LastSpawnWasLocal = isLocalPlayer;
}
public void ProjectPosition(
RuntimeEntityRecord record,
bool isLocalPlayer,
PositionTimestampDisposition disposition)
{
PositionCount++;
LastRecord = record;
LastPositionWasLocal = isLocalPlayer;
LastPositionDisposition = disposition;
}
public void BeginTeleport() => TeleportStartCount++;
public RuntimeDestinationReadiness PrepareDestination(
long revealGeneration,
RuntimeTeleportDestination destination)
{
PrepareCount++;
LastDestination = destination;
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
return new RuntimeDestinationReadiness(
revealGeneration,
destination.CellId,
indoor,
IsUnhydratable: false,
RequiredRenderRadius: indoor ? 0 : 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
}
}
private sealed class FixtureGameplayOperations
: IRuntimeCombatAttackOperations,
IRuntimeCombatTargetOperations,
IRuntimeCombatModeOperations,
IRuntimeSpellCastOperations
{
private GameRuntime? _runtime;
public void Bind(GameRuntime runtime) => _runtime = runtime;
public bool CanStartAttack() => false;
public void PrepareAttackRequest()
{
}
public bool SendAttack(AttackHeight height, float power) => false;
public void SendCancelAttack()
{
}
public bool IsDualWield => false;
public bool PlayerReadyForAttack => false;
public bool AutoRepeatAttack => false;
public bool AutoTarget => false;
public uint? SelectClosestTarget() => null;
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
public void NotifyExplicitCombatModeRequest()
{
}
public void SendChangeCombatMode(CombatMode mode)
{
}
public uint LocalPlayerId =>
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
public bool CanSend => false;
public bool HasRequiredComponents(uint spellId) => false;
public bool IsTargetCompatible(
uint targetId,
SpellMetadata spell,
bool showMessage) => false;
public void StopCompletely()
{
}
public void SendUntargeted(uint spellId)
{
}
public void SendTargeted(uint targetId, uint spellId)
{
}
public void DisplayMessage(string message)
{
}
public void IncrementBusy()
{
}
}
}