using System.Collections.Generic;
using Cthangover.Core.Events;
using Cthangover.Core.UI.Dialog;
using Cthangover.Core.Utils;
using Godot;
namespace Mods.TestRunner
{
///
/// Мод-порт интеграционного DialogAutoDriver: автопроигрывает
/// активный диалог, кликая IDialogBox.NextAction() каждые 0.1с и
/// отвечая на выборы по индексам из --choices= (по порядку, после
/// исчерпания — первый вариант). Тик реализован дочерним Godot
/// , а не override _Process: сборка мода
/// компилируется Roslyn вне source-генератора Godot, поэтому виртуальные
/// методы Node не диспетчеризуются, тогда как сигнал Timer.Timeout
/// — это Godot-объект и работает из мод-сборок. DialogBox ищется лениво
/// через корень дерева по имени «DialogBox» (контракт ядра
/// , типы dialog_base не нужны). Завершение
/// диалога и общий таймаут печатают строку
/// [TEST_RESULT] name=test_runner passed=N failed=M — формат парсит
/// tests/Cthangover.TestRunner (ResultParser), после чего процесс
/// завершается с соответствующим кодом выхода. Как и в оригинале,
/// IsDialogEnded истинно до загрузки первой очереди, поэтому
/// end-обработка ждёт, пока диалог реально наблюдался запущенным, плюс
/// стартовая пауза 0.5с на построение сцены.
/// With --dialogs=N the run passes after N s
/// instead: hub games never go idle, because the next scene's dialog starts
/// in the same frame the previous one ends.
///
public partial class DialogAutoDriverNode : Node
{
/// Имя, под которым раннер атрибутирует результат прогона.
private const string ResultName = "test_runner";
/// Пауза после старта, чтобы сцена успела построиться (как в SceneSwitcher).
private const double StartDelaySeconds = 0.5;
/// Сколько бокс может быть найден, но не запущен, прежде чем считаем сцену без диалога.
private const double FirstDialogGraceSeconds = 3.0;
/// Интервал автопрокликивания, сек (как в оригинальном драйвере).
private const double AdvanceIntervalSeconds = 0.1;
private readonly List choices;
private readonly float? timeoutSeconds;
private readonly int? dialogsTarget;
private IDialogBox dialogBox;
private int choiceIndex;
private double elapsedSeconds;
private double dialogFoundAtSeconds;
private bool hasSeenRunningDialog;
private bool finished;
private int endedDialogs;
///
/// Создаёт драйвер; — индексы вариантов по
/// порядку появления, — общий лимит
/// прогона (null — без таймаута). Тикающий таймер-ребёнок ставится
/// прямо в конструкторе (без override _EnterTree/_Ready — мод-сборка
/// вне source-генератора Godot): WaitTime 0.1с с автостартом
/// воспроизводит ритм оригинального драйвера, а сигнал
/// Timer.Timeout — Godot-сигнал и работает из мод-сборок.
/// — number of finished dialogs that passes
/// the run (null — pass when the box goes idle).
///
public DialogAutoDriverNode(List choices, float? timeoutSeconds, int? dialogsTarget)
{
this.choices = choices ?? new List();
this.timeoutSeconds = timeoutSeconds;
this.dialogsTarget = dialogsTarget;
if (dialogsTarget.HasValue)
ModEventBus.Subscribe(OnDialogEnded);
var timer = new Timer
{
Name = "TickTimer",
WaitTime = AdvanceIntervalSeconds,
Autostart = true,
OneShot = false
};
AddChild(timer);
timer.Timeout += Tick;
}
/// Один тик автопроигрывания: ленивый поиск бокса, прокликивание, выборы, end/timeout-обработка.
private void Tick()
{
if (finished)
return;
elapsedSeconds += AdvanceIntervalSeconds;
if (timeoutSeconds.HasValue && elapsedSeconds >= timeoutSeconds.Value)
{
Finish(0, 1, $"timeout ({timeoutSeconds.Value}s) reached");
return;
}
if (elapsedSeconds < StartDelaySeconds)
return;
if (dialogBox is not GodotObject liveBox || !GodotObject.IsInstanceValid(liveBox) || liveBox is not Node liveNode || !liveNode.IsInsideTree())
{
dialogBox = FindDialogBox();
if (dialogBox == null)
return;
dialogFoundAtSeconds = elapsedSeconds;
hasSeenRunningDialog = false;
var choicesInfo = choices.Count > 0
? "choices=[" + string.Join(",", choices) + "]"
: "no choices, will pick first variant each time";
GameLogger.Log("TEST", $"DialogAutoDriverNode started, interval={AdvanceIntervalSeconds}, {choicesInfo}");
}
if (dialogBox.IsRunning)
hasSeenRunningDialog = true;
// IsDialogEnded истинно и до первой очереди — ждём наблюдения
// запущенного диалога плюс льготный период на пустых сценах.
var waitingForFirstDialog = !hasSeenRunningDialog
&& elapsedSeconds - dialogFoundAtSeconds < FirstDialogGraceSeconds;
if (!dialogsTarget.HasValue && dialogBox.IsDialogEnded && !waitingForFirstDialog)
{
GameLogger.Log("TEST", "DialogAutoDriverNode: dialog ended, quitting");
Finish(1, 0, null);
return;
}
if (dialogBox.IsWaitingAnswer)
{
HandleSelection();
return;
}
dialogBox.NextAction();
}
/// Counts finished dialogs and passes the run once the --dialogs= target is reached.
private void OnDialogEnded(DialogEndedEvent evt)
{
endedDialogs++;
GameLogger.Log("TEST", $"DialogAutoDriverNode: dialog #{endedDialogs} ended");
if (endedDialogs >= dialogsTarget.Value)
Finish(1, 0, $"{endedDialogs} dialog(s) ended, quitting");
}
/// Выбирает показанный вариант по индексу из --choices= (вне диапазона — первый).
private void HandleSelection()
{
var count = dialogBox.AnswerVariantCount;
if (count <= 0)
return;
var pick = 0;
if (choiceIndex < choices.Count)
{
pick = choices[choiceIndex];
if (pick >= count)
pick = 0;
choiceIndex++;
}
GameLogger.Log("TEST", $"DialogAutoDriverNode: choice #{choiceIndex} picking index={pick} out of {count}");
dialogBox.TrySelectVariant(pick);
}
///
/// Ищет DialogBox через корень дерева: драйвер часто добавляется до
/// появления диалоговой сцены, а каст к контракту ядра избавляет мод
/// от ссылки на конкретную реализацию.
///
private IDialogBox FindDialogBox()
{
var tree = GetTree();
if (tree?.Root == null)
return null;
return tree.Root.FindChild("DialogBox", true, false) as IDialogBox;
}
/// Печатает итоговую строку для раннера и завершает процесс (0 — успех, 1 — провал).
private void Finish(int passed, int failed, string reason)
{
if (finished)
return;
finished = true;
if (dialogsTarget.HasValue)
ModEventBus.Unsubscribe(OnDialogEnded);
if (reason != null)
GameLogger.Log("TEST", $"DialogAutoDriverNode: {reason}");
GameLogger.Log("TEST", $"[TEST_RESULT] name={ResultName} passed={passed} failed={failed}");
Cthangover.Core.Bootstrap.ApplicationExit.Quit(GetTree(), failed == 0 ? 0 : 1);
}
}
}