From e883341bbf0058b8df2f71cf83c8d737e6d3198a Mon Sep 17 00:00:00 2001 From: Daniel Willett Date: Sun, 6 Oct 2024 12:17:21 -0400 Subject: [PATCH] fixed phase progression and unity loop ticker. --- UncreatedWarfare/Layouts/Layout.cs | 43 +++++++++-- .../Phases/Flags/FlagActionPhaseLayout.cs | 17 ++-- .../Layouts/Phases/LeaderboardPhase.cs | 2 +- .../Layouts/Phases/PreparationPhase.cs | 26 +++---- .../Logging/WarfareLoggingExtensions.cs | 3 +- .../Proximity/ColliderProximity.cs | 13 +++- .../Util/Timing/UnityLoopTicker.cs | 77 ++++++++++++++----- UncreatedWarfare/WarfareModule.cs | 1 + UncreatedWarfare/Zones/ZoneStore.cs | 2 + 9 files changed, 136 insertions(+), 48 deletions(-) diff --git a/UncreatedWarfare/Layouts/Layout.cs b/UncreatedWarfare/Layouts/Layout.cs index 951f1cc9..8006522e 100644 --- a/UncreatedWarfare/Layouts/Layout.cs +++ b/UncreatedWarfare/Layouts/Layout.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using Uncreated.Warfare.Configuration; using Uncreated.Warfare.Layouts.Phases; using Uncreated.Warfare.Layouts.Teams; +using Uncreated.Warfare.Logging; using Uncreated.Warfare.Util; namespace Uncreated.Warfare.Layouts; @@ -259,7 +261,7 @@ protected internal virtual async UniTask BeginLayoutAsync(CancellationToken toke await MoveToNextPhase(token); } - public virtual async UniTask MoveToNextPhase(CancellationToken token = default) + public virtual async UniTask MoveToNextPhase(CancellationToken token = default) { // keep moving to the next phase until one is activated by BeginPhase. ILayoutPhase newPhase; @@ -283,11 +285,27 @@ public virtual async UniTask MoveToNextPhase(CancellationToken token = def { string phaseTypeFormat = Accessor.Formatter.Format(oldPhase.GetType()); Logger.LogDebug("Ending phase: {0}.", phaseTypeFormat); - await oldPhase.EndPhaseAsync(token); + try + { + await oldPhase.EndPhaseAsync(token); + } + catch (Exception ex) + { + if (oldPhase.IsActive) + { + Logger.LogError(ex, "Error ending phase {0}.", phaseTypeFormat); + await _factory.StartNextLayout(CancellationToken.None); + throw new OperationCanceledException(); + } + + Logger.LogWarning(ex, "Error ending phase {0}.", phaseTypeFormat); + } + if (oldPhase.IsActive) { Logger.LogError("Failed to end phase {0}.", phaseTypeFormat); - return false; + await _factory.StartNextLayout(CancellationToken.None); + throw new OperationCanceledException(); } await UniTask.SwitchToMainThread(token); @@ -311,11 +329,24 @@ public virtual async UniTask MoveToNextPhase(CancellationToken token = def } Logger.LogDebug("Starting next phase: {0}.", Accessor.Formatter.Format(newPhase.GetType())); - await newPhase.BeginPhaseAsync(CancellationToken.None); + + try + { + await newPhase.BeginPhaseAsync(CancellationToken.None); + } + catch (Exception ex) + { + if (!newPhase.IsActive) + { + Logger.LogError(ex, "Error beginning phase {0}.", Accessor.Formatter.Format(newPhase.GetType())); + await _factory.StartNextLayout(CancellationToken.None); + throw new OperationCanceledException(); + } + + Logger.LogWarning(ex, "Error beginning phase {0}.", Accessor.Formatter.Format(newPhase.GetType())); + } } while (!newPhase.IsActive); - - return true; } private void CheckEmptyPhases() diff --git a/UncreatedWarfare/Layouts/Phases/Flags/FlagActionPhaseLayout.cs b/UncreatedWarfare/Layouts/Phases/Flags/FlagActionPhaseLayout.cs index f334df22..850c1b24 100644 --- a/UncreatedWarfare/Layouts/Phases/Flags/FlagActionPhaseLayout.cs +++ b/UncreatedWarfare/Layouts/Phases/Flags/FlagActionPhaseLayout.cs @@ -65,6 +65,8 @@ public async UniTask InitializePhaseAsync(CancellationToken token = default) _zoneStore = ActivatorUtilities.CreateInstance(_serviceProvider, [ zoneProviders, false ]); + await _zoneStore.Initialize(token); + // load pathing provider IConfigurationSection config = Configuration.GetSection("PathingData"); IZonePathingProvider pathingProvider = (IZonePathingProvider)ReflectionUtility.CreateInstanceFixed(_serviceProvider, pathingProviderType, [ _zoneStore, this, config ]); @@ -77,17 +79,13 @@ public async UniTask InitializePhaseAsync(CancellationToken token = default) _logger.LogInformation("Zone path: {{{0}}}.", string.Join(" -> ", _pathingResult.Skip(1).SkipLast(1).Select(zone => zone.Name))); } - public virtual async UniTask BeginPhaseAsync(CancellationToken token = default) + public virtual UniTask BeginPhaseAsync(CancellationToken token = default) { - IsActive = true; - if (_pathingResult == null || _zoneStore == null) { throw new LayoutConfigurationException(this, "Unable to create zone path."); } - await UniTask.SwitchToMainThread(token); - // create zones as objects with colliders List zoneList = new List(_pathingResult.Count); @@ -110,12 +108,13 @@ public virtual async UniTask BeginPhaseAsync(CancellationToken token = default) ActiveZones = new ReadOnlyCollection(new ArraySegment(_zones, 1, _zones.Length - 2)); StartingTeam = _zones[0]; EndingTeam = _zones[^1]; + IsActive = true; + + return UniTask.CompletedTask; } - public virtual async UniTask EndPhaseAsync(CancellationToken token = default) + public virtual UniTask EndPhaseAsync(CancellationToken token = default) { - await UniTask.SwitchToMainThread(token); - // destroy collider objects foreach (ActiveZoneCluster cluster in ActiveZones) { @@ -125,5 +124,7 @@ public virtual async UniTask EndPhaseAsync(CancellationToken token = default) _zones = null; ActiveZones = Array.Empty(); IsActive = false; + + return UniTask.CompletedTask; } } \ No newline at end of file diff --git a/UncreatedWarfare/Layouts/Phases/LeaderboardPhase.cs b/UncreatedWarfare/Layouts/Phases/LeaderboardPhase.cs index a1e23458..8c578963 100644 --- a/UncreatedWarfare/Layouts/Phases/LeaderboardPhase.cs +++ b/UncreatedWarfare/Layouts/Phases/LeaderboardPhase.cs @@ -31,9 +31,9 @@ public override async UniTask BeginPhaseAsync(CancellationToken token = default) // todo show leaderboard and count-down timer _ticker = _tickerFactory.CreateTicker(Duration, invokeImmediately: false, queueOnGameThread: true, (_, _, _) => { - UniTask.Create(() => _session.MoveToNextPhase(CancellationToken.None)); _ticker?.Dispose(); _ticker = null; + UniTask.Create(() => _session.MoveToNextPhase(CancellationToken.None)); }); await base.BeginPhaseAsync(token); diff --git a/UncreatedWarfare/Layouts/Phases/PreparationPhase.cs b/UncreatedWarfare/Layouts/Phases/PreparationPhase.cs index 0910eb2d..b951f43c 100644 --- a/UncreatedWarfare/Layouts/Phases/PreparationPhase.cs +++ b/UncreatedWarfare/Layouts/Phases/PreparationPhase.cs @@ -27,24 +27,20 @@ public PreparationPhase(IServiceProvider serviceProvider, IConfiguration config) } /// - public override async UniTask BeginPhaseAsync(CancellationToken token = default) + public override UniTask BeginPhaseAsync(CancellationToken token = default) { - await UniTask.SwitchToMainThread(token); - StartBroadcastingStagingUI(); - await base.BeginPhaseAsync(token); + return base.BeginPhaseAsync(token); } /// - public override async UniTask EndPhaseAsync(CancellationToken token = default) + public override UniTask EndPhaseAsync(CancellationToken token = default) { - await UniTask.SwitchToMainThread(token); - _stagingUi.ClearFromAllPlayers(); _ticker?.Dispose(); - await base.EndPhaseAsync(token); + return base.EndPhaseAsync(token); } /// @@ -78,16 +74,20 @@ protected void StartBroadcastingStagingUI() return; // tick down the UI timer - _ticker = _tickerFactory.CreateTicker(TimeSpan.FromSeconds(1d), invokeImmediately: false, queueOnGameThread: true, (_, timeSinceStart, _) => + _ticker = _tickerFactory.CreateTicker(TimeSpan.FromSeconds(1d), invokeImmediately: false, state: this, queueOnGameThread: true, static (ticker, timeSinceStart, _) => { - if (timeSinceStart >= Duration) + PreparationPhase phase = ticker.State!; + if (timeSinceStart >= phase.Duration) { - _stagingUi.UpdateForAll(_translationService.SetOf.AllPlayers(), TimeSpan.Zero); - UniTask.Create(() => _session.MoveToNextPhase(CancellationToken.None)); + phase._stagingUi.UpdateForAll(phase._translationService.SetOf.AllPlayers(), TimeSpan.Zero); + phase._ticker?.Dispose(); + phase._ticker = null; + PreparationPhase phase2 = phase; + UniTask.Create(() => phase2._session.MoveToNextPhase(CancellationToken.None)); } else { - _stagingUi.UpdateForAll(_translationService.SetOf.AllPlayers(), Duration - timeSinceStart); + phase._stagingUi.UpdateForAll(phase._translationService.SetOf.AllPlayers(), phase.Duration - timeSinceStart); } }); } diff --git a/UncreatedWarfare/Logging/WarfareLoggingExtensions.cs b/UncreatedWarfare/Logging/WarfareLoggingExtensions.cs index 270335b9..ab61bd1d 100644 --- a/UncreatedWarfare/Logging/WarfareLoggingExtensions.cs +++ b/UncreatedWarfare/Logging/WarfareLoggingExtensions.cs @@ -3,7 +3,7 @@ // ReSharper disable once CheckNamespace namespace Uncreated.Warfare; - +#if false // this class is mostly copied from https://github.com/dotnet/extensions/blob/v3.1.0/src/Logging/Logging.Abstractions/src/LoggerExtensions.cs public static class WarfareLoggingExtensions { @@ -422,3 +422,4 @@ private static string MessageFormatterMtd(WarfareFormattedLogValues state, Excep return state.ToString(); } } +#endif \ No newline at end of file diff --git a/UncreatedWarfare/Proximity/ColliderProximity.cs b/UncreatedWarfare/Proximity/ColliderProximity.cs index 66dbec67..1844cb03 100644 --- a/UncreatedWarfare/Proximity/ColliderProximity.cs +++ b/UncreatedWarfare/Proximity/ColliderProximity.cs @@ -70,6 +70,17 @@ private void SetupCollider() _collider = boxCollider; break; + case IAACylinderProximity cylinder: + transform.position = cylinder.Center; + + CapsuleCollider capsuleCollider = gameObject.AddComponent(); + capsuleCollider.center = Vector3.zero; + capsuleCollider.radius = cylinder.Radius; + capsuleCollider.height = cylinder.Height; + capsuleCollider.isTrigger = true; + + break; + case ISphereProximity sphere: BoundingSphere sphereInfo = sphere.Sphere; @@ -90,8 +101,8 @@ private void SetupCollider() MeshCollider meshCollider = gameObject.AddComponent(); meshCollider.sharedMesh = mesh; - meshCollider.isTrigger = true; meshCollider.convex = true; + meshCollider.isTrigger = true; _collider = meshCollider; break; diff --git a/UncreatedWarfare/Util/Timing/UnityLoopTicker.cs b/UncreatedWarfare/Util/Timing/UnityLoopTicker.cs index d4968d3c..5743240d 100644 --- a/UncreatedWarfare/Util/Timing/UnityLoopTicker.cs +++ b/UncreatedWarfare/Util/Timing/UnityLoopTicker.cs @@ -1,5 +1,6 @@ using SDG.Framework.Utilities; using System; +using Uncreated.Warfare.Components; using Uncreated.Warfare.Logging; namespace Uncreated.Warfare.Util.Timing; @@ -9,9 +10,12 @@ namespace Uncreated.Warfare.Util.Timing; /// public class UnityLoopTicker : ILoopTicker { + private readonly ILogger _logger; private readonly DateTime _createdAt; private DateTime _lastInvokedAt; private Coroutine? _coroutine; + private MonoBehaviour? _component; + private bool _isDisposed; /// public TimeSpan InitialDelay { get; } @@ -31,8 +35,8 @@ public class UnityLoopTicker : ILoopTicker /// How often to invoke the timer. /// If the timer should be invoked now or wait a period. /// Callback since the timer being invoked now would mean you couldn't subscribe to the event first. - public UnityLoopTicker(TimeSpan periodicDelay, bool invokeImmediately, TState? state, TickerCallback>? onTick = null) - : this(invokeImmediately ? TimeSpan.Zero : periodicDelay, periodicDelay <= TimeSpan.Zero ? Timeout.InfiniteTimeSpan : periodicDelay, state, onTick) { } + public UnityLoopTicker(MonoBehaviour component, ILogger logger, TimeSpan periodicDelay, bool invokeImmediately, TState? state, TickerCallback>? onTick = null) + : this(component, logger, invokeImmediately ? TimeSpan.Zero : periodicDelay, periodicDelay <= TimeSpan.Zero ? Timeout.InfiniteTimeSpan : periodicDelay, state, onTick) { } /// /// Create a new timer. @@ -40,8 +44,9 @@ public UnityLoopTicker(TimeSpan periodicDelay, bool invokeImmediately, TState? s /// How long to wait to initially invoke the timer. /// How often to invoke the timer. /// Callback since the timer being invoked now would mean you couldn't subscribe to the event first. - public UnityLoopTicker(TimeSpan initialDelay, TimeSpan periodicDelay, TState? state, TickerCallback>? onTick = null) + public UnityLoopTicker(MonoBehaviour component, ILogger logger, TimeSpan initialDelay, TimeSpan periodicDelay, TState? state, TickerCallback>? onTick = null) { + _logger = logger; if (periodicDelay <= TimeSpan.Zero) periodicDelay = Timeout.InfiniteTimeSpan; @@ -68,11 +73,13 @@ public UnityLoopTicker(TimeSpan initialDelay, TimeSpan periodicDelay, TState? st return; } - _coroutine = TimeUtility.InvokeAfterDelay(InvokeTimer, (float)(initialDelay == TimeSpan.Zero ? periodicDelay : initialDelay).TotalSeconds); + _component = component; + _coroutine = _component.StartCoroutine(Coroutine(initialDelay)); } else { DateTime st = DateTime.UtcNow; + _component = component; UniTask.Create(async () => { await UniTask.SwitchToMainThread(); @@ -87,11 +94,32 @@ public UnityLoopTicker(TimeSpan initialDelay, TimeSpan periodicDelay, TState? st return; } - _coroutine = TimeUtility.InvokeAfterDelay(InvokeTimer, (float)(invokedAlready ? PeriodicDelay : newInitialDelay).TotalSeconds); + _coroutine = _component.StartCoroutine(Coroutine(invokedAlready ? TimeSpan.Zero : newInitialDelay)); }); } } + private IEnumerator Coroutine(TimeSpan initialDelay) + { + if (_isDisposed) + yield break; + + if (initialDelay > TimeSpan.Zero) + { + yield return new WaitForSecondsRealtime((float)initialDelay.TotalSeconds); + InvokeTimer(); + } + + if (PeriodicDelay <= TimeSpan.Zero) + yield break; + + while (!_isDisposed) + { + yield return new WaitForSecondsRealtime((float)PeriodicDelay.TotalSeconds); + InvokeTimer(); + } + } + ~UnityLoopTicker() { Dispose(false); @@ -105,20 +133,27 @@ public void Dispose() private void Dispose(bool disposing) { - Coroutine? coroutine = Interlocked.Exchange(ref _coroutine, null); - if (coroutine == null) - return; + _isDisposed = true; if (GameThread.IsCurrent) { - TimeUtility.StaticStopCoroutine(coroutine); + if (_coroutine != null && _component != null) + _component.StopCoroutine(_coroutine); + + _coroutine = null; + _component = null; } else { UniTask.Create(async () => { await UniTask.SwitchToMainThread(); - TimeUtility.StaticStopCoroutine(coroutine); + + if (_coroutine != null && _component != null) + _component.StopCoroutine(_coroutine); + + _coroutine = null; + _component = null; }); } @@ -135,14 +170,11 @@ private void InvokeTimer() } catch (Exception ex) { - L.LogError("Error invoking ticker."); - L.LogError(ex); + _logger.LogError(ex, "Error invoking loop ticker."); } finally { _lastInvokedAt = utcNow; - if (PeriodicDelay > TimeSpan.Zero) - _coroutine = TimeUtility.InvokeAfterDelay(InvokeTimer, (float)PeriodicDelay.TotalSeconds); } } @@ -162,27 +194,36 @@ event TickerCallback? ILoopTicker.OnTick /// public class UnityLoopTickerFactory : ILoopTickerFactory { + private readonly WarfareLifetimeComponent _component; + private readonly ILogger _logger; + + public UnityLoopTickerFactory(WarfareLifetimeComponent component, ILogger logger) + { + _component = component; + _logger = logger; + } + /// public ILoopTicker CreateTicker(TimeSpan periodicDelay, bool invokeImmediately, bool queueOnGameThread, TickerCallback? onTick = null) { - return new UnityLoopTicker(periodicDelay, invokeImmediately, null, onTick); + return new UnityLoopTicker(_component, _logger, periodicDelay, invokeImmediately, null, onTick); } /// public ILoopTicker CreateTicker(TimeSpan initialDelay, TimeSpan periodicDelay, bool queueOnGameThread, TickerCallback? onTick = null) { - return new UnityLoopTicker(periodicDelay, periodicDelay, null, onTick); + return new UnityLoopTicker(_component, _logger, periodicDelay, periodicDelay, null, onTick); } /// public ILoopTicker CreateTicker(TimeSpan periodicDelay, bool invokeImmediately, TState? state, bool queueOnGameThread, TickerCallback>? onTick = null) { - return new UnityLoopTicker(periodicDelay, invokeImmediately, state, onTick); + return new UnityLoopTicker(_component, _logger, periodicDelay, invokeImmediately, state, onTick); } /// public ILoopTicker CreateTicker(TimeSpan initialDelay, TimeSpan periodicDelay, TState? state, bool queueOnGameThread, TickerCallback>? onTick = null) { - return new UnityLoopTicker(periodicDelay, periodicDelay, state, onTick); + return new UnityLoopTicker(_component, _logger, periodicDelay, periodicDelay, state, onTick); } } \ No newline at end of file diff --git a/UncreatedWarfare/WarfareModule.cs b/UncreatedWarfare/WarfareModule.cs index 85690817..b70a4423 100644 --- a/UncreatedWarfare/WarfareModule.cs +++ b/UncreatedWarfare/WarfareModule.cs @@ -620,6 +620,7 @@ public async UniTask ShutdownAsync(string reason, CancellationToken token = defa { await UniTask.SwitchToMainThread(token); + // prevent players from joining after shutdown start IPlayerService? playerService = ServiceProvider.ResolveOptional(); playerService?.TakePlayerConnectionLock(token); diff --git a/UncreatedWarfare/Zones/ZoneStore.cs b/UncreatedWarfare/Zones/ZoneStore.cs index ec0cd7b8..a1a6549f 100644 --- a/UncreatedWarfare/Zones/ZoneStore.cs +++ b/UncreatedWarfare/Zones/ZoneStore.cs @@ -50,6 +50,8 @@ public ZoneStore(IEnumerable zoneProviders, IPlayerService player _logger = logger; _warfare = warfare; IsGlobal = isGlobal; + + Zones = Array.Empty(); } async UniTask IHostedService.StartAsync(CancellationToken token)