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( 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(); await config.LoadAllAsync(cancellationToken); (feature, context) = CreateFeature(cancellationToken); await feature.LoadAsync(context); await feature.EnterAsync(context); _activeFeature = feature; _activeContext = context; State = GameFlowState.Running; } catch { BestEffortDisposeContext(context); _activeFeature = null; _activeContext = null; State = GameFlowState.Idle; throw; } } public async Task SwitchToAsync(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(cancellationToken); await nextFeature.LoadAsync(nextContext); await nextFeature.EnterAsync(nextContext); _activeFeature = nextFeature; _activeContext = nextContext; State = GameFlowState.Running; } catch { 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( CancellationToken cancellationToken) where TFeature : IFeature { ContainerType scope = null; IResourceGroup resources = null; FeatureContext context = null; try { scope = _root.CreateScope(); var resourceService = _root.Resolve(); resources = resourceService.CreateGroup(); context = new FeatureContext( scope, resources, new CompositeDisposable(), cancellationToken); var feature = scope.Resolve(); 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); } } BestEffortDisposeContext(context); } private void BestEffortDisposeContext(FeatureContext context) { if (context == null) { return; } try { context.Disposables.Clear(); } catch (Exception exception) { Debug.LogError(exception); } try { context.Scope?.Dispose(); } catch (Exception exception) { Debug.LogError(exception); } try { context.Resources?.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}."); } } }