using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Scripting; namespace NecromancerTome { /// /// Меры нагрузки урн (решение 17.10: «общий бюджет шагов работы духов за кадр (очередь по кругу - нагрузка постоянна при /// любом числе урн); полная анимация только ближайших ~15-20 духов; замер 100 урн»). /// Бюджет: дел (пробный шаг + настоящий) в секунду - не больше StepsPerSecond на все урны сейва (23.10, было за кадр); урна, которой не досталось, /// пробует в следующем кадре. Урна делает дело раз в CinerariumWork.StepSeconds, поэтому очередь сама идёт по кругу. /// Анимация: двигаются (полёт, жесты, покачивание) только AnimateNearest ближайших к игроку духов, остальные замирают дома, /// как уснувшие (CinerariumGenieMotion); работа у всех идёт. /// Прилив (решение 17.10, «Прилив прячет некротехнологов»): ночью кровавой луны духи всех урн уходят в урны и работа стоит - /// признак берётся раз за кадр. /// public static class CinerariumLoad { /// Дел в секунду на все урны (23.10, было «3 за кадр»). Замер: урна получает тик раз в ~1.4 с, и тики всех урн /// приходят пачкой в одном кадре, в порядке участков от ближних к игроку. Лимит «3 за кадр» доставался трём ближним урнам, /// дальние не получали шага никогда («стою в 19 м - не работают, подхожу - работают»). Теперь бюджет копится между /// тиками (не больше StepsPerSecond за раз), и пачка обслуживает всех, у кого подошёл срок. Консоль: necrourn budget N. public static float StepsPerSecond = 30f; /// Запас бюджета, секунд (пачка тиков раз в ~1.4 с должна уместиться целиком). public const float BurstSeconds = 2f; static float _tokens; static float _lastRefill; /// Очередь (23.10, пользователь: «нужно иметь очередь урн»): урна, которой отказали, ждёт; пока очередь не пуста, /// бюджет сначала ей - новая урна получит шаг, только если после всех ждущих ещё останется. Иначе ближние урны (их участки /// идут первыми) снова забирали бы всё. Запись старше QueueForget секунд выбрасывается (урну сняли или участок выгружен). static readonly Dictionary _queue = new Dictionary(); const float QueueForget = 10f; static readonly List _stale = new List(); public static int AnimateNearest = 16; static int _frame = -1; static bool _tide; static void NewFrame() { if (Time.frameCount != _frame) { _frame = Time.frameCount; // По реальному времени с прошлого пополнения: NewFrame зовут только кадры с тиками урн (раз в ~1.4 с), и // Time.deltaTime (один кадр) давал полшага на пачку (замер 23.10: 279 шагов за 600 с при лимите 30/с). float now = Time.time; _tokens = Mathf.Min(StepsPerSecond * BurstSeconds, _tokens + StepsPerSecond * Mathf.Max(0f, now - _lastRefill)); _lastRefill = now; _stale.Clear(); foreach (KeyValuePair kv in _queue) { if (now - kv.Value > QueueForget) { _stale.Add(kv.Key); } } foreach (Vector3i p in _stale) { _queue.Remove(p); } _tide = SnakeMode.IsBloodTide(); } } /// Взять одно дело из бюджета кадра. _who - урна (учёт для necrourn budget). public static bool TryTakeStep(Vector3i _who) { NewFrame(); bool waiting = _queue.ContainsKey(_who); // Ждущая урна - если есть хоть один шаг; новая - только если хватит и всем ждущим. if (_tokens < 1f || (!waiting && _tokens < 1f + _queue.Count)) { if (!waiting) { _queue[_who] = Time.time; } Count(_denied, _who); return false; } _tokens -= 1f; _queue.Remove(_who); Count(_granted, _who); return true; } // Учёт бюджета (23.10: сборщики 600 с не получили ни одного шага, логист рядом - получал): кому дали, кому отказали. static readonly Dictionary _granted = new Dictionary(); static readonly Dictionary _denied = new Dictionary(); static float _countSince; static int _frames0; static void Count(Dictionary _d, Vector3i _who) { _d.TryGetValue(_who, out int n); _d[_who] = n + 1; } /// Сводка с прошлого вызова и сброс (команда necrourn budget). public static string Budget() { float sec = Time.time - _countSince; int frames = Time.frameCount - _frames0; string s = "за " + sec.ToString("0") + " с, кадров " + frames + ", лимит " + StepsPerSecond + "/с, в очереди " + _queue.Count + "; дано:"; foreach (KeyValuePair kv in _granted) { s += " " + kv.Key + "=" + kv.Value; } s += "; отказано:"; foreach (KeyValuePair kv in _denied) { s += " " + kv.Key + "=" + kv.Value; } _granted.Clear(); _denied.Clear(); _countSince = Time.time; _frames0 = Time.frameCount; return s; } /// Идёт ли кровавый прилив (раз за кадр). public static bool Tide { get { NewFrame(); return _tide; } } // ------------------------------------------------------------------ // Ближайшие духи. // ------------------------------------------------------------------ static float _nextRank; static readonly HashSet _animated = new HashSet(); static readonly List _sorted = new List(); /// Двигается ли дух: среди AnimateNearest ближайших к игроку (пересчёт раз в 0.5 с, без выделений памяти). public static bool Animated(CinerariumGenieMotion _m) { if (Time.time >= _nextRank) { _nextRank = Time.time + 0.5f; Rank(); } return _animated.Contains(_m); } static void Rank() { _animated.Clear(); EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer(); if (pl == null) { return; } Vector3 me = pl.GetPosition() - Origin.position; _sorted.Clear(); _sorted.AddRange(CinerariumGenie.Live); if (_sorted.Count > AnimateNearest) { _sorted.Sort((a, b) => (a.transform.position - me).sqrMagnitude.CompareTo((b.transform.position - me).sqrMagnitude)); } for (int i = 0; i < _sorted.Count && i < AnimateNearest; i++) { _animated.Add(_sorted[i]); } } } /// necrofps [секунд] - средний и худший кадр за время замера (по умолчанию 5 с), число духов и урн; ответ - строкой /// почтового ящика в лог. Для замера нагрузки урн (17.10: «замер 100 урн»). [Preserve] public class ConsoleCmdNecroFps : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necrofps" }; public override string getDescription() => "NecromancerTome (разработка): средний и худший кадр за N секунд"; public override bool AllowedInMainMenu => false; public override void Execute(List _params, CommandSenderInfo _senderInfo) { float sec = _params.Count > 0 ? float.Parse(_params[0], System.Globalization.CultureInfo.InvariantCulture) : 5f; bool uncap = _params.Count > 1 && _params[1] == "uncap"; SdtdConsole.Instance.Output("necrofps: замер " + sec + " с" + (uncap ? " без ограничения кадров" : "")); ThreadManager.StartCoroutine(Measure(sec, uncap)); } /// _uncap - на время замера снять вертикальную синхронизацию и предел кадров: при 60 fps по синхронизации /// запас не виден (18.10, замер 100 урн). static IEnumerator Measure(float _sec, bool _uncap) { int vs = QualitySettings.vSyncCount, fr = Application.targetFrameRate; if (_uncap) { QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; yield return new WaitForSecondsRealtime(0.5f); } float end = Time.realtimeSinceStartup + _sec; int frames = 0; float worst = 0f, sum = 0f; long gc0 = System.GC.CollectionCount(0); while (Time.realtimeSinceStartup < end) { yield return null; frames++; sum += Time.unscaledDeltaTime; worst = Mathf.Max(worst, Time.unscaledDeltaTime); } if (_uncap) { QualitySettings.vSyncCount = vs; Application.targetFrameRate = fr; } float avg = frames > 0 ? sum / frames * 1000f : 0f; Debug.Log("[NecromancerTome] mailbox: necrofps: кадров " + frames + ", средний " + avg.ToString("F1") + " мс (" + (avg > 0 ? 1000f / avg : 0f).ToString("F0") + " fps), худший " + (worst * 1000f).ToString("F1") + " мс, сборок мусора " + (System.GC.CollectionCount(0) - gc0) + ", духов " + CinerariumGenie.Live.Count + ", урн в сейве " + CinerariumRegistry.All().Count); } } /// necrotide on|off|auto - прилив для проверок без ожидания ночи кровавой луны (SnakeMode.ForceTide). [Preserve] public class ConsoleCmdNecroTide : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necrotide" }; public override string getDescription() => "NecromancerTome (разработка): прилив on|off|auto"; public override bool AllowedInMainMenu => false; public override void Execute(List _params, CommandSenderInfo _senderInfo) { string v = _params.Count > 0 ? _params[0] : ""; SnakeMode.ForceTide = v == "on" ? true : v == "off" ? (bool?)false : null; SdtdConsole.Instance.Output("necrotide: " + (SnakeMode.ForceTide.HasValue ? (SnakeMode.ForceTide.Value ? "прилив" : "нет прилива") : "по часам") + ", сейчас " + (SnakeMode.IsBloodTide() ? "прилив" : "не прилив")); } } }