init mod
This commit is contained in:
commit
ee6fdc3576
32 files changed
+4117
No files matched your search
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Menu Flow Test",
|
||||
"description": "Integration test mod for the front-end: boots only the menu collections, checks the Play/Continue/New game buttons, the game picker cards, the per-game save browser and the composition comparison. Activated only by --itest=menu_flow (with --frontend).",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": [
|
||||
"mod_manager>=1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.BaseMenu;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Games;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.MenuFlowTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Front-end walk-through, one step per tick so deferred UI work settles:
|
||||
/// only the menu mods are loaded; the menu offers Play (no New game
|
||||
/// without a game); Play opens a card per game; the save browser lists a
|
||||
/// game's save; a player's own game can be deleted from the collection
|
||||
/// editor (a built-in one cannot) while its mods stay; the composition of
|
||||
/// a game is computed without loading it; Continue sees the last session. Prints <c>[TEST_RESULT] name=menu_flow</c>.
|
||||
/// </summary>
|
||||
public partial class MenuFlowTestDriverNode : Node
|
||||
{
|
||||
private const string GameId = "card_novel";
|
||||
private const string Slot = "slot_942";
|
||||
private const string DeletableId = "menu_flow_deletable";
|
||||
private const float TimeoutSeconds = 90f;
|
||||
|
||||
private readonly List<string> failures = new();
|
||||
private int passed;
|
||||
private int step;
|
||||
private double elapsed;
|
||||
private double stepAt;
|
||||
private bool done;
|
||||
private Node hub;
|
||||
|
||||
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 Control Menu => GetTree().CurrentScene as Control;
|
||||
|
||||
private void RunStep()
|
||||
{
|
||||
switch (step)
|
||||
{
|
||||
case 0:
|
||||
if (!Boot.IsReady)
|
||||
return;
|
||||
// First launch shows the launcher screen; go on like a player.
|
||||
if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay)
|
||||
{
|
||||
launcherPlay.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
return;
|
||||
}
|
||||
if (GetTree().CurrentScene is not MainMenu)
|
||||
return;
|
||||
Check("front-end only", ActiveGame.IsFrontendOnly && !ActiveGame.IsActive);
|
||||
Check("menu mods loaded", ModRegistry.Instance.Mods.ContainsKey("base_menu") && ModRegistry.Instance.Mods.ContainsKey("mod_manager"));
|
||||
Check("game content not loaded", !ModRegistry.Instance.Mods.ContainsKey("visual_novel_core"));
|
||||
Check("Play button shown", Menu.FindChild("play", true, false) is Button);
|
||||
Check("New game hidden without a game", Menu.FindChild("new_game", true, false) == null);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
((Button)Menu.FindChild("play", true, false)).EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
var picker = Menu.GetNodeOrNull<GamePickerPanel>("GamePickerPanel");
|
||||
Check("Play opens the game picker", picker?.Visible == true);
|
||||
var games = GameCatalog.Games();
|
||||
var newGameText = TranslationServer.Translate("menu/play/new_game").ToString();
|
||||
var startButtons = Buttons(picker).Where(b => b.Text == newGameText).ToList();
|
||||
Check($"one card per game ({games.Count})", games.Count >= 2 && startButtons.Count == games.Count);
|
||||
var ready = games.Count(g => g.IsReady);
|
||||
Check("ready games can start", startButtons.Count(b => !b.Disabled) == ready && ready > 0);
|
||||
var configureText = TranslationServer.Translate("menu/play/configure").ToString();
|
||||
var configure = Buttons(picker).FirstOrDefault(b => b.Text == configureText);
|
||||
Check("cards offer Configure", configure != null);
|
||||
configure?.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
}
|
||||
|
||||
case 3:
|
||||
{
|
||||
hub = Menu.FindChildren("*", "", true, false).FirstOrDefault(n => n.HasMethod("OpenCollectionEditor"));
|
||||
Check("Configure opens the mod manager", hub != null);
|
||||
var duplicateText = TranslationServer.Translate("mod_manager/collection_duplicate").ToString();
|
||||
Check("editor opened with Duplicate", hub != null && Buttons(hub).Any(b => b.Text == duplicateText && b.IsVisibleInTree()));
|
||||
Check("a built-in game offers no Delete", DeleteButton() == null);
|
||||
|
||||
var user = ModCollectionStore.LoadUser();
|
||||
user.Add(new ModCollection { Id = DeletableId, Name = "Deletable", Kind = ModCollectionKind.Game });
|
||||
ModCollectionStore.SaveUser(user);
|
||||
hub?.Call("OpenCollectionEditor", DeletableId);
|
||||
break;
|
||||
}
|
||||
|
||||
case 4:
|
||||
{
|
||||
var delete = DeleteButton();
|
||||
Check("a player's own game offers Delete", delete != null);
|
||||
delete?.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
}
|
||||
|
||||
case 5:
|
||||
{
|
||||
var dialog = hub?.FindChildren("*", "ConfirmationDialog", true, false).OfType<ConfirmationDialog>().FirstOrDefault(d => d.Visible);
|
||||
var expected = string.Format(TranslationServer.Translate("mod_manager/collection_delete_confirm"), "Deletable", 0);
|
||||
Check("Delete warns that the mods stay", dialog?.DialogText == expected);
|
||||
dialog?.EmitSignal(AcceptDialog.SignalName.Confirmed);
|
||||
Check("the collection is deleted", ModCollectionStore.LoadAll().All(c => c.Id != DeletableId));
|
||||
Check("its mods stay in the system", ModRegistry.Instance.AllDiscoveredMods.ContainsKey("card_battle"));
|
||||
hub?.QueueFree();
|
||||
break;
|
||||
}
|
||||
|
||||
case 6:
|
||||
WriteFakeSave();
|
||||
Menu.GetNode<SaveLoadMenu>("SaveLoadPanel").OpenForGame(GameId);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
{
|
||||
var browser = Menu.GetNode<SaveLoadMenu>("SaveLoadPanel");
|
||||
var slots = browser.FindChildren("*", "", true, false).OfType<SaveSlotItem>().ToList();
|
||||
Check("save browser lists the game's save", slots.Any(s => s.Model?.FileName == Slot));
|
||||
Check("save list carries the game id", SaveService.ListSaves(GameId).Any(s => s.FileName == Slot && s.GameId == GameId));
|
||||
browser.Visible = false;
|
||||
|
||||
var composition = GameCatalog.Composition(GameId);
|
||||
Check("composition computed without loading the game", composition?.Any(m => m.Id == "card_battle") == true);
|
||||
var diff = SaveCompositionDiff.Compute(
|
||||
new[] { new SaveGameMod { Id = "visual_novel_core", Version = "2.0.0" }, new SaveGameMod { Id = "ghost_mechanic", Version = "1.0.0" } },
|
||||
composition);
|
||||
Check("comparison reports removed and added mechanics", diff.Removed.Contains("ghost_mechanic") && diff.Added.Contains("card_battle"));
|
||||
|
||||
LastSessionStore.Record(GameId, Slot);
|
||||
Check("Continue sees the last session", LastSessionStore.Load()?.GameId == GameId);
|
||||
Cleanup();
|
||||
Finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
step++;
|
||||
}
|
||||
|
||||
private Button DeleteButton()
|
||||
{
|
||||
var text = TranslationServer.Translate("mod_manager/collection_delete").ToString();
|
||||
return Buttons(hub).FirstOrDefault(b => b.Text == text && b.IsVisibleInTree());
|
||||
}
|
||||
|
||||
private static IEnumerable<Button> Buttons(Node root)
|
||||
=> root == null ? Enumerable.Empty<Button>() : root.FindChildren("*", "Button", true, false).OfType<Button>();
|
||||
|
||||
private static void WriteFakeSave()
|
||||
{
|
||||
var package = new SavePackage
|
||||
{
|
||||
SaveId = Slot,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
CoreVersion = "0.1.0",
|
||||
Game = new SaveGameInfo { Id = GameId, Name = "Card Novel", Version = "1.0.0" },
|
||||
};
|
||||
package.ModData[SavePackage.CoreSlotKey] = new ModSlot();
|
||||
var dir = ProjectSettings.GlobalizePath(SaveService.GameDirectory(GameId));
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(Path.Combine(dir, Slot + ".json"), JsonSerializer.Serialize(package, JsonOptionsProvider.WriteCompactOptions));
|
||||
}
|
||||
|
||||
private static void Cleanup()
|
||||
{
|
||||
var save = Path.Combine(ProjectSettings.GlobalizePath(SaveService.GameDirectory(GameId)), Slot + ".json");
|
||||
if (File.Exists(save))
|
||||
File.Delete(save);
|
||||
ModCollectionStore.RemoveUser(DeletableId);
|
||||
var last = ProjectSettings.GlobalizePath("user://last_session.json");
|
||||
if (File.Exists(last))
|
||||
File.Delete(last);
|
||||
}
|
||||
|
||||
private void Check(string name, bool ok)
|
||||
{
|
||||
if (ok)
|
||||
{
|
||||
passed++;
|
||||
GameLogger.Log("TEST", $"MenuFlowTest: [OK] {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.Add(name);
|
||||
GameLogger.Log("TEST", $"MenuFlowTest: [FAIL] {name}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
done = true;
|
||||
foreach (var failure in failures)
|
||||
GameLogger.Log("TEST", $"MenuFlowTest: failed: {failure}");
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=menu_flow passed={passed} failed={failures.Count}");
|
||||
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.MenuFlowTest
|
||||
{
|
||||
/// <summary>Queues <see cref="MenuFlowTestDriverNode"/> only for <c>--itest=menu_flow</c>.</summary>
|
||||
public class MenuFlowTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool queued;
|
||||
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=menu_flow"))
|
||||
return;
|
||||
|
||||
queued = true;
|
||||
GameLogger.Log("TEST", "MenuFlowTest: activating");
|
||||
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new MenuFlowTestDriverNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user