using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Storage.Entities.Profile; namespace Artemis.Core; /// /// Represents a timeline used by profile elements /// public class Timeline : CorePropertyChanged, IStorageModel { private readonly object _lock = new(); /// /// Creates a new instance of the class /// public Timeline() { Entity = new TimelineEntity(); MainSegmentLength = TimeSpan.FromSeconds(5); Save(); } internal Timeline(TimelineEntity entity) { Entity = entity; Load(); } /// public override string ToString() { return $"Progress: {Position}/{Length} - delta: {Delta}"; } #region Properties private TimeSpan _position; private TimeSpan _lastDelta; private TimelinePlayMode _playMode; private TimelineStopMode _stopMode; private TimeSpan _startSegmentLength; private TimeSpan _mainSegmentLength; private TimeSpan _endSegmentLength; private TimeSpan _lastOverride; /// /// Gets the current position of the timeline /// public TimeSpan Position { get => _position; private set => SetAndNotify(ref _position, value); } /// /// Gets the cumulative delta of all calls to that took place after the last call to /// /// public TimeSpan Delta { get => _lastDelta; private set => SetAndNotify(ref _lastDelta, value); } /// /// Gets or sets the mode in which the render element starts its timeline when display conditions are met /// public TimelinePlayMode PlayMode { get => _playMode; set => SetAndNotify(ref _playMode, value); } /// /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met /// public TimelineStopMode StopMode { get => _stopMode; set => SetAndNotify(ref _stopMode, value); } /// /// Gets a boolean indicating whether the timeline has finished its run /// public bool IsFinished => Position > Length; /// /// Gets a boolean indicating whether the timeline progress has been overridden /// public bool IsOverridden { get; private set; } #region Segments /// /// Gets the total length of this timeline /// public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength; /// /// Gets or sets the length of the start segment /// public TimeSpan StartSegmentLength { get => _startSegmentLength; set { if (SetAndNotify(ref _startSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Start, false); } } /// /// Gets or sets the length of the main segment /// public TimeSpan MainSegmentLength { get => _mainSegmentLength; set { if (SetAndNotify(ref _mainSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Main, false); } } /// /// Gets or sets the length of the end segment /// public TimeSpan EndSegmentLength { get => _endSegmentLength; set { if (SetAndNotify(ref _endSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.End, false); } } /// /// Gets or sets the start position of the main segment /// public TimeSpan MainSegmentStartPosition { get => StartSegmentEndPosition; set { StartSegmentEndPosition = value; NotifySegmentShiftAt(TimelineSegment.Main, true); } } /// /// Gets or sets the end position of the end segment /// public TimeSpan EndSegmentStartPosition { get => MainSegmentEndPosition; set { MainSegmentEndPosition = value; NotifySegmentShiftAt(TimelineSegment.End, true); } } /// /// Gets or sets the end position of the start segment /// public TimeSpan StartSegmentEndPosition { get => StartSegmentLength; set { StartSegmentLength = value; NotifySegmentShiftAt(TimelineSegment.Start, false); } } /// /// Gets or sets the end position of the main segment /// public TimeSpan MainSegmentEndPosition { get => StartSegmentEndPosition + MainSegmentLength; set { MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero; NotifySegmentShiftAt(TimelineSegment.Main, false); } } /// /// Gets or sets the end position of the end segment /// public TimeSpan EndSegmentEndPosition { get => MainSegmentEndPosition + EndSegmentLength; set { EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero; NotifySegmentShiftAt(TimelineSegment.End, false); } } internal TimelineEntity Entity { get; set; } /// /// Notifies the right segments in a way that I don't have to think about it /// /// The segment that was updated /// Whether the start point of the was updated private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated) { if (segment <= TimelineSegment.End) { if (startUpdated || segment < TimelineSegment.End) OnPropertyChanged(nameof(EndSegmentStartPosition)); OnPropertyChanged(nameof(EndSegmentEndPosition)); } if (segment <= TimelineSegment.Main) { if (startUpdated || segment < TimelineSegment.Main) OnPropertyChanged(nameof(MainSegmentStartPosition)); OnPropertyChanged(nameof(MainSegmentEndPosition)); } if (segment <= TimelineSegment.Start) OnPropertyChanged(nameof(StartSegmentEndPosition)); OnPropertyChanged(nameof(Length)); OnTimelineChanged(); } /// /// Occurs when changes have been made to any of the segments of the timeline. /// public event EventHandler? TimelineChanged; private void OnTimelineChanged() { TimelineChanged?.Invoke(this, EventArgs.Empty); } #endregion #endregion #region Updating /// /// Updates the timeline, applying the provided to the /// /// The amount of time to apply to the position /// Whether to stick to the main segment, wrapping around if needed public void Update(TimeSpan delta, bool stickToMainSegment) { lock (_lock) { if (IsOverridden) throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first."); Delta += delta; Position += delta; if (!stickToMainSegment || Position <= MainSegmentEndPosition) return; // If the main segment has no length, simply stick to the start of the segment if (MainSegmentLength == TimeSpan.Zero) Position = MainSegmentStartPosition; // Ensure wrapping back around retains the delta time else Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); } } /// /// Moves the position of the timeline backwards to the very start of the timeline /// public void JumpToStart() { lock (_lock) { if (Position == TimeSpan.Zero) return; Delta = TimeSpan.Zero - Position; Position = TimeSpan.Zero; } } /// /// Moves the position of the timeline forwards to the beginning of the end segment /// public void JumpToEndSegment() { lock (_lock) { if (Position >= EndSegmentStartPosition) return; Delta = EndSegmentStartPosition - Position; Position = EndSegmentStartPosition; } } /// /// Moves the position of the timeline forwards to the very end of the timeline /// public void JumpToEnd() { lock (_lock) { if (Position >= EndSegmentEndPosition) return; Delta = EndSegmentEndPosition - Position; Position = EndSegmentEndPosition; } } /// /// Overrides the to the specified time /// /// The position to set the timeline to /// Whether to stick to the main segment, wrapping around if needed internal void Override(TimeSpan position, bool stickToMainSegment) { lock (_lock) { if (_lastOverride == TimeSpan.Zero) Delta = Position - position; else Delta = position - _lastOverride; Position = position; IsOverridden = true; _lastOverride = position; if (!stickToMainSegment || Position < MainSegmentStartPosition) return; bool atSegmentStart = Position >= MainSegmentStartPosition; if (MainSegmentLength > TimeSpan.Zero) { Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor // is actually at the start of the segment if (Position == MainSegmentStartPosition && !atSegmentStart) Position = MainSegmentEndPosition; } else { Position = MainSegmentStartPosition; } } } internal void ClearOverride() { IsOverridden = false; _lastOverride = TimeSpan.Zero; } /// /// Sets the to /// public void ClearDelta() { lock (_lock) { Delta = TimeSpan.Zero; } } #endregion #region Storage /// public void Load() { StartSegmentLength = Entity.StartSegmentLength; MainSegmentLength = Entity.MainSegmentLength; EndSegmentLength = Entity.EndSegmentLength; PlayMode = (TimelinePlayMode) Entity.PlayMode; StopMode = (TimelineStopMode) Entity.StopMode; JumpToEnd(); } /// public void Save() { Entity.StartSegmentLength = StartSegmentLength; Entity.MainSegmentLength = MainSegmentLength; Entity.EndSegmentLength = EndSegmentLength; Entity.PlayMode = (int) PlayMode; Entity.StopMode = (int) StopMode; } #endregion } internal enum TimelineSegment { Start, Main, End } /// /// Represents a mode for render elements to start their timeline when display conditions are met /// public enum TimelinePlayMode { /// /// Continue repeating the main segment of the timeline while the condition is met /// Repeat, /// /// Only play the timeline once when the condition is met /// Once } /// /// Represents a mode for render elements to stop their timeline when display conditions are no longer met /// public enum TimelineStopMode { /// /// When conditions are no longer met, finish the the current run of the main timeline /// Finish, /// /// When conditions are no longer met, skip to the end segment of the timeline /// SkipToEnd } /// /// Represents a mode for render elements to start their timeline when display conditions events are fired /// public enum TimeLineEventOverlapMode { /// /// Stop the current run and restart the timeline /// Restart, /// /// Ignore subsequent event fires until the timeline finishes /// Ignore, /// /// Play another copy of the timeline on top of the current run /// Copy, /// /// Repeat the timeline until the event fires again /// Toggle }