C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
namespace Oxide.Plugins
{
[Info("Coroutines", "birthdates", "3.0.8")]
[Description(
"Allows other plugins to spread out large workloads over time to reduce lag spikes")]
public class Coroutines : CovalencePlugin
{
#region Variables
private readonly LinkedList<KeyValuePair<string, LinkedList<Coroutine>>> _coroutines =
new LinkedList<KeyValuePair<string, LinkedList<Coroutine>>>();
private int _currentCoroutineCount;
private readonly IDictionary<string, Number> _maxCoroutineCounter = new Dictionary<string, Number>();
private readonly IDictionary<string, int> _idToMax = new Dictionary<string, int>();
private readonly LinkedList<Coroutine> _queuedCoroutines = new LinkedList<Coroutine>();
private readonly Stopwatch _stopwatch = new Stopwatch();
public static Coroutines Instance { get; private set; }
#endregion
#region Classes
/// <summary>
/// The main coroutine class that handles instructions via <see cref="Coroutines.OnFrame" />
/// </summary>
public class Coroutine
{
public Coroutine(Plugin owner, IEnumerator instructions, Action onComplete, string id = null,
int cachedMaxInstances = -1)
{
Owner = owner;
OnComplete = onComplete;
RecursiveInstructions = new LinkedList<IEnumerator>();
RecursiveInstructions.AddFirst(instructions);
CurrentInstructions = instructions;
Id = id;
CachedMaxInstances = cachedMaxInstances;
}
/// <summary>
/// The maximum allowed of this instance
/// </summary>
public int CachedMaxInstances { get; }
/// <summary>
/// Returns if we should do counter cleanup for <see cref="Coroutines.OnFrame" />
/// </summary>
public bool HasCounter => CachedMaxInstances != -1;
public string Id { get; }
/// <summary>
/// The owner of this coroutine
/// </summary>
public Plugin Owner { get; }
/// <summary>
/// The recursive list of instructions
/// </summary>
private LinkedList<IEnumerator> RecursiveInstructions { get; }
/// <summary>
/// The current set of instructions
/// </summary>
private IEnumerator CurrentInstructions { get; set; }
/// <summary>
/// The current stage/index of <see cref="CurrentInstructions" />
/// </summary>
private int CurrentStage { get; set; } = -1;
/// <summary>
/// The current level/index of <see cref="RecursiveInstructions" />
/// </summary>
private int CurrentLevel { get; set; }
/// <summary>
/// A bool used to stop ticking
/// </summary>
public bool Stop { get; set; }
/// <summary>
/// The callback for when this coroutine is complete
/// </summary>
private Action OnComplete { get; }
/// <summary>
/// <para>
/// On tick, it checks if the current instruction is completed or null, if so, it continues on to the next
/// instruction
/// </para>
/// <para>
/// If there are no more instructions, we will mark this coroutine as finished and to be removed & call
/// <see cref="OnComplete" />
/// </para>
/// </summary>
/// <param name="deltaTime">The current deltaTime</param>
public void Tick(float deltaTime)
{
if (Stop) return;
if (CurrentInstructions == null) goto finish;
var currentInstruction = CurrentInstructions.Current;
var coroutineInstruction = currentInstruction as ICoroutineInstruction;
if (!coroutineInstruction?.IsCompleted ?? false)
{
coroutineInstruction.Tick(deltaTime);
return;
}
CurrentStage++;
try
{
RestartStopWatch(Instance._stopwatch);
Owner.TrackStart();
var ret = CurrentInstructions.MoveNext();
Owner.TrackEnd();
if (ret)
{
CheckStopwatch();
CheckForInstructionRecursion();
return;
}
}
catch (Exception exception)
{
Instance.PrintError(
"An exception occurred whilst executing coroutine from {0} {1} at stage {2}, level {3}:\n{4}\n{5}",
Owner.Name, GetIdOrEmpty(), CurrentStage, CurrentLevel,
exception.Message, exception.StackTrace);
goto finish;
}
CurrentStage = 0;
CurrentLevel++;
if (UpdateCurrentInstructions()) return;
finish:
Stop = true;
OnComplete?.Invoke();
}
private string GetIdOrEmpty()
{
return string.IsNullOrEmpty(Id) ? string.Empty : $"named {Id}";
}
/// <summary>
/// Check if the current instruction is another list of instructions, if so add it to
/// <see cref="RecursiveInstructions" />
/// </summary>
private void CheckForInstructionRecursion()
{
var enumeratorInstruction = CurrentInstructions.Current as IEnumerator;
if (enumeratorInstruction == null)
{
var coroutineInstruction = CurrentInstructions.Current as Coroutine;
enumeratorInstruction = coroutineInstruction?.CurrentInstructions;
if (enumeratorInstruction == null) return;
}
RecursiveInstructions.AddLast(enumeratorInstruction);
CurrentInstructions = enumeratorInstruction;
}
/// <summary>
/// Check if the execution time of the coroutine is greater than the time specified in the config
/// </summary>
private void CheckStopwatch()
{
Instance._stopwatch.Stop();
var elapsed = Instance._stopwatch.ElapsedMilliseconds;
if (Instance._config.WarnTimeMilliseconds > 0 &&
elapsed < Instance._config.WarnTimeMilliseconds) return;
var stackTrace = Instance._config.PrintStackTraceOnWarn
? $"\n{new Exception().StackTrace}"
: string.Empty;
Instance.PrintWarning("A coroutine from {0} {1} stage {2}, level {3} took {4}ms{5}", Owner.Name,
GetIdOrEmpty(),
CurrentStage, CurrentLevel, elapsed,
stackTrace);
}
/// <summary>
/// Update the current recursive instructions and set it null if there are none left
/// </summary>
/// <returns>True if we have anymore instructions</returns>
private bool UpdateCurrentInstructions()
{
RecursiveInstructions.RemoveLast();
CurrentInstructions = RecursiveInstructions.Last?.Value;
return true;
}
}
#region Coroutine Instructions
/// <summary>
/// The class used for coroutine instructions like <see cref="WaitForSeconds" />
/// </summary>
public interface ICoroutineInstruction
{
/// <summary>
/// Returns if this current instruction is complete
/// </summary>
bool IsCompleted { get; }
/// <summary>
/// Tick the current instruction
/// </summary>
/// <param name="deltaTime">The current delta time (scaled time)</param>
void Tick(float deltaTime);
}
/// <summary>
/// A <see cref="ICoroutineInstruction" /> to wait for a certain amount of milliseconds in real or scaled time
/// </summary>
public class WaitForMilliseconds : ICoroutineInstruction
{
public WaitForMilliseconds(float time) : this(time, !Instance._config.UseScaledTime)
{
}
public WaitForMilliseconds(float time, bool realTime)
{
Reset(time, realTime);
}
/// <summary>
/// Are we using real or delta/scaled time?
/// Warning, not all games use delta/scaled time
/// </summary>
private bool RealTime { get; set; }
/// <summary>
/// Returns the date time
/// </summary>
private static DateTime DateTime => DateTime.UtcNow;
/// <summary>
/// Returns the current time in seconds
/// </summary>
private static double Milliseconds => GetMilliseconds(DateTime);
/// <summary>
/// Returns total seconds from <see cref="DateTime" />
/// </summary>
private static Func<DateTime, double> GetMilliseconds => dateTime => dateTime.TimeOfDay.TotalMilliseconds;
/// <summary>
/// Returns the time until this instruction expires (completes)
/// </summary>
private double ExpiryTime { get; set; }
/// <summary>
/// The seconds given
/// </summary>
protected float Time { get; private set; }
/// <summary>
/// Returns <see langword="true" /> if <see cref="ExpiryTime" /> has expired
/// </summary>
public bool IsCompleted => RealTime ? ExpiryTime <= Milliseconds : ExpiryTime <= 0f;
/// <summary>
/// Remove <see cref="deltaTime" /> as milliseconds from <see cref="Time" />
/// </summary>
/// <param name="deltaTime">Current delta time (scaled time)</param>
public void Tick(float deltaTime)
{
if (RealTime) return;
ExpiryTime -= SecondsToMilliseconds(deltaTime);
}
/// <summary>
/// Set <see cref="Time" /> if given a time and reset expiry time
/// </summary>
public void Reset(float time = -1f)
{
Reset(time, !Instance._config.UseScaledTime);
}
public void Reset(float time, bool realTime)
{
RealTime = realTime;
if (time > 0f) Time = time;
ExpiryTime = realTime ? GetMilliseconds.Invoke(DateTime.AddMilliseconds(Time)) : time;
}
}
/// <summary>
/// A <see cref="ICoroutineInstruction" /> to wait for a certain amount of seconds in real or scaled time
/// </summary>
public class WaitForSeconds : WaitForMilliseconds
{
public WaitForSeconds(float time) : this(time, !Instance._config.UseScaledTime)
{
}
public WaitForSeconds(float time, bool realTime) : base(SecondsToMilliseconds(time), realTime)
{
}
public new void Reset(float time = -1f)
{
Reset(time, !Instance._config.UseScaledTime);
}
public new void Reset(float time, bool realTime)
{
base.Reset(time > 0f ? SecondsToMilliseconds(time) : Time, realTime);
}
}
/// <summary>
/// A <see cref="ICoroutineInstruction" /> to wait for a given boolean
/// </summary>
public class WaitForBool : ICoroutineInstruction
{
public WaitForBool(Func<bool> predicate)
{
Predicate = predicate;
}
/// <summary>
/// The boolean we are waiting for
/// </summary>
private Func<bool> Predicate { get; }
/// <summary>
/// Returns if <see cref="Predicate" /> is true
/// </summary>
public bool IsCompleted => Predicate.Invoke();
public void Tick(float deltaTime)
{
}
}
#endregion
#endregion
#region Hooks
/// <summary>
/// On initialized, set instance
/// </summary>
private void Init()
{
Instance = this;
}
/// <summary>
/// On unload, unset instance
/// </summary>
private void Unload()
{
Instance = null;
}
/// <summary>
/// Tick all of the active coroutines and cleanup any of them that are done
/// </summary>
/// <param name="deltaTime">The current delta/scaled time</param>
private void OnFrame(float deltaTime = 0f)
{
if (_currentCoroutineCount == 0) return;
TrackEnd(); //end track on coroutine ticks
var currentPluginNode = _coroutines.First;
while (currentPluginNode != null)
{
var entry = currentPluginNode.Value;
var currentCoroutineNode = entry.Value.First;
while (currentCoroutineNode != null)
{
var coroutine = currentCoroutineNode.Value;
coroutine.Tick(deltaTime);
if (!coroutine.Stop) goto end;
if (coroutine.HasCounter)
{
Number counter;
if (_maxCoroutineCounter.TryGetValue(coroutine.Id, out counter) &&
(counter.Value += 1) >= counter.Max)
_maxCoroutineCounter.Remove(coroutine.Id);
}
_currentCoroutineCount--;
entry.Value.Remove(currentCoroutineNode);
end:
currentCoroutineNode = currentCoroutineNode.Next;
}
if (entry.Value.Count <= 0) _coroutines.Remove(currentPluginNode);
currentPluginNode = currentPluginNode.Next;
}
TrackStart(); //continue track on cleanup
if (!_config.CheckQueueOnFrame) return;
CheckQueue();
}
/// <summary>
/// On unload of a plugin, stop all of it's coroutines
/// </summary>
/// <param name="plugin">Target plugin (owner)</param>
private void OnPluginUnloaded(Plugin plugin)
{
StopCoroutines(plugin);
}
#endregion
#region Hook Methods
/// <summary>
/// Register a maximum amount of instances of an id
/// </summary>
/// <param name="id">Target id</param>
/// <param name="max">Target maximum</param>
[HookMethod("RegisterMax")]
public void RegisterMax(string id, int max)
{
_idToMax[id] = max;
}
/// <summary>
/// Unregister a previously set maximum of instances from <see cref="RegisterMax" />
/// </summary>
/// <param name="id">Target id</param>
/// <returns><see langword="true" /> if an item was removed from <see cref="_idToMax" /></returns>
[HookMethod("UnregisterMax")]
public bool UnregisterMax(string id)
{
return _idToMax.Remove(id);
}
/// <summary>
/// Start an asynchronous task that isn't repeated
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="task">The task to complete</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id for this coroutine</param>
/// <param name="onComplete">Callback for when the task is completed</param>
/// <returns>A <see cref="Coroutine" /> to run</returns>
[HookMethod("GetDelayedTask")]
public Coroutine GetDelayedTask(Plugin owner, Action task, float initialDelay = 0f, string id = null,
Action onComplete = null)
{
return CreateCoroutine(owner, GetInitialDelayCoroutine(initialDelay, task), id, onComplete);
}
/// <summary>
/// The same as <see cref="StartDelayedTask" /> but it repeats
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="continuePredicate">Predicate to keep repeating</param>
/// <param name="interval">Interval between each repetition</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <param name="onComplete">Callback when <see cref="continuePredicate" /> returns false (task is complete)</param>
/// <returns>A <see cref="Coroutine" /> to run</returns>
[HookMethod("GetAsynchronousRepeatingTask")]
public Coroutine GetAsynchronousRepeatingTask(Plugin owner, Func<bool> continuePredicate, float interval,
float initialDelay = 0f, string id = null, Action onComplete = null)
{
return CreateCoroutine(owner, GetRepeatingCoroutine(
continuePredicate, interval, initialDelay), id, onComplete);
}
/// <summary>
/// Asynchronously loop through list (this loops through the whole list no stopping)
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="callback">The callback for each item in the list</param>
/// <param name="list">Target list</param>
/// <param name="interval">Interval between each repetition</param>
/// <param name="startIndex">The index in the list to start at</param>
/// <param name="reverse">If it just should loop in reverse</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <param name="onComplete">Callback when list is done looping</param>
/// <typeparam name="T">Type parameter of <see cref="list" /></typeparam>
/// <returns>A <see cref="Coroutine" /> to run</returns>
public Coroutine LoopListAsynchronously<T>(Plugin owner, Action<T> callback, IList<T> list,
float interval, int startIndex = -1, bool reverse = false, int completePerTick = 1, float initialDelay = 0f,
string id = null, Action onComplete = null)
{
if (list.Count != 0)
return LoopListAsynchronously(owner, obj =>
{
callback.Invoke(obj);
return true;
}, list, interval, startIndex, reverse, completePerTick, initialDelay, id, onComplete);
onComplete?.Invoke();
return null;
}
/// <summary>
/// Asynchronously loop through list
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="callback">The callback for each item in the list (return false to stop looping)</param>
/// <param name="list">Target list</param>
/// <param name="interval">Interval between each repetition</param>
/// <param name="startIndex">The index in the list to start at</param>
/// <param name="reverse">If it just should loop in reverse</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <param name="onComplete">Callback when list is done looping</param>
/// <typeparam name="T">Type parameter of <see cref="list" /></typeparam>
/// <returns>A <see cref="Coroutine" /> to run</returns>
public Coroutine LoopListAsynchronously<T>(Plugin owner, Func<T, bool> callback, IList<T> list,
float interval, int startIndex = -1, bool reverse = false, int completePerTick = 1,
float initialDelay = 0f, string id = null, Action onComplete = null)
{
var index = startIndex == -1 ? reverse ? list.Count - 1 : 0 : startIndex;
if (reverse && completePerTick > 0) completePerTick = -completePerTick;
var increment = reverse ? -1 : 1;
return GetAsynchronousRepeatingTask(owner, () =>
{
var max = index + completePerTick;
for (var i = index; reverse ? i >= 0 : i < max && i < list.Count; i += increment)
{
var obj = list[i];
if (callback.Invoke(obj)) continue;
return false;
}
index = max;
return reverse ? index > 0 : index < list.Count;
}, interval, initialDelay, id, onComplete);
}
/// <summary>
/// Asynchronously search through a list (check if value is in list)
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="target">Target value</param>
/// <param name="callback">Callback on complete</param>
/// <param name="list">Target list</param>
/// <param name="interval">Interval between loop</param>
/// <param name="startIndex">The index in the list to start at</param>
/// <param name="reverse">If it just should loop in reverse</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <typeparam name="T">Type parameter of <see cref="list" /></typeparam>
/// <returns>A <see cref="Coroutine" /> to run</returns>
public Coroutine SearchListAsynchronously<T>(Plugin owner, T target, Action<bool> callback, IList<T> list,
float interval, int startIndex = -1, bool reverse = false, int completePerTick = 1, float initialDelay = 0f,
string id = null)
{
var found = false;
return LoopListAsynchronously(owner, obj =>
{
if (!obj.Equals(target)) return true;
found = true;
callback.Invoke(true);
return false;
}, list, interval, startIndex, reverse, completePerTick, initialDelay, id, () =>
{
if (found) return;
callback.Invoke(false);
});
}
/// <summary>
/// Asynchronously find a value from it's corresponding key in a <see cref="IDictionary{TKey,TValue}" />
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="target">Target key (Key -> Value)</param>
/// <param name="callback">Callback with value</param>
/// <param name="dictionary">Target dictionary</param>
/// <param name="interval">Interval between loop</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <typeparam name="TK">Key type</typeparam>
/// <typeparam name="TV">Value type</typeparam>
/// <returns>A <see cref="Coroutine" /> to run</returns>
public Coroutine SearchDictionaryAsynchronously<TK, TV>(Plugin owner, TK target, Action<TV> callback,
IDictionary<TK, TV> dictionary, float interval, int completePerTick = 1, float initialDelay = 0f,
string id = null)
{
if (dictionary.Count != 0)
return SearchAsynchronously(owner, target, dictionary, item => item.Count,
(index, item) => index < item.Count,
(index, givenTarget, item) =>
{
var element = item.ElementAt(index);
return !element.Key?.Equals(target) ?? false ? default(TV) : element.Value;
}, callback, interval, completePerTick, initialDelay, id);
callback.Invoke(default(TV));
return null;
}
/// <summary>
/// Method to be implemented (searching)
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="target">Target item</param>
/// <param name="item">Object to search in</param>
/// <param name="getSize">Get size method</param>
/// <param name="keepSearchGoing">Keep search going method</param>
/// <param name="handleSearchIndex">Handle search method</param>
/// <param name="callback">Callback with found object</param>
/// <param name="interval">Interval between each repetition</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="id">Id of this coroutine</param>
/// <typeparam name="T">Type to search in</typeparam>
/// <typeparam name="TK">Type of <see cref="target" /></typeparam>
/// <typeparam name="TV">Type of <see cref="handleSearchIndex" /> return</typeparam>
/// <returns>A <see cref="Coroutine" /> to run</returns>
public Coroutine SearchAsynchronously<T, TK, TV>(Plugin owner, TK target, T item, Func<T, int> getSize,
Func<int, T, bool> keepSearchGoing, Func<int, TK, T, TV> handleSearchIndex, Action<TV> callback,
float interval,
int completePerTick = 1, float initialDelay = 0f, string id = null)
{
var found = false;
var index = 0;
return GetAsynchronousRepeatingTask(owner, () =>
{
var max = index + completePerTick;
for (var i = index; i < max && i < getSize.Invoke(item); i++)
{
var value = handleSearchIndex.Invoke(i, target, item);
if (value == null) continue;
found = true;
callback.Invoke(value);
return false;
}
index = max;
return keepSearchGoing.Invoke(index, item);
}, interval, initialDelay, id, () =>
{
if (found) return;
callback.Invoke(default(TV));
});
}
/// <summary>
/// Find a player from the server asynchronously
/// </summary>
/// <param name="owner">Owner plugin</param>
/// <param name="data">Data about the player (name/id)</param>
/// <param name="callback">Callback with player or <see langword="null" /></param>
/// <param name="includeName">Should we check for name?</param>
/// <param name="ignoreCase">Should we check name ignore case?</param>
/// <param name="interval">Interval between each repetition</param>
/// <param name="completePerTick">How many loops to complete each tick</param>
/// <param name="initialDelay">Any initial delay</param>
/// <param name="reverse">Start in reverse?</param>
/// <param name="startIndex">Start index</param>
/// <param name="id">Id of this coroutine</param>
/// <typeparam name="T"></typeparam>
private void FindPlayerAsynchronously<T>(Plugin owner, string data, Action<T> callback, bool includeName = true,
bool ignoreCase = false, float interval = 0.01f, int completePerTick = 15, float initialDelay = 0f,
bool reverse = false, int startIndex = -1, string id = null) where T : class, IPlayer
{
var list = players.Connected.ToList();
if (list.Count == 0)
{
callback(null);
return;
}
var found = false;
LoopListAsynchronously(owner, player =>
{
if (!player.Id.Equals(data) && (!includeName || !player.Name.Equals(data,
ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.Ordinal)))
return false;
callback(player as T);
found = true;
return true;
}, list, interval, startIndex, reverse, completePerTick,
initialDelay, id,
() =>
{
if (found) return;
callback(null);
});
}
/// <summary>
/// Get a <see cref="IEnumerable{T}"/> of coroutines with that id
/// </summary>
/// <param name="id">Target id</param>
/// <returns><see cref="IEnumerable{T}"/> of coroutines</returns>
private IEnumerable<Coroutine> GetCoroutinesById(string id)
{
return GetAllCoroutines().Select(coroutines =>
coroutines.FirstOrDefault(coroutine => Equals(coroutine.Id, id))).DefaultIfEmpty();
}
/// <summary>
/// Check if a coroutine is running
/// </summary>
/// <param name="id">Coroutine id</param>
/// <returns><see langword="true" /> if coroutine is running</returns>
public bool IsCoroutineRunning(string id)
{
var coroutine = GetCoroutinesById(id).FirstOrDefault();
return coroutine != null && !coroutine.Stop;
}
/// <summary>
/// Stop a coroutine with a given id
/// </summary>
/// <param name="id">Given id</param>
/// <returns>If a coroutine was stopped</returns>
[HookMethod("StopCoroutine")]
public bool StopCoroutine(string id)
{
return GetCoroutinesById(id).All(StopCoroutine);
}
/// <summary>
/// Stop coroutines started by <see cref="owner" />
/// </summary>
/// <param name="owner">Target plugin</param>
[HookMethod("StopCoroutines")]
private void StopCoroutines(Plugin owner)
{
LinkedList<Coroutine> coroutines;
if (!TryGetCoroutines(owner.Name, out coroutines)) return;
StopCoroutines(coroutines);
}
#endregion
#region Coroutines
/// <summary>
/// Start checking the queue from <see cref="_queuedCoroutines" />
/// </summary>
private void StartCheckingQueue()
{
if (_config.CheckQueueOnFrame || _queuedCoroutines.Count == 0) return;
timer.In(_config.QueueCheckTime, () =>
{
CheckQueue();
StartCheckingQueue();
});
}
/// <summary>
/// Check if we should remove any coroutines from the queue
/// </summary>
private void CheckQueue()
{
var count = _currentCoroutineCount;
if (!CanTakeCoroutine(count)) return;
var allowed = Math.Min(_config.MaxRoutines - count, _queuedCoroutines.Count - 1);
RemoveFromQueue(allowed);
}
/// <summary>
/// Remove <see cref="Coroutine" /> from the queue & run it
/// </summary>
/// <param name="allowed">Amount of <see cref="Coroutine" /> to run</param>
private void RemoveFromQueue(int allowed)
{
var currentNode = _queuedCoroutines.First;
var i = -1;
while (currentNode != null && i < allowed)
{
var coroutine = currentNode.Value;
if (coroutine.HasCounter && !CanTakeCoroutine(coroutine.Id, coroutine.CachedMaxInstances)) goto end;
i++;
ForcefullyAddCoroutine(coroutine);
_queuedCoroutines.Remove(currentNode);
end:
currentNode = currentNode.Next;
}
}
/// <summary>
/// Are there too many coroutines?
/// </summary>
/// <param name="count">Current number (if applicable)</param>
/// <returns>If we can add another coroutine</returns>
private bool CanTakeCoroutine(int count = -1)
{
if (count == -1) count = _currentCoroutineCount;
return count < _config.MaxRoutines;
}
/// <summary>
/// Return the corresponding maximum from <see cref="_idToMax" />
/// </summary>
/// <param name="id">Target id</param>
/// <returns>The instance maximum if applicable</returns>
private int GetMaximumInstances(string id)
{
int max;
return !string.IsNullOrEmpty(id) && _idToMax.TryGetValue(id, out max) ? max : -1;
}
/// <summary>
/// Create a <see cref="Coroutine" />
/// </summary>
/// <param name="owner">Owner</param>
/// <param name="enumerator">The instructions (leave null to create instructions</param>
/// <param name="id">Id of this coroutine</param>
/// <param name="onComplete">Callback when the task completes</param>
/// <returns>A <see cref="Coroutine" /> to run</returns>
[HookMethod("CreateCoroutine")]
private Coroutine CreateCoroutine(Plugin owner, IEnumerator enumerator = null, string id = null,
Action onComplete = null)
{
var instructions = enumerator;
var max = GetMaximumInstances(id);
var routine = new Coroutine(owner, instructions, onComplete, id, max);
return routine;
}
/// <summary>
/// Start a <see cref="Coroutine" />
/// </summary>
/// <param name="coroutine">Target coroutine</param>
/// <returns>
/// <see langword="true" /> if the coroutine was stored & <see langword="false" /> if the coroutine is
/// <see langword="null" /> or it was queued
/// </returns>
public bool StartCoroutine(Coroutine coroutine)
{
if (coroutine == null) return false; //just in case you don't check it
if (!CanTakeCoroutine() || !CanTakeCoroutine(coroutine.Id, coroutine.CachedMaxInstances))
{
if (_queuedCoroutines.AddLast(coroutine).Previous == null) StartCheckingQueue(); //if we're the first element
return false;
}
ForcefullyAddCoroutine(coroutine);
return true;
}
/// <summary>
/// Are there too many coroutines with the id of <see cref="id" /> (max being <see cref="max" />)
/// </summary>
/// <param name="id">Target id</param>
/// <param name="max">Target maximum</param>
/// <returns>
/// <see langword="true" /> if the amount of active coroutines with the id <see cref="id" /> is less than
/// <see cref="max" />
/// </returns>
private bool CanTakeCoroutine(string id, int max)
{
if (max == -1 || string.IsNullOrEmpty(id)) return true;
Number count;
var check = false;
if (!_maxCoroutineCounter.TryGetValue(id, out count))
{
_maxCoroutineCounter[id] = count = new Number(max);
check = true;
}
var newValue = count.Value - 1;
var ret = newValue > 0;
if ((newValue == 0 || ret) && !check) count.Value = newValue;
return ret;
}
/// <summary>
/// Forcefully add a coroutine
/// </summary>
/// <param name="coroutine">Target coroutine</param>
private void ForcefullyAddCoroutine(Coroutine coroutine)
{
var ownerName = coroutine.Owner.Name;
LinkedList<Coroutine> coroutines;
if (!TryGetCoroutines(ownerName, out coroutines))
{
_coroutines.AddLast(new KeyValuePair<string, LinkedList<Coroutine>>(ownerName,
coroutines = new LinkedList<Coroutine>()));
_currentCoroutineCount++;
}
coroutines.AddLast(coroutine);
}
/// <summary>
/// Stop a <see cref="IEnumerable{T}" /> of coroutines
/// </summary>
/// <param name="coroutines"><see cref="IEnumerable{T}" /> of coroutines</param>
private static void StopCoroutines(IEnumerable<Coroutine> coroutines)
{
foreach (var coroutine in coroutines) StopCoroutine(coroutine);
}
/// <summary>
/// Stop an individual coroutine
/// </summary>
/// <param name="coroutine">Target coroutine</param>
private static bool StopCoroutine(Coroutine coroutine)
{
if (coroutine == null) return false;
return coroutine.Stop = true;
}
/// <summary>
/// Returns coroutine instructions that wait an initial delay before completing a task
/// </summary>
/// <param name="initialDelay">Time to wait</param>
/// <param name="task">Task to complete</param>
/// <param name="waitForSeconds">The initial wait object if it exists</param>
/// <returns></returns>
private static IEnumerator GetInitialDelayCoroutine(float initialDelay = 0f, Action task = null,
WaitForSeconds waitForSeconds = null)
{
waitForSeconds = waitForSeconds ?? new WaitForSeconds(initialDelay);
yield return waitForSeconds;
task?.Invoke();
}
/// <summary>
/// Get the coroutine to start it
/// </summary>
/// <param name="continuePredicate">Returns if the task should keep repeating</param>
/// <param name="interval">Interval between repetitions</param>
/// <param name="initialDelay">Any initial delay</param>
/// <returns>A <see cref="IEnumerator" /> for a coroutine</returns>
private static IEnumerator GetRepeatingCoroutine(Func<bool> continuePredicate,
float interval,
float initialDelay = 0f)
{
var hasInitial = initialDelay > 0f;
var wait = new WaitForSeconds(hasInitial ? initialDelay : interval);
if (hasInitial)
{
yield return GetInitialDelayCoroutine(waitForSeconds: wait);
wait.Reset(interval);
}
while (continuePredicate.Invoke())
{
yield return wait;
wait.Reset();
}
}
#endregion
#region Helpers
/// <summary>
/// Get a list of all the coroutines
/// </summary>
/// <returns>All coroutines</returns>
private IEnumerable<LinkedList<Coroutine>> GetAllCoroutines()
{
return _coroutines.Select(entry => entry.Value);
}
/// <summary>
/// Finds the corresponding <see cref="coroutines" /> from <see cref="owner" />
/// </summary>
/// <param name="owner">Target owner</param>
/// <param name="coroutines">Output coroutines</param>
/// <returns>If we found coroutines</returns>
private bool TryGetCoroutines(string owner, out LinkedList<Coroutine> coroutines)
{
foreach (var entry in _coroutines.Where(entry => entry.Key.Equals(owner)))
{
coroutines = entry.Value;
return true;
}
coroutines = null;
return false;
}
/// <summary>
/// Restart a stopwatch (a feature in modern.NET)
/// </summary>
/// <param name="stopwatch">Target stopwatch</param>
private static void RestartStopWatch(Stopwatch stopwatch)
{
stopwatch.Reset();
stopwatch.Start();
}
/// <summary>
/// Change <see cref="time" /> to milliseconds
/// </summary>
/// <param name="time">Time in seconds</param>
/// <returns><see cref="time" /> as milliseconds</returns>
private static float SecondsToMilliseconds(float time)
{
return time * 1000f;
}
private class Number
{
public Number(int value)
{
Value = value;
Max = value;
}
public int Max { get; }
public int Value { get; set; }
}
#endregion
#region Configuration
private ConfigFile _config;
private class ConfigFile
{
[JsonProperty("Max Routines (others will be stacked up in a queue)")]
public int MaxRoutines { get; set; }
[JsonProperty("Queue Check Time (Seconds)")]
public float QueueCheckTime { get; set; }
[JsonProperty("Check Queue on Frame?")]
public bool CheckQueueOnFrame { get; set; }
[JsonProperty("Execution Warn Time (Milliseconds)")]
public long WarnTimeMilliseconds { get; set; }
[JsonProperty("Print Stacktrace on Warn?")]
public bool PrintStackTraceOnWarn { get; set; }
[JsonProperty("Use scaled time for time calculations?")]
public bool UseScaledTime { get; set; }
public static ConfigFile DefaultConfig()
{
return new ConfigFile
{
MaxRoutines = 100,
QueueCheckTime = 1f,
WarnTimeMilliseconds = 150,
PrintStackTraceOnWarn = false,
CheckQueueOnFrame = false,
UseScaledTime = true
};
}
}
protected override void LoadConfig()
{
base.LoadConfig();
_config = Config.ReadObject<ConfigFile>();
if (_config == null) LoadDefaultConfig();
}
protected override void LoadDefaultConfig()
{
_config = ConfigFile.DefaultConfig();
PrintWarning("Default configuration has been loaded.");
}
protected override void SaveConfig()
{
Config.WriteObject(_config);
}
#endregion
}
}