fix(headless): complete connected movement gate

This commit is contained in:
Erik 2026-07-27 10:26:44 +02:00
parent fd9559a063
commit 3f3401257c
11 changed files with 756 additions and 48 deletions

View file

@ -23,7 +23,8 @@ internal sealed class HeadlessProcessHost : IDisposable
TextWriter diagnostics,
ILiveSessionOperations? sessionOperations = null,
TimeProvider? timeProvider = null,
IHeadlessProcessContentFactory? contentFactory = null)
IHeadlessProcessContentFactory? contentFactory = null,
HeadlessDirectCredentials? directCredentials = null)
{
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(paths);
@ -34,6 +35,12 @@ internal sealed class HeadlessProcessHost : IDisposable
throw new HeadlessConfigurationException(
"Headless run mode requires at least one session.");
}
if (directCredentials is not null
&& configuration.Sessions.Count != 1)
{
throw new HeadlessConfigurationException(
"Direct credentials require exactly one configured session.");
}
HeadlessStaticStateAudit.ValidateProcessIsolation();
_diagnostics = new HeadlessDiagnosticWriter(diagnostics);
@ -61,10 +68,19 @@ internal sealed class HeadlessProcessHost : IDisposable
HeadlessSessionDescriptor descriptor = candidate
?? throw new HeadlessConfigurationException(
"Headless run mode cannot contain a null session.");
HeadlessCredentialSecret secret =
credentials.Resolve(
if (directCredentials is not null)
{
descriptor = WithAccount(
descriptor,
directCredentials.User);
}
HeadlessCredentialSecret secret = directCredentials is null
? credentials.Resolve(
descriptor.Id,
descriptor.Credential);
descriptor.Credential)
: new HeadlessCredentialSecret(
"command-line",
directCredentials.Password);
HeadlessProcessContentOwner.HeadlessProcessContentLease?
contentLease = content?.AcquireLease(descriptor.Id);
try
@ -111,6 +127,19 @@ internal sealed class HeadlessProcessHost : IDisposable
internal HeadlessProcessContentSnapshot? Content =>
_content?.CaptureSnapshot();
private static HeadlessSessionDescriptor WithAccount(
HeadlessSessionDescriptor source,
string account) =>
new()
{
Id = source.Id,
Endpoint = source.Endpoint,
Account = account,
Character = source.Character,
Policy = source.Policy,
Credential = source.Credential,
};
internal async Task<HeadlessExitCode> RunAsync(
CancellationToken cancellationToken)
{

View file

@ -293,10 +293,20 @@ internal sealed class HeadlessSessionWorldProjection
public void ProjectPosition(
RuntimeEntityRecord record,
bool isLocalPlayer)
bool isLocalPlayer,
PositionTimestampDisposition disposition)
{
if (isLocalPlayer)
if (!isLocalPlayer)
return;
if (_runtime.MovementOwner.Controller is null)
{
SynchronizeLocalPlayer(record);
return;
}
if (disposition is PositionTimestampDisposition.ForcePosition)
BlipLocalPlayer(record);
}
public void BeginTeleport()
@ -383,6 +393,28 @@ internal sealed class HeadlessSessionWorldProjection
controller.SetBodyOrientation(orientation);
}
private void BlipLocalPlayer(RuntimeEntityRecord record)
{
if (record.ServerGuid
!= _runtime.PlayerIdentity.ServerGuid
|| record.Snapshot.Position is not { } position
|| _runtime.MovementOwner.Controller is not { } controller)
{
return;
}
_collision.CenterOn(position.LandblockId);
Vector3 wirePosition = new(
position.PositionX,
position.PositionY,
position.PositionZ);
controller.LocalEntityId = record.LocalEntityId ?? 0u;
controller.BlipPosition(
wirePosition,
position.LandblockId,
wirePosition);
}
private PlayerMovementController CreateController(
RuntimeEntityRecord record)
{