feat(headless): share immutable gameplay content
This commit is contained in:
parent
12b500d383
commit
9569dadb57
25 changed files with 1031 additions and 429 deletions
|
|
@ -2,6 +2,7 @@ using AcDream.Core.Combat;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Content;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
|
@ -43,12 +44,29 @@ internal sealed class HeadlessGameplayOperations
|
|||
|
||||
private readonly object _gate = new();
|
||||
private GameRuntime? _runtime;
|
||||
private SpellComponentRequirementService? _componentRequirements;
|
||||
private WorldSession? _session;
|
||||
private SessionRoute? _route;
|
||||
|
||||
internal void Bind(GameRuntime runtime) =>
|
||||
_runtime = runtime
|
||||
?? throw new ArgumentNullException(nameof(runtime));
|
||||
internal void Bind(
|
||||
GameRuntime runtime,
|
||||
MagicCatalog? catalog,
|
||||
Func<string> accountName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(runtime);
|
||||
ArgumentNullException.ThrowIfNull(accountName);
|
||||
if (_runtime is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless gameplay operations are already bound.");
|
||||
}
|
||||
|
||||
_runtime = runtime;
|
||||
_componentRequirements = catalog?.CreateRequirementService(
|
||||
runtime.InventoryOwner.Objects,
|
||||
() => runtime.PlayerIdentity.ServerGuid,
|
||||
accountName);
|
||||
}
|
||||
|
||||
internal ILiveSessionCommandRouting CreateRoute(
|
||||
WorldSession session)
|
||||
|
|
@ -151,12 +169,16 @@ internal sealed class HeadlessGameplayOperations
|
|||
|
||||
public bool HasRequiredComponents(uint spellId)
|
||||
{
|
||||
if (_componentRequirements is { } requirements)
|
||||
return requirements.HasRequiredComponents(spellId);
|
||||
|
||||
GameRuntime runtime = RequireRuntime();
|
||||
ClientObject? player = runtime.InventoryOwner.Objects.Get(
|
||||
runtime.PlayerIdentity.ServerGuid);
|
||||
return player is not null
|
||||
&& !player.Properties.GetBool(
|
||||
SpellComponentsRequiredProperty,
|
||||
SpellComponentRequirementService
|
||||
.SpellComponentsRequiredProperty,
|
||||
true);
|
||||
}
|
||||
|
||||
|
|
@ -264,6 +286,4 @@ internal sealed class HeadlessGameplayOperations
|
|||
_route = null;
|
||||
}
|
||||
}
|
||||
|
||||
private const uint SpellComponentsRequiredProperty = 68u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,20 @@ internal readonly record struct HeadlessProcessContentSnapshot(
|
|||
|
||||
internal interface IHeadlessProcessContentFactory
|
||||
{
|
||||
(IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
||||
HeadlessOpenedProcessContent Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic);
|
||||
}
|
||||
|
||||
internal sealed record HeadlessOpenedProcessContent(
|
||||
IDatReaderWriter Dats,
|
||||
IPreparedAssetSource Prepared,
|
||||
MagicCatalog Magic);
|
||||
|
||||
internal sealed class ProductionHeadlessProcessContentFactory
|
||||
: IHeadlessProcessContentFactory
|
||||
{
|
||||
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
||||
public HeadlessOpenedProcessContent Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic)
|
||||
{
|
||||
|
|
@ -36,18 +41,30 @@ internal sealed class ProductionHeadlessProcessContentFactory
|
|||
string preparedAssetPath =
|
||||
Path.GetFullPath(descriptor.PreparedAssetPath);
|
||||
IDatReaderWriter? dats = null;
|
||||
IPreparedAssetSource? prepared = null;
|
||||
try
|
||||
{
|
||||
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
||||
var prepared = new PakPreparedAssetSource(
|
||||
prepared = new PakPreparedAssetSource(
|
||||
preparedAssetPath,
|
||||
dats,
|
||||
diagnostic);
|
||||
return (dats, prepared);
|
||||
MagicCatalog magic = MagicCatalog.Load(dats);
|
||||
return new HeadlessOpenedProcessContent(
|
||||
dats,
|
||||
prepared,
|
||||
magic);
|
||||
}
|
||||
catch
|
||||
{
|
||||
dats?.Dispose();
|
||||
try
|
||||
{
|
||||
prepared?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
dats?.Dispose();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,8 +79,10 @@ internal sealed class ProductionHeadlessProcessContentFactory
|
|||
internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly MagicCatalog _magic;
|
||||
private IDatReaderWriter? _dats;
|
||||
private IPreparedAssetSource? _prepared;
|
||||
private SharedPreparedCollisionCache? _collision;
|
||||
private int _leaseCount;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposed;
|
||||
|
|
@ -75,30 +94,41 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(diagnostic);
|
||||
(IDatReaderWriter dats, IPreparedAssetSource prepared) =
|
||||
HeadlessOpenedProcessContent? opened =
|
||||
(factory ?? new ProductionHeadlessProcessContentFactory())
|
||||
.Open(descriptor, diagnostic);
|
||||
_dats = dats
|
||||
?? throw new InvalidOperationException(
|
||||
"The headless content factory returned no DAT owner.");
|
||||
_prepared = prepared
|
||||
?? throw new InvalidOperationException(
|
||||
"The headless content factory returned no prepared source.");
|
||||
if (prepared is not IPreparedCollisionSource)
|
||||
if (opened is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The headless content factory returned no content owner.");
|
||||
}
|
||||
IDatReaderWriter? dats = opened.Dats;
|
||||
IPreparedAssetSource? prepared = opened.Prepared;
|
||||
MagicCatalog? magic = opened.Magic;
|
||||
bool incomplete =
|
||||
dats is null || prepared is null || magic is null;
|
||||
if (incomplete || prepared is not IPreparedCollisionSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
prepared.Dispose();
|
||||
prepared?.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
dats.Dispose();
|
||||
dats?.Dispose();
|
||||
}
|
||||
_prepared = null;
|
||||
_dats = null;
|
||||
throw new NotSupportedException(
|
||||
"Headless production content must expose prepared collision.");
|
||||
throw !incomplete
|
||||
&& prepared is not IPreparedCollisionSource
|
||||
? new NotSupportedException(
|
||||
"Headless production content must expose prepared collision.")
|
||||
: new InvalidOperationException(
|
||||
"The headless content factory returned an incomplete owner.");
|
||||
}
|
||||
_dats = dats!;
|
||||
_prepared = prepared!;
|
||||
_magic = magic!;
|
||||
_collision = new SharedPreparedCollisionCache(
|
||||
(IPreparedCollisionSource)prepared!);
|
||||
}
|
||||
|
||||
internal HeadlessProcessContentLease AcquireLease(string sessionId)
|
||||
|
|
@ -117,7 +147,9 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
|||
this,
|
||||
sessionId,
|
||||
_dats!,
|
||||
_prepared!);
|
||||
_prepared!,
|
||||
_collision!,
|
||||
_magic);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +195,11 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
|||
|
||||
private void DrainResources()
|
||||
{
|
||||
if (_collision is not null)
|
||||
{
|
||||
_collision.Dispose();
|
||||
_collision = null;
|
||||
}
|
||||
if (_prepared is not null)
|
||||
{
|
||||
_prepared.Dispose();
|
||||
|
|
@ -181,17 +218,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
|||
private HeadlessProcessContentOwner? _owner;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IPreparedAssetSource _prepared;
|
||||
private readonly IPreparedCollisionSource _collision;
|
||||
private readonly MagicCatalog _magic;
|
||||
|
||||
internal HeadlessProcessContentLease(
|
||||
HeadlessProcessContentOwner owner,
|
||||
string sessionId,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource prepared)
|
||||
IPreparedAssetSource prepared,
|
||||
IPreparedCollisionSource collision,
|
||||
MagicCatalog magic)
|
||||
{
|
||||
_owner = owner;
|
||||
SessionId = sessionId;
|
||||
_dats = dats;
|
||||
_prepared = prepared;
|
||||
_collision = collision;
|
||||
_magic = magic;
|
||||
}
|
||||
|
||||
internal string SessionId { get; }
|
||||
|
|
@ -214,8 +257,23 @@ internal sealed class HeadlessProcessContentOwner : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal IPreparedCollisionSource PreparedCollision =>
|
||||
(IPreparedCollisionSource)PreparedAssets;
|
||||
internal IPreparedCollisionSource PreparedCollision
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||
return _collision;
|
||||
}
|
||||
}
|
||||
|
||||
internal MagicCatalog MagicCatalog
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||
return _magic;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
private long _reconnectDeadline;
|
||||
private bool _reconnectPending;
|
||||
private ulong _stoppedGeneration;
|
||||
private string _accountName = string.Empty;
|
||||
private bool _disposed;
|
||||
|
||||
internal HeadlessSessionHost(
|
||||
|
|
@ -172,7 +173,15 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
CombatTime: () =>
|
||||
runtimeRef?.Clock.SimulationTimeSeconds ?? 0d));
|
||||
runtimeRef = runtime;
|
||||
gameplay.Bind(runtime);
|
||||
if (contentLease is { } content)
|
||||
{
|
||||
runtime.CharacterOwner.InstallSpellMetadata(
|
||||
content.MagicCatalog.SpellTable);
|
||||
}
|
||||
gameplay.Bind(
|
||||
runtime,
|
||||
contentLease?.MagicCatalog,
|
||||
() => _accountName);
|
||||
|
||||
var bridge = new SessionCommandBridge();
|
||||
var commands = new DirectGameRuntimeCommandAdapter(
|
||||
|
|
@ -448,6 +457,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
password,
|
||||
MapCharacterSelector(_descriptor.Character));
|
||||
LiveSessionStartResult result = _liveSession.Start(options);
|
||||
if (result.Selection is { } selection)
|
||||
_accountName = selection.AccountName;
|
||||
RuntimeSessionStartResult converted = Convert(result);
|
||||
if (converted.Error is { } error)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue