using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.UI.Menu;
using Cthangover.Core.Utils;
using Cthangover.Mods.Interface.Menu;
using Cthangover.Mods.Interface.Theming;
using Cthangover.Core.UI.Theming;
using Cthangover.Mods.Interface.View;
using Godot;
using Mods.ModManager;
namespace Mods.ModManagerHelpTest
{
using System.Text.Json;
using Cthangover.Core.Mods;
///
/// Exercises contextual help as a player uses it: pin a hint beside a mod search,
/// keep the hub interactive while help is open, inspect a mod page and the scheme
/// legend, invoke F1 from keyboard focus and leave with Escape without navigating
/// away. Also rejects tooltips that merely repeat their button captions and
/// exercises Graphite's scoped version edits, duplicate occurrences, zoom and
/// overview, shared dialog chrome and title-bar close hit areas. Collection
/// edits remain drafts and the user's layout is restored.
/// --shots=<dir> saves the inspected states. Prints mod_help's result.
///
public partial class ModHelpTestDriverNode : Node
{
private const string HubId = "mods_hub";
private const string GameId = "novel_template";
private const float TimeoutSeconds = 90f;
private readonly List failures = new();
private int passed;
private int step;
private double elapsed;
private double stepAt;
private bool done;
private ModHubScreen hub;
private ModCollectionsScreen collectionEditor;
private CollectionSchemeCanvas scheme;
private Vector2 firstOccurrencePosition;
private string layoutPath;
private byte[] layoutBackup;
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > TimeoutSeconds)
{
failures.Add($"timeout at step {step}");
Finish();
return;
}
if (elapsed - stepAt < 0.5)
return;
stepAt = elapsed;
try
{
RunStep();
}
catch (Exception ex)
{
failures.Add($"step {step}: {ex.GetType().Name}: {ex.Message}");
Finish();
}
}
private void RunStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady)
return;
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
{
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
return;
}
if (GetTree().CurrentScene?.FindChild("play", true, false) is not Button)
return;
var entry = MenuRegistry.GetButtonById(HubId);
hub = MenuBuilder.OpenScene((Control)GetTree().CurrentScene, entry?.ScenePath, HubId) as ModHubScreen;
Check("the hub opens", hub != null);
if (hub == null)
Finish();
break;
case 1:
{
PressTab("mod_manager/hub_tab_mods");
CheckLevel("mods");
PressHelp();
ModHelpPanel help = Help();
Check("the header enables help mode", help != null && help.Visible);
Check("help has no dim backdrop", help != null && help.GetNodeOrNull("Dim") == null);
Check("help does not capture the mouse", help != null && help.MouseFilter == Control.MouseFilterEnum.Ignore);
Control search = Annotated("mod_search");
CheckHelp("mods_search", search);
break;
}
case 2:
{
ModHelpPanel help = Help();
Check("a shown hint is pinned", help != null && help.IsPinned);
var card = hub.FindChildren("*", "", true, false).OfType().FirstOrDefault(c => c.IsVisibleInTree());
Check("the grid shows mod cards", card != null);
card?.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton { ButtonIndex = MouseButton.Left, Pressed = true });
break;
}
case 3:
{
Check("the hub stays interactive while help is open", hub.FindChildren("*", "", true, false).OfType().Any(d => d.IsVisibleInTree()));
CheckLevel("mod_page");
CheckHelp("mod_version", Annotated("mod_version"));
PressTab("mod_manager/hub_tab_collections");
break;
}
case 4:
{
Check("collections open without closing help", Gallery() != null && Gallery().IsVisibleInTree() && Help().Visible);
CheckLevel("gallery");
EscapeHelp();
hub.OpenCollectionEditor(GameId);
break;
}
case 5:
{
CheckLevel("scheme");
Control legend = Annotated("load_order");
Check("the scheme always shows its load-order legend", legend != null && legend.IsVisibleInTree());
CheckHelp("scheme_load_order", legend);
var group = hub.FindChildren("*", "", true, false).OfType().FirstOrDefault(g => g.IsVisibleInTree());
Check("the scheme shows mechanics", group != null);
Click(group);
break;
}
case 6:
{
CheckLevel("scheme_mechanic");
var chip = hub.FindChildren("*", "", true, false).OfType().FirstOrDefault(c => c.IsVisibleInTree());
Check("the mechanic shows its mods", chip != null);
Click(chip);
break;
}
case 7:
{
CheckLevel("scheme_mod");
Control links = Annotated("collection_links");
Check("the links toggle has keyboard focus", links != null);
links?.GrabFocus();
hub._UnhandledInput(new InputEventKey { Keycode = Key.F1, Pressed = true });
ModHelpPanel help = Help();
Check("F1 explains the focused control", help != null && help.CurrentTarget == links && help.IsPinned);
Shot("scheme_f1");
break;
}
case 8:
// Leave the F1 state visible for the deferred screenshot.
EscapeHelp();
Check("Escape leaves help without closing the editor", Annotated("load_order") != null);
hub.OpenCollectionEditor("cthangover");
break;
case 9:
collectionEditor = FindControl(hub);
scheme = FindControl(collectionEditor);
Check("graphite has the existing native canvas", scheme != null);
Check("graphite has a scene-defined version form", collectionEditor?.FindChild("VersionEditor", true, false) != null);
Check("graphite has a visible overview", FindControl(collectionEditor)?.Size.X > 0);
CheckSourceSyntax();
if (GameEnvironment.IsRuntime)
{
layoutPath = ProjectSettings.GlobalizePath("user://collection_layout.json");
layoutBackup = File.Exists(layoutPath) ? File.ReadAllBytes(layoutPath) : null;
}
Shot("graphite_01_collection");
break;
case 10:
SetGraphiteFixture();
break;
case 11:
var second = FindOccurrence("m:cthangover:second");
firstOccurrencePosition = FindOccurrence("m:cthangover:first").Position;
Click(second);
Check("shared mod selection retains its clicked group", scheme.SelectedGroupKey == "m:cthangover:second");
Check("version form reads the selected occurrence's pin", VersionField("Value").Text == "1");
Shot("graphite_02_shared_context");
break;
case 12:
SelectVersionPolicy("=");
VersionField("Value").Text = "999.0.0";
PressEditorButton("SaveRule");
Shot("graphite_03_version_conflict");
break;
case 13:
Check("incompatible version blocks apply", collectionEditor.FindChild("Apply", true, false) is Button apply && apply.Disabled);
PressEditorButton("Json");
Shot("graphite_06_json_dialog");
break;
case 14:
var jsonDialog = FindControl(collectionEditor);
CheckDialogChrome("JSON", jsonDialog);
var jsonText = FindControl(jsonDialog);
Check("collection JSON uses a highlighted code editor", jsonText is CodeEdit && jsonText.SyntaxHighlighter != null);
var edited = JsonSerializer.Deserialize(jsonText.Text);
Check("version edit preserves the other group", edited.Mechanics[0].Mods[0] == "ui_base>=0");
Check("version edit survives the real JSON editor", edited.Mechanics[1].Mods[0] == "ui_base=999.0.0");
jsonDialog.EmitSignal(ConfirmationDialog.SignalName.Canceled);
break;
case 15:
SelectVersionPolicy(">=");
VersionField("Value").Text = "1";
PressEditorButton("SaveRule");
PressEditorButton("TabProblems");
Check("restoring a compatible pin clears problems", HasLabel(collectionEditor, TranslationServer.Translate("mod_manager/collection_problems_none")));
PressEditorButton("TabInspector");
var zoom = scheme.Zoom;
PressEditorButton("ZoomOut");
Check("zoom updates the view scale", scheme.Zoom < zoom);
Check("zoom preserves logical chip positions", FindOccurrence("m:cthangover:first").Position == firstOccurrencePosition);
break;
case 16:
var movedChip = FindOccurrence("m:cthangover:second");
var beforeMove = movedChip.Position;
MoveOccurrence(movedChip, new Vector2(12f, 4f));
var stored = CollectionLayoutStore.Load("cthangover");
var position = stored.Mods[movedChip.GroupKey + "|" + movedChip.ModId];
Check("drag persists the selected occurrence", Mathf.IsEqualApprox(position.X, beforeMove.X + 12f));
Check("drag leaves the first occurrence unchanged", FindOccurrence("m:cthangover:first").Position == firstOccurrencePosition);
scheme.CenterOn(new Vector2(2000f, 1500f));
var beforePan = scheme.VisibleBounds.Position;
var map = FindControl(collectionEditor);
map.EmitSignal(Control.SignalName.GuiInput, new InputEventMouseButton
{
ButtonIndex = MouseButton.Left, Pressed = true, Position = map.Size / 2f,
});
Check("overview click moves the scroll viewport", scheme.VisibleBounds.Position != beforePan);
break;
case 17:
PressEditorButton("Fit");
Shot("graphite_04_layout");
break;
case 18:
SetIncludedFixture();
break;
case 19:
Click(FindOccurrence("i:cthangover:dev_tools>=1", "editor_tools"));
SelectVersionPolicy("=");
VersionField("Value").Text = "not a version?";
PressEditorButton("SaveRule");
Check("invalid input is explained", VersionField