feat(headless): complete portable single-session host
This commit is contained in:
parent
fbebb91848
commit
f8cb840fb1
30 changed files with 3571 additions and 32 deletions
427
src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs
Normal file
427
src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
using AcDream.Core.Net;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Session;
|
||||
|
||||
/// <summary>
|
||||
/// Typed command surface for a presentation-free Runtime host. K1 supplies
|
||||
/// session, local chat, and recall operations; later Slice-K work extends the
|
||||
/// same adapter rather than introducing a second bot command model.
|
||||
/// </summary>
|
||||
public sealed class DirectGameRuntimeCommandAdapter
|
||||
: IGameRuntimeCommands,
|
||||
IRuntimeSessionCommands,
|
||||
IRuntimeSelectionCommands,
|
||||
IRuntimeCombatCommands,
|
||||
IRuntimeMagicCommands,
|
||||
IRuntimeMovementCommands,
|
||||
IRuntimeChatCommands,
|
||||
IRuntimePortalCommands,
|
||||
IRuntimeInventoryStateCommands,
|
||||
IRuntimeSpellbookCommands,
|
||||
IRuntimeCharacterCommands,
|
||||
IRuntimeSocialCommands
|
||||
{
|
||||
private sealed class CommandRoute(
|
||||
DirectGameRuntimeCommandAdapter owner,
|
||||
WorldSession session,
|
||||
RuntimeGenerationToken generation)
|
||||
: ILiveSessionCommandRouting
|
||||
{
|
||||
private bool _active;
|
||||
|
||||
public void Activate()
|
||||
{
|
||||
if (_active)
|
||||
return;
|
||||
owner.Activate(session, generation, this);
|
||||
_active = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
owner.Deactivate(this);
|
||||
_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly GameRuntime _runtime;
|
||||
private readonly IRuntimeSessionCommands _sessionCommands;
|
||||
private WorldSession? _session;
|
||||
private CommandRoute? _route;
|
||||
private RuntimeGenerationToken _routeGeneration;
|
||||
|
||||
public DirectGameRuntimeCommandAdapter(
|
||||
GameRuntime runtime,
|
||||
IRuntimeSessionCommands sessionCommands)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_sessionCommands = sessionCommands
|
||||
?? throw new ArgumentNullException(nameof(sessionCommands));
|
||||
}
|
||||
|
||||
public IRuntimeSessionCommands Session => this;
|
||||
public IRuntimeSelectionCommands Selection => this;
|
||||
public IRuntimeCombatCommands Combat => this;
|
||||
public IRuntimeMagicCommands Magic => this;
|
||||
public IRuntimeMovementCommands Movement => this;
|
||||
public IRuntimeChatCommands Chat => this;
|
||||
public IRuntimePortalCommands Portal => this;
|
||||
public IRuntimeInventoryStateCommands InventoryState => this;
|
||||
public IRuntimeSpellbookCommands Spellbook => this;
|
||||
public IRuntimeCharacterCommands Character => this;
|
||||
public IRuntimeSocialCommands Social => this;
|
||||
|
||||
public ILiveSessionCommandRouting CreateRoute(WorldSession session)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
return new CommandRoute(this, session, _runtime.Generation);
|
||||
}
|
||||
|
||||
public RuntimeSessionStartResult Start(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
|
||||
RuntimeSessionStartResult result =
|
||||
_sessionCommands.Start(expectedGeneration);
|
||||
_runtime.EventSink.EmitLifecycle(
|
||||
previous,
|
||||
_runtime.Lifecycle.State);
|
||||
return result;
|
||||
}
|
||||
|
||||
public RuntimeSessionStartResult Reconnect(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
|
||||
RuntimeSessionStartResult result =
|
||||
_sessionCommands.Reconnect(expectedGeneration);
|
||||
_runtime.EventSink.EmitLifecycle(
|
||||
previous,
|
||||
_runtime.Lifecycle.State);
|
||||
return result;
|
||||
}
|
||||
|
||||
public RuntimeTeardownAcknowledgement Stop(
|
||||
RuntimeGenerationToken expectedGeneration)
|
||||
{
|
||||
RuntimeLifecycleState previous = _runtime.Lifecycle.State;
|
||||
RuntimeTeardownAcknowledgement result =
|
||||
_sessionCommands.Stop(expectedGeneration);
|
||||
_runtime.EventSink.EmitLifecycle(
|
||||
previous,
|
||||
_runtime.Lifecycle.State);
|
||||
return result;
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeChatCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate =
|
||||
Validate(expectedGeneration, out WorldSession? session);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
if (string.IsNullOrWhiteSpace(command.Text))
|
||||
return EmitUnsupported(
|
||||
RuntimeCommandDomain.Chat,
|
||||
(int)command.Channel,
|
||||
RuntimeCommandStatus.Rejected);
|
||||
|
||||
switch (command.Channel)
|
||||
{
|
||||
case RuntimeChatChannel.Say:
|
||||
session!.SendTalk(command.Text);
|
||||
break;
|
||||
case RuntimeChatChannel.Tell
|
||||
when !string.IsNullOrWhiteSpace(command.TargetName):
|
||||
session!.SendTell(command.TargetName, command.Text);
|
||||
break;
|
||||
default:
|
||||
return EmitUnsupported(
|
||||
RuntimeCommandDomain.Chat,
|
||||
(int)command.Channel);
|
||||
}
|
||||
|
||||
_runtime.EventSink.EmitCommand(
|
||||
RuntimeCommandDomain.Chat,
|
||||
(int)command.Channel,
|
||||
RuntimeCommandStatus.Accepted,
|
||||
text: command.Text);
|
||||
return Result(RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimePortalCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate =
|
||||
Validate(expectedGeneration, out WorldSession? session);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case RuntimePortalCommand.RecallLifestone:
|
||||
session!.SendTeleportToLifestone();
|
||||
break;
|
||||
case RuntimePortalCommand.RecallMarketplace:
|
||||
session!.SendTeleportToMarketplace();
|
||||
break;
|
||||
case RuntimePortalCommand.RecallHouse:
|
||||
session!.SendTeleportToHouse();
|
||||
break;
|
||||
case RuntimePortalCommand.RecallMansion:
|
||||
session!.SendTeleportToMansion();
|
||||
break;
|
||||
default:
|
||||
return EmitUnsupported(
|
||||
RuntimeCommandDomain.Portal,
|
||||
(int)command);
|
||||
}
|
||||
|
||||
_runtime.EventSink.EmitCommand(
|
||||
RuntimeCommandDomain.Portal,
|
||||
(int)command,
|
||||
RuntimeCommandStatus.Accepted);
|
||||
return Result(RuntimeCommandStatus.Accepted);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeSelectionCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Selection,
|
||||
(int)command);
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeCombatCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Combat,
|
||||
(int)command);
|
||||
|
||||
public RuntimeCommandResult ExecuteAttack(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeCombatAttackInput command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Combat,
|
||||
(int)command.Command);
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeMagicCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Magic,
|
||||
operation: 0,
|
||||
command.SpellId);
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeMovementCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Movement,
|
||||
(int)command);
|
||||
|
||||
public RuntimeCommandResult AddShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeShortcutCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 0,
|
||||
command.ObjectId);
|
||||
|
||||
public RuntimeCommandResult RemoveShortcut(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int index) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.InventoryState,
|
||||
operation: 1);
|
||||
|
||||
public RuntimeCommandResult AddFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
int position,
|
||||
uint spellId) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 0,
|
||||
spellId);
|
||||
|
||||
public RuntimeCommandResult RemoveFavorite(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
int tabIndex,
|
||||
uint spellId) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 1,
|
||||
spellId);
|
||||
|
||||
public RuntimeCommandResult SetFilter(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint filters) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 2);
|
||||
|
||||
public RuntimeCommandResult ForgetSpell(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint spellId) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 3,
|
||||
spellId);
|
||||
|
||||
public RuntimeCommandResult SetDesiredComponent(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint componentId,
|
||||
uint amount) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 4,
|
||||
componentId);
|
||||
|
||||
public RuntimeCommandResult ClearDesiredComponents(
|
||||
RuntimeGenerationToken expectedGeneration) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Spellbook,
|
||||
operation: 5);
|
||||
|
||||
public RuntimeCommandResult Advance(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeAdvancementCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Character,
|
||||
(int)command.Kind,
|
||||
command.StatId);
|
||||
|
||||
public RuntimeCommandResult SetOptions1(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
uint options) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Character,
|
||||
operation: 4);
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeFriendCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Social,
|
||||
(int)command.Kind,
|
||||
command.CharacterId);
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeSquelchCommand command) =>
|
||||
Unsupported(
|
||||
expectedGeneration,
|
||||
RuntimeCommandDomain.Social,
|
||||
(int)command.Scope,
|
||||
command.CharacterId);
|
||||
|
||||
private RuntimeCommandResult Unsupported(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeCommandDomain domain,
|
||||
int operation,
|
||||
uint primaryObjectId = 0u)
|
||||
{
|
||||
RuntimeCommandStatus gate =
|
||||
Validate(expectedGeneration, out _);
|
||||
return gate == RuntimeCommandStatus.Accepted
|
||||
? EmitUnsupported(
|
||||
domain,
|
||||
operation,
|
||||
RuntimeCommandStatus.Unsupported,
|
||||
primaryObjectId)
|
||||
: Result(gate);
|
||||
}
|
||||
|
||||
private RuntimeCommandResult EmitUnsupported(
|
||||
RuntimeCommandDomain domain,
|
||||
int operation,
|
||||
RuntimeCommandStatus status = RuntimeCommandStatus.Unsupported,
|
||||
uint primaryObjectId = 0u)
|
||||
{
|
||||
_runtime.EventSink.EmitCommand(
|
||||
domain,
|
||||
operation,
|
||||
status,
|
||||
primaryObjectId);
|
||||
return Result(status, primaryObjectId);
|
||||
}
|
||||
|
||||
private RuntimeCommandStatus Validate(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
out WorldSession? session)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (expectedGeneration != _runtime.Generation
|
||||
|| (_route is not null
|
||||
&& _routeGeneration != expectedGeneration))
|
||||
{
|
||||
session = null;
|
||||
return RuntimeCommandStatus.StaleGeneration;
|
||||
}
|
||||
|
||||
session = _session;
|
||||
return _route is not null
|
||||
&& session is not null
|
||||
&& _runtime.Session.IsInWorld
|
||||
? RuntimeCommandStatus.Accepted
|
||||
: RuntimeCommandStatus.Inactive;
|
||||
}
|
||||
}
|
||||
|
||||
private RuntimeCommandResult Result(
|
||||
RuntimeCommandStatus status,
|
||||
uint objectId = 0u) =>
|
||||
new(status, _runtime.Generation, objectId);
|
||||
|
||||
private void Activate(
|
||||
WorldSession session,
|
||||
RuntimeGenerationToken generation,
|
||||
CommandRoute route)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_route is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A direct Runtime command route is already active.");
|
||||
}
|
||||
_session = session;
|
||||
_routeGeneration = generation;
|
||||
_route = route;
|
||||
}
|
||||
}
|
||||
|
||||
private void Deactivate(CommandRoute route)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!ReferenceEquals(_route, route))
|
||||
return;
|
||||
_route = null;
|
||||
_session = null;
|
||||
_routeGeneration = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,18 @@ using AcDream.Core.Net;
|
|||
|
||||
namespace AcDream.Runtime.Session;
|
||||
|
||||
public sealed record LiveSessionCharacterSelector(
|
||||
int? ActiveIndex = null,
|
||||
uint? CharacterId = null,
|
||||
string? CharacterName = null);
|
||||
|
||||
public sealed record LiveSessionConnectOptions(
|
||||
bool Enabled,
|
||||
string Host,
|
||||
int Port,
|
||||
string User,
|
||||
string Password);
|
||||
string Password,
|
||||
LiveSessionCharacterSelector? Character = null);
|
||||
|
||||
public interface IRuntimeLiveSessionFramePhase
|
||||
{
|
||||
|
|
|
|||
|
|
@ -556,7 +556,10 @@ public sealed class LiveSessionController
|
|||
|
||||
CharacterList.Parsed? characters = _operations.GetCharacters(session);
|
||||
if (characters is null
|
||||
|| !CharacterList.TrySelectFirstAvailable(characters, out CharacterList.Selection selected))
|
||||
|| !TrySelectCharacter(
|
||||
characters,
|
||||
options.Character,
|
||||
out CharacterList.Selection selected))
|
||||
{
|
||||
Console.WriteLine("live: no available characters on account; disconnecting");
|
||||
StopCore();
|
||||
|
|
@ -755,6 +758,62 @@ public sealed class LiveSessionController
|
|||
private LiveSessionStartResult ConnectedResult()
|
||||
=> new(LiveSessionStartStatus.Connected, _activeSelection);
|
||||
|
||||
private static bool TrySelectCharacter(
|
||||
CharacterList.Parsed characters,
|
||||
LiveSessionCharacterSelector? selector,
|
||||
out CharacterList.Selection selection)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(characters);
|
||||
if (selector is null)
|
||||
{
|
||||
return CharacterList.TrySelectFirstAvailable(
|
||||
characters,
|
||||
out selection);
|
||||
}
|
||||
|
||||
int selectorCount =
|
||||
(selector.ActiveIndex.HasValue ? 1 : 0)
|
||||
+ (selector.CharacterId.HasValue ? 1 : 0)
|
||||
+ (!string.IsNullOrWhiteSpace(selector.CharacterName) ? 1 : 0);
|
||||
if (selectorCount != 1)
|
||||
{
|
||||
selection = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int index = 0; index < characters.Characters.Count; index++)
|
||||
{
|
||||
CharacterList.Character character =
|
||||
characters.Characters[index];
|
||||
if (!CharacterList.IsAvailableActiveIdentity(character))
|
||||
continue;
|
||||
if (selector.ActiveIndex is { } requestedIndex
|
||||
&& requestedIndex != index)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (selector.CharacterId is { } requestedId
|
||||
&& requestedId != character.Id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (selector.CharacterName is { } requestedName
|
||||
&& !string.Equals(
|
||||
requestedName,
|
||||
character.Name,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
selection = new CharacterList.Selection(index, character);
|
||||
return true;
|
||||
}
|
||||
|
||||
selection = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposing()
|
||||
{
|
||||
if (_disposeRequested || _disposed)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,291 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Session;
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-free inbound entity route for a direct Runtime host. It
|
||||
/// applies the same canonical identity, timestamp, object-table, and transit
|
||||
/// owners used by the graphical route without constructing App hydration,
|
||||
/// rendering, animation, or effect projections.
|
||||
/// </summary>
|
||||
public sealed class RuntimeLiveEntitySessionController
|
||||
{
|
||||
private readonly GameRuntime _runtime;
|
||||
private readonly WorldSession _session;
|
||||
private readonly Action<string> _log;
|
||||
|
||||
public RuntimeLiveEntitySessionController(
|
||||
GameRuntime runtime,
|
||||
WorldSession session,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_log = log ?? (_ => { });
|
||||
}
|
||||
|
||||
public LiveEntitySessionSink CreateSink() => new(
|
||||
OnSpawned,
|
||||
OnDeleted,
|
||||
OnPickedUp,
|
||||
OnMotionUpdated,
|
||||
OnPositionUpdated,
|
||||
OnVectorUpdated,
|
||||
OnStateUpdated,
|
||||
OnParentUpdated,
|
||||
OnTeleportStarted,
|
||||
OnAppearanceUpdated,
|
||||
_ => { },
|
||||
_ => { });
|
||||
|
||||
private RuntimeEntityObjectLifetime Entities =>
|
||||
_runtime.EntityObjects;
|
||||
|
||||
private void OnSpawned(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
RuntimeEntityRegistrationResult registration =
|
||||
Entities.RegisterEntity(spawn);
|
||||
if (registration.Canonical is not { } canonical)
|
||||
return;
|
||||
|
||||
ulong integrationVersion = canonical.CreateIntegrationVersion;
|
||||
_ = Entities.ApplyAcceptedSpawn(
|
||||
canonical,
|
||||
integrationVersion,
|
||||
canonical.Snapshot,
|
||||
replaceGeneration:
|
||||
registration.Inbound.Disposition
|
||||
is CreateObjectTimestampDisposition.NewGeneration);
|
||||
}
|
||||
|
||||
private void OnDeleted(DeleteObject.Parsed delete)
|
||||
{
|
||||
if (delete.Guid == _runtime.PlayerIdentity.ServerGuid
|
||||
|| !Entities.TryAcceptDelete(
|
||||
delete,
|
||||
isLocalPlayer: false,
|
||||
removeRetainedObject: true,
|
||||
out RuntimeEntityDeleteAcceptance acceptance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Entities.CompleteAcceptedDelete(acceptance);
|
||||
if (acceptance.RetiredCanonical is { } retired)
|
||||
{
|
||||
Exception? failure = Entities.RetireCanonicalOnly(retired);
|
||||
if (failure is not null)
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPickedUp(PickupEvent.Parsed pickup) =>
|
||||
_ = Entities.TryApplyPickup(
|
||||
pickup,
|
||||
acknowledgeProjection: null,
|
||||
out _);
|
||||
|
||||
private void OnMotionUpdated(
|
||||
WorldSession.EntityMotionUpdate update)
|
||||
{
|
||||
bool isLocal =
|
||||
update.Guid == _runtime.PlayerIdentity.ServerGuid;
|
||||
_ = Entities.TryApplyMotion(
|
||||
update,
|
||||
retainPayload: !isLocal || !update.IsAutonomous,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
|
||||
private void OnPositionUpdated(
|
||||
WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
bool isLocal =
|
||||
update.Guid == _runtime.PlayerIdentity.ServerGuid;
|
||||
bool known = Entities.TryApplyPosition(
|
||||
update,
|
||||
isLocal,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
projectionRequiresTeleportHook: false,
|
||||
acknowledgeProjection: null,
|
||||
out PositionTimestampDisposition disposition,
|
||||
out _,
|
||||
out AcceptedPhysicsTimestamps timestamps);
|
||||
if (!known
|
||||
|| !isLocal
|
||||
|| disposition is PositionTimestampDisposition.Rejected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = update.Position;
|
||||
var destination = new RuntimeTeleportDestination(
|
||||
update.Guid,
|
||||
update.InstanceSequence,
|
||||
update.PositionSequence,
|
||||
update.TeleportSequence,
|
||||
update.ForcePositionSequence,
|
||||
new Position(
|
||||
position.LandblockId,
|
||||
new Vector3(
|
||||
position.PositionX,
|
||||
position.PositionY,
|
||||
position.PositionZ),
|
||||
new Quaternion(
|
||||
position.RotationX,
|
||||
position.RotationY,
|
||||
position.RotationZ,
|
||||
position.RotationW)));
|
||||
_runtime.TransitOwner.OfferTeleportDestination(
|
||||
destination,
|
||||
timestamps.TeleportAdvanced);
|
||||
TryCompletePortal();
|
||||
}
|
||||
|
||||
private void OnVectorUpdated(VectorUpdate.Parsed update) =>
|
||||
_ = Entities.TryApplyVector(
|
||||
update,
|
||||
acknowledgeProjection: null,
|
||||
out _);
|
||||
|
||||
private void OnStateUpdated(SetState.Parsed update) =>
|
||||
_ = Entities.TryApplyState(
|
||||
update,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _);
|
||||
|
||||
private void OnParentUpdated(ParentEvent.Parsed update) =>
|
||||
_ = Entities.TryApplyParent(
|
||||
update,
|
||||
acknowledgeProjection: null,
|
||||
out _);
|
||||
|
||||
private void OnTeleportStarted(uint rawSequence)
|
||||
{
|
||||
ushort sequence = unchecked((ushort)rawSequence);
|
||||
RuntimeWorldTransitState transit = _runtime.TransitOwner;
|
||||
if (!transit.TryQueueTeleportStart(sequence))
|
||||
return;
|
||||
if (!transit.ActivateQueuedTeleport())
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected its queued headless teleport activation.");
|
||||
}
|
||||
TryCompletePortal();
|
||||
}
|
||||
|
||||
private void OnAppearanceUpdated(ObjDescEvent.Parsed update) =>
|
||||
_ = Entities.TryApplyObjDesc(
|
||||
update,
|
||||
acknowledgeProjection: null,
|
||||
out _);
|
||||
|
||||
private void TryCompletePortal()
|
||||
{
|
||||
RuntimeWorldTransitState transit = _runtime.TransitOwner;
|
||||
if (!transit.TryGetAcceptedTeleportDestination(
|
||||
out RuntimeTeleportDestination destination)
|
||||
|| !transit.TryBeginPortalReveal(
|
||||
destination.TeleportSequence,
|
||||
destination.CellId,
|
||||
out long generation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transit.TryRegisterHostProjection(
|
||||
generation,
|
||||
destination.CellId,
|
||||
out RuntimeWorldHostProjectionToken projection))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected the headless portal projection.");
|
||||
}
|
||||
|
||||
Acknowledge(
|
||||
transit,
|
||||
projection,
|
||||
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
||||
|
||||
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
|
||||
if (!transit.AcknowledgeDestinationReadiness(
|
||||
new RuntimeDestinationReadiness(
|
||||
generation,
|
||||
destination.CellId,
|
||||
indoor,
|
||||
IsUnhydratable: false,
|
||||
RequiredRenderRadius: indoor ? 0 : 1,
|
||||
IsRenderNeighborhoodReady: true,
|
||||
AreCompositeTexturesReady: true,
|
||||
IsCollisionReady: true)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected headless destination readiness.");
|
||||
}
|
||||
|
||||
if (!transit.AcknowledgePortalMaterialized(
|
||||
generation,
|
||||
destination.TeleportSequence,
|
||||
destination.CellId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected headless portal materialization.");
|
||||
}
|
||||
Acknowledge(
|
||||
transit,
|
||||
projection,
|
||||
RuntimeWorldHostAcknowledgementStage
|
||||
.SimulationReleaseProjected);
|
||||
|
||||
if (!transit.RequireDestinationReservationRelease(projection))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected headless destination release.");
|
||||
}
|
||||
Acknowledge(
|
||||
transit,
|
||||
projection,
|
||||
RuntimeWorldHostAcknowledgementStage
|
||||
.DestinationReservationReleased);
|
||||
|
||||
if (!transit.AcknowledgeWorldViewportVisible(generation)
|
||||
|| !transit.Complete(generation))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Runtime rejected headless portal completion.");
|
||||
}
|
||||
Acknowledge(
|
||||
transit,
|
||||
projection,
|
||||
RuntimeWorldHostAcknowledgementStage.TerminalProjected);
|
||||
|
||||
_session.SendGameAction(GameActionLoginComplete.Build());
|
||||
transit.EndTeleport();
|
||||
_log(
|
||||
$"headless: portal complete generation={generation} "
|
||||
+ $"cell=0x{destination.CellId:X8}");
|
||||
}
|
||||
|
||||
private static void Acknowledge(
|
||||
RuntimeWorldTransitState transit,
|
||||
RuntimeWorldHostProjectionToken projection,
|
||||
RuntimeWorldHostAcknowledgementStage stage)
|
||||
{
|
||||
if (!transit.AcknowledgeHostProjection(
|
||||
new RuntimeWorldHostAcknowledgement(
|
||||
projection,
|
||||
stage)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Runtime rejected headless host acknowledgement {stage}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue