using System.Diagnostics; using System.Collections; using System.Collections.Generic; using System; using Godot; using Rokojori.Extensions; namespace Rokojori; public class TimelineRunner { public Timeline timeLine; public TimelineManager manager; public float lastPosition = 0; public float position = 0; public bool playing = false; public float deltaScale = 1; public float speed = 1; List events = new List(); List spans = new List(); List callbacks = new List(); AnimationCurve modulatorCurve; float modulatorTime; Vector3 modulatorRandomization; Action modulatorOnReady = null; float _lastModulation = 1; float _currentDelta = 1; public float modulatedSpeed => (float)( speed * _lastModulation * deltaScale ); public float currentDelta => _currentDelta; public TimelineRunner( Timeline timeLine, TimelineManager manager ) { this.timeLine = timeLine; this.manager = manager; this.speed = timeLine.startSpeed; playing = timeLine.autoStart; } public void Modulate( AnimationCurve curve, Action onReady ) { if ( modulatorOnReady != null ) { modulatorOnReady( false ); } modulatorCurve = curve; modulatorTime = 0; modulatorRandomization = curve.Randomize(); modulatorOnReady = onReady; } public void UpdateTimeLine( float realtimeDelta ) { if ( ! playing || ( manager.paused && ! timeLine.executesInPause )) { return; } var modulation = 1f; if ( modulatorCurve != null ) { modulation = modulatorCurve.SampleRandomized( modulatorTime, modulatorRandomization ); _lastModulation = modulation; modulatorTime += realtimeDelta; if ( modulatorTime >= modulatorCurve.GetRandomizedEndTime( modulatorRandomization ) ) { var mr = modulatorOnReady; modulatorOnReady = null; modulatorCurve = null; modulatorTime = 0; modulatorRandomization = Vector3.Zero; if ( mr != null ) { mr( true ); } } } else { _lastModulation = 1; } _currentDelta = realtimeDelta * deltaScale * speed * modulation; lastPosition = position; position += _currentDelta; var isForward = speed >= 0; if ( isForward ) { ProcessForward(); } } void ProcessForward() { ProcessEvents(); ProcessSpans(); ProcessCallbacks(); } List _callbackAdditions = new List(); void ProcessCallbacks() { if ( _callbackAdditions.Count > 0 ) { callbacks.AddRange( _callbackAdditions ); _callbackAdditions.Clear(); } callbacks.ForEach( c => c.callback( c ) ); } void ProcessEvents() { if ( requestedRemovalIDs.Count > 0 ) { requestedRemovalIDs.Sort(); RJLog.Log( "Removing:", requestedRemovalIDs, "From:", events ); //Lists.RemoveIncreasingSortedIndices( events, requestedRemovalIDs ); events = events.Filter( e => ! requestedRemovalIDs.Contains( e.id ) ); requestedRemovalIDs.Clear(); } List eventRemovals = null; for ( int i = 0; i < events.Count; i++ ) { var timelineEvent = events[ i ]; if ( timelineEvent.looping ) { ProcessLoopingEvent( timelineEvent ); continue; } if ( ! RangeDouble.ContainsExclusiveMax( lastPosition, position, timelineEvent.position ) ) { if ( timelineEvent.wasInside ) { eventRemovals = AddRemoval( timelineEvent.persistent, i, eventRemovals ); } continue; } timelineEvent.callback( timelineEvent ); timelineEvent.wasInside = true; } if ( eventRemovals != null ) { ListExtensions.RemoveIncreasingSortedIndices( events, eventRemovals ); } } // Upper bound of callbacks a single looping event can emit in one frame, so a huge // frame spike or a jumped timeline position can't stall the game with catching up. const int maxLoopCallbacksPerFrame = 128; // Fires once for every loop boundary inside [ lastPosition, position ). Checking for a // single boundary dropped the tick completely whenever a frame was longer than the loop // duration, which silently stopped loop events at low frame rates. Looping events are // only removed by an explicit RemoveEvent, never by elapsing. void ProcessLoopingEvent( TimelineEvent timelineEvent ) { if ( timelineEvent.loopDuration <= 0 ) { return; } var boundary = timelineEvent.GetNextLoopPosition( lastPosition ); var numCallbacks = 0; while ( boundary < position && numCallbacks < maxLoopCallbacksPerFrame ) { timelineEvent.callback( timelineEvent ); timelineEvent.wasInside = true; boundary += timelineEvent.loopDuration; numCallbacks ++; } } void ProcessSpans() { if ( spans.Count == 0 ) { return; } List spanRemovals = null; var timelineSpan = RangeDouble.Create( lastPosition, position ); var span = RangeDouble.Create( 0, 1 ); var isForward = lastPosition < position; for ( int i = 0; i < spans.Count; i++ ) { span.min = spans[ i ].start; span.max = spans[ i ].end; var overlaps = timelineSpan.Overlaps( span ); if ( ! overlaps ) { if ( spans[ i ].wasInside ) { spanRemovals = AddRemoval( spans[ i ].persistent, i, spanRemovals ); } continue; } var isStart = timelineSpan.ContainsValue( spans[ i ].start ); var isEnd = timelineSpan.ContainsValue( spans[ i ].end ); var spanType = TimelineSpanUpdateType.InSpan; if ( isStart && isEnd ) { spanType = TimelineSpanUpdateType.CompletelyInside; } else if ( isStart ) { spanType = TimelineSpanUpdateType.Start; } else if ( isEnd ) { spanType = TimelineSpanUpdateType.End; } spans[ i ].wasInside = true; spans[ i ].callback( spans[ i ], spanType ); } if ( spanRemovals != null ) { ListExtensions.RemoveIncreasingSortedIndices( spans, spanRemovals ); } } List AddRemoval( bool isPersistent, int index, List list ) { if ( isPersistent ) { return list; } if ( list == null ) { list = new List(); } list.Add( index ); return list; } List requestedRemovalIDs = new List(); public void RemoveEvent( int eventID ) { requestedRemovalIDs.Add( eventID ); RJLog.Log( "requestedRemovals add:", eventID ); } public TimelineCallback _ScheduleCallback( Action callback, int eventID ) { var tle = new TimelineCallback(); tle.id = eventID; tle.callback = callback; tle.timeLine = timeLine; _callbackAdditions.Add( tle ); return tle; } public TimelineEvent _ScheduleEvent( float position, int eventID, bool isPersistent, Action callback ) { var tle = new TimelineEvent(); tle.position = position; tle.id = eventID; tle.persistent = isPersistent; tle.callback = callback; tle.timeLine = timeLine; events.Add( tle ); return tle; } public TimelineEvent _ScheduleLoopEvent( float loopDuration, float loopOffset, int eventID, bool isPersistent, Action callback ) { var tle = new TimelineEvent(); tle.position = loopOffset; tle.looping = true; tle.loopDuration = loopDuration; tle.id = eventID; tle.persistent = isPersistent; tle.callback = callback; tle.timeLine = timeLine; events.Add( tle ); return tle; } public TimelineSpan _ScheduleSpan( float start, float end, int eventID, bool isPersistent, Action callback ) { var tse = new TimelineSpan(); tse.start = start; tse.end = end; tse.id = eventID; tse.persistent = isPersistent; tse.wasInside = false; tse.callback = callback; tse.timeLine = timeLine; spans.Add( tse ); return tse; } }