286 lines
7.8 KiB
C#
286 lines
7.8 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using FlowScope.Config;
|
|
using FlowScope.Resources;
|
|
using R3;
|
|
using UnityEngine;
|
|
using ContainerType = FlowScope.Container.Container;
|
|
|
|
namespace FlowScope.Flow
|
|
{
|
|
public sealed class GameFlow
|
|
{
|
|
private ContainerType _root;
|
|
private IFeature _activeFeature;
|
|
private FeatureContext _activeContext;
|
|
|
|
public GameFlowState State { get; private set; } = GameFlowState.Idle;
|
|
|
|
public async Task StartupAsync<TInitialFeature>(
|
|
ContainerType root,
|
|
CancellationToken cancellationToken)
|
|
where TInitialFeature : IFeature
|
|
{
|
|
EnsureState(nameof(StartupAsync), GameFlowState.Idle);
|
|
_root = root ?? throw new ArgumentNullException(nameof(root));
|
|
State = GameFlowState.Starting;
|
|
|
|
IFeature feature = null;
|
|
FeatureContext context = null;
|
|
|
|
try
|
|
{
|
|
var config = _root.Resolve<IConfigProvider>();
|
|
await config.LoadAllAsync(cancellationToken);
|
|
|
|
(feature, context) = CreateFeature<TInitialFeature>(cancellationToken);
|
|
await feature.LoadAsync(context);
|
|
await feature.EnterAsync(context);
|
|
|
|
_activeFeature = feature;
|
|
_activeContext = context;
|
|
State = GameFlowState.Running;
|
|
}
|
|
catch
|
|
{
|
|
BestEffortDisposeFeature(feature);
|
|
BestEffortDisposeContext(context);
|
|
_activeFeature = null;
|
|
_activeContext = null;
|
|
State = GameFlowState.Idle;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task SwitchToAsync<TFeature>(CancellationToken cancellationToken)
|
|
where TFeature : IFeature
|
|
{
|
|
EnsureState(
|
|
nameof(SwitchToAsync),
|
|
GameFlowState.Running,
|
|
GameFlowState.NoActiveFeature);
|
|
|
|
State = GameFlowState.Switching;
|
|
await CleanupActiveFeatureAsync();
|
|
|
|
IFeature nextFeature = null;
|
|
FeatureContext nextContext = null;
|
|
|
|
try
|
|
{
|
|
(nextFeature, nextContext) = CreateFeature<TFeature>(cancellationToken);
|
|
await nextFeature.LoadAsync(nextContext);
|
|
await nextFeature.EnterAsync(nextContext);
|
|
|
|
_activeFeature = nextFeature;
|
|
_activeContext = nextContext;
|
|
State = GameFlowState.Running;
|
|
}
|
|
catch
|
|
{
|
|
BestEffortDisposeFeature(nextFeature);
|
|
BestEffortDisposeContext(nextContext);
|
|
_activeFeature = null;
|
|
_activeContext = null;
|
|
State = GameFlowState.NoActiveFeature;
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
|
{
|
|
EnsureState(
|
|
nameof(ShutdownAsync),
|
|
GameFlowState.Running,
|
|
GameFlowState.NoActiveFeature);
|
|
|
|
State = GameFlowState.ShuttingDown;
|
|
Exception cancellationException = null;
|
|
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
}
|
|
catch (OperationCanceledException exception)
|
|
{
|
|
cancellationException = exception;
|
|
}
|
|
|
|
await CleanupActiveFeatureAsync();
|
|
BestEffortDisposeRoot();
|
|
State = GameFlowState.Disposed;
|
|
|
|
if (cancellationException != null)
|
|
{
|
|
throw cancellationException;
|
|
}
|
|
}
|
|
|
|
private (IFeature Feature, FeatureContext Context) CreateFeature<TFeature>(
|
|
CancellationToken cancellationToken)
|
|
where TFeature : IFeature
|
|
{
|
|
ContainerType scope = null;
|
|
IResourceGroup resources = null;
|
|
FeatureContext context = null;
|
|
|
|
try
|
|
{
|
|
scope = _root.CreateScope();
|
|
var resourceService = _root.Resolve<IResourceService>();
|
|
resources = resourceService.CreateGroup();
|
|
context = new FeatureContext(
|
|
scope,
|
|
resources,
|
|
new CompositeDisposable(),
|
|
cancellationToken);
|
|
var feature = scope.Resolve<TFeature>();
|
|
return (feature, context);
|
|
}
|
|
catch
|
|
{
|
|
if (context != null)
|
|
{
|
|
BestEffortDisposeContext(context);
|
|
}
|
|
else
|
|
{
|
|
BestEffortDisposePartialContext(resources, scope);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task CleanupActiveFeatureAsync()
|
|
{
|
|
var feature = _activeFeature;
|
|
var context = _activeContext;
|
|
_activeFeature = null;
|
|
_activeContext = null;
|
|
|
|
if (feature != null)
|
|
{
|
|
try
|
|
{
|
|
await feature.ExitAsync(context);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
|
|
BestEffortDisposeFeature(feature);
|
|
}
|
|
|
|
BestEffortDisposeContext(context);
|
|
}
|
|
|
|
private void BestEffortDisposeFeature(IFeature feature)
|
|
{
|
|
if (feature == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
feature.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
}
|
|
|
|
private void BestEffortDisposeContext(FeatureContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
context.Disposables.Clear();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
|
|
try
|
|
{
|
|
context.Resources?.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
|
|
try
|
|
{
|
|
context.Scope?.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
}
|
|
|
|
private void BestEffortDisposePartialContext(
|
|
IResourceGroup resources,
|
|
ContainerType scope)
|
|
{
|
|
try
|
|
{
|
|
resources?.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
|
|
try
|
|
{
|
|
scope?.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
}
|
|
|
|
private void BestEffortDisposeRoot()
|
|
{
|
|
try
|
|
{
|
|
_root?.Dispose();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Debug.LogError(exception);
|
|
}
|
|
finally
|
|
{
|
|
_root = null;
|
|
}
|
|
}
|
|
|
|
private void EnsureState(string operation, params GameFlowState[] allowedStates)
|
|
{
|
|
foreach (var allowedState in allowedStates)
|
|
{
|
|
if (State == allowedState)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
$"{operation} cannot run while GameFlow state is {State}.");
|
|
}
|
|
}
|
|
}
|