Files
necromants-tome-7d2d-3-2/HarmonySrc/CinerariumLoad.cs
T
AlexCubeandClaude Opus 5.5 108ff82914 Урны: бюджет дел в секунду с запасом и очередь вместо «3 за кадр» (#78)
Замер necrourn budget: тики всех урн приходят пачкой раз в ~1.4 с в порядке участков от ближних к игроку;
лимит «3 за кадр» доставался трём ближним, 11 урн (сборщики, логист, дальние) не получали шага никогда -
«стою в 19 м - не работают, подхожу - работают». Теперь 30 дел/с, запас 2 с, пополнение по реальному
времени; отказанная урна в очереди и обслуживается первой. После: 578 с, 19 урн по ~128 шагов, отказов 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:15:57 +03:00

240 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
/// <summary>
/// Меры нагрузки урн (решение 17.10: «общий бюджет шагов работы духов за кадр (очередь по кругу - нагрузка постоянна при
/// любом числе урн); полная анимация только ближайших ~15-20 духов; замер 100 урн»).
/// Бюджет: дел (пробный шаг + настоящий) в секунду - не больше StepsPerSecond на все урны сейва (23.10, было за кадр); урна, которой не досталось,
/// пробует в следующем кадре. Урна делает дело раз в CinerariumWork.StepSeconds, поэтому очередь сама идёт по кругу.
/// Анимация: двигаются (полёт, жесты, покачивание) только AnimateNearest ближайших к игроку духов, остальные замирают дома,
/// как уснувшие (CinerariumGenieMotion); работа у всех идёт.
/// Прилив (решение 17.10, «Прилив прячет некротехнологов»): ночью кровавой луны духи всех урн уходят в урны и работа стоит -
/// признак берётся раз за кадр.
/// </summary>
public static class CinerariumLoad
{
/// <summary>Дел в секунду на все урны (23.10, было «3 за кадр»). Замер: урна получает тик раз в ~1.4 с, и тики всех урн
/// приходят пачкой в одном кадре, в порядке участков от ближних к игроку. Лимит «3 за кадр» доставался трём ближним урнам,
/// дальние не получали шага никогда («стою в 19 м - не работают, подхожу - работают»). Теперь бюджет копится между
/// тиками (не больше StepsPerSecond за раз), и пачка обслуживает всех, у кого подошёл срок. Консоль: necrourn budget N.</summary>
public static float StepsPerSecond = 30f;
/// <summary>Запас бюджета, секунд (пачка тиков раз в ~1.4 с должна уместиться целиком).</summary>
public const float BurstSeconds = 2f;
static float _tokens;
static float _lastRefill;
/// <summary>Очередь (23.10, пользователь: «нужно иметь очередь урн»): урна, которой отказали, ждёт; пока очередь не пуста,
/// бюджет сначала ей - новая урна получит шаг, только если после всех ждущих ещё останется. Иначе ближние урны (их участки
/// идут первыми) снова забирали бы всё. Запись старше QueueForget секунд выбрасывается (урну сняли или участок выгружен).</summary>
static readonly Dictionary<Vector3i, float> _queue = new Dictionary<Vector3i, float>();
const float QueueForget = 10f;
static readonly List<Vector3i> _stale = new List<Vector3i>();
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<Vector3i, float> kv in _queue)
{
if (now - kv.Value > QueueForget)
{
_stale.Add(kv.Key);
}
}
foreach (Vector3i p in _stale)
{
_queue.Remove(p);
}
_tide = SnakeMode.IsBloodTide();
}
}
/// <summary>Взять одно дело из бюджета кадра. _who - урна (учёт для necrourn budget).</summary>
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<Vector3i, int> _granted = new Dictionary<Vector3i, int>();
static readonly Dictionary<Vector3i, int> _denied = new Dictionary<Vector3i, int>();
static float _countSince;
static int _frames0;
static void Count(Dictionary<Vector3i, int> _d, Vector3i _who)
{
_d.TryGetValue(_who, out int n);
_d[_who] = n + 1;
}
/// <summary>Сводка с прошлого вызова и сброс (команда necrourn budget).</summary>
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<Vector3i, int> kv in _granted)
{
s += " " + kv.Key + "=" + kv.Value;
}
s += "; отказано:";
foreach (KeyValuePair<Vector3i, int> kv in _denied)
{
s += " " + kv.Key + "=" + kv.Value;
}
_granted.Clear();
_denied.Clear();
_countSince = Time.time;
_frames0 = Time.frameCount;
return s;
}
/// <summary>Идёт ли кровавый прилив (раз за кадр).</summary>
public static bool Tide
{
get
{
NewFrame();
return _tide;
}
}
// ------------------------------------------------------------------
// Ближайшие духи.
// ------------------------------------------------------------------
static float _nextRank;
static readonly HashSet<CinerariumGenieMotion> _animated = new HashSet<CinerariumGenieMotion>();
static readonly List<CinerariumGenieMotion> _sorted = new List<CinerariumGenieMotion>();
/// <summary>Двигается ли дух: среди AnimateNearest ближайших к игроку (пересчёт раз в 0.5 с, без выделений памяти).</summary>
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]);
}
}
}
/// <summary>necrofps [секунд] - средний и худший кадр за время замера (по умолчанию 5 с), число духов и урн; ответ - строкой
/// почтового ящика в лог. Для замера нагрузки урн (17.10: «замер 100 урн»).</summary>
[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<string> _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));
}
/// <summary>_uncap - на время замера снять вертикальную синхронизацию и предел кадров: при 60 fps по синхронизации
/// запас не виден (18.10, замер 100 урн).</summary>
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);
}
}
/// <summary>necrotide on|off|auto - прилив для проверок без ожидания ночи кровавой луны (SnakeMode.ForceTide).</summary>
[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<string> _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() ? "прилив" : "не прилив"));
}
}
}