using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Cthangover.Core.Bootstrap;
using Cthangover.Core.Events;
using Cthangover.Core.Games;
using Cthangover.Core.Mods;
using Cthangover.Core.Mods.HotReload;
using Cthangover.Core.Scenes;
using Cthangover.Core.Utils;
using Godot;
namespace Mods.CollectionEditTest
{
///
/// The player edits the collection of the game that is running, in ONE process:
///
/// The novel template is entered; it matches its collection and the
/// editors are not loaded.
/// The collection gets the developer toolset (dev_tools: editors,
/// the scenario studio, …), saved the way the collection editor saves it (a
/// user package with the same id): the running game is now outdated.
/// Menu → Play → New game of the same game switches the mod set in
/// place: the editors and the studio are loaded, the studio's button is on
/// the game's toolbar and the game matches its collection again.
///
/// A player's own package of the game is moved aside for the run and put
/// back afterwards. Prints [TEST_RESULT] name=collection_edit.
///
public partial class CollectionEditTestDriverNode : Node
{
public const string Flag = "--itest=collection_edit";
private const string GameId = "novel_template";
private const string AddedToolset = "dev_tools";
private const string AddedMod = "editor_tools";
private const string StudioMod = "scenario_studio";
private const float TimeoutSeconds = 240f;
private readonly List failures = new();
private readonly List enteredScenes = new();
private readonly List<(string From, string To)> movedAside = new();
private int passed;
private int step;
private double elapsed;
private double stepAt;
private bool done;
public override void _Ready()
{
// The driver must keep ticking across the mod-set switch.
ProcessMode = ProcessModeEnum.Always;
ModEventBus.Subscribe(OnSceneEntered);
}
public override void _ExitTree()
{
ModEventBus.Unsubscribe(OnSceneEntered);
}
public override void _Process(double delta)
{
if (done)
return;
elapsed += delta;
if (elapsed > TimeoutSeconds)
{
failures.Add($"timeout at step {step} (scene='{SceneManager.GetCurrentSceneName()}', game='{ActiveGame.Id}')");
Finish();
return;
}
if (elapsed - stepAt < 0.25)
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 static bool Busy => GameSession.IsSwitching || ModManager.Instance.IsReloading || ModHotReloadService.IsReloadRunning;
private static bool EditorsLoaded => ModRegistry.Instance.Mods.ContainsKey(AddedMod);
/// Scenes of the game entered since the last clear; the main menu does not count.
private void OnSceneEntered(SceneEnteredEvent e)
{
if (!string.Equals(e.SceneName, "mainmenu", StringComparison.OrdinalIgnoreCase))
enteredScenes.Add(e.SceneName);
}
private void RunStep()
{
switch (step)
{
case 0:
if (!Boot.IsReady || Busy || ActiveGame.Id != GameId || enteredScenes.Count == 0)
return;
Check($"game '{GameId}' entered ('{enteredScenes[0]}')", true);
Check("a freshly entered game matches its collection", !GameCatalog.IsActiveOutdated());
Check($"'{AddedMod}' is not loaded before the edit", !EditorsLoaded);
AddEditorsToCollection();
Check("the edited collection makes the running game outdated", GameCatalog.IsActiveOutdated());
GetTree().Root.GetNode("GodotSceneService").SwitchToMenu();
break;
case 1:
if (Busy || !PressMenuPlay())
return;
enteredScenes.Clear();
break;
case 2:
if (!StartFromPicker())
return;
break;
case 3:
{
if (Busy || ActiveGame.Id != GameId || enteredScenes.Count == 0)
return;
var reload = ModManager.Instance.LastReload;
Check("New game after the edit switched the mod set", reload?.Applied == true && reload.Plan?.Load.Contains(AddedMod) == true);
Check($"'{AddedMod}' is loaded after the edit", EditorsLoaded);
Check($"'{StudioMod}' is loaded after the edit", ModRegistry.Instance.Mods.ContainsKey(StudioMod));
Check("the studio's button is on the game's toolbar", HasToolbarButton(TranslationServer.Translate("studio/title").ToString()));
Check("the game matches its edited collection", !GameCatalog.IsActiveOutdated());
Check($"entered a scene of '{GameId}' ('{enteredScenes[0]}')", true);
Finish();
return;
}
}
step++;
}
///
/// Saves the game's collection with the developer toolset included, as a
/// user package — what the collection editor does when the player adds it.
///
private void AddEditorsToCollection()
{
MoveUserPackageAside();
var source = GameCatalog.LoadCollections()[GameId];
var edited = JsonSerializer.Deserialize(JsonSerializer.Serialize(source));
edited.Package = source.Package;
edited.Includes ??= new List();
edited.Includes.Add(new ModCollectionInclude { Ref = AddedToolset + ">=1", Role = "tools" });
var user = ModCollectionStore.LoadUser().Where(c => !string.Equals(c.Id, GameId, StringComparison.OrdinalIgnoreCase)).ToList();
user.Add(edited);
ModCollectionStore.SaveUser(user);
}
private void MoveUserPackageAside()
{
var root = ModCollectionStore.UserPath;
foreach (var path in new[] { Path.Combine(root, GameId), Path.Combine(root, GameId + ".zip") })
{
var aside = Path.Combine(Path.GetDirectoryName(root) ?? root, $"collection_edit_backup_{Path.GetFileName(path)}");
if (Directory.Exists(path))
Directory.Move(path, aside);
else if (File.Exists(path))
File.Move(path, aside);
else
continue;
movedAside.Add((path, aside));
}
}
private void Cleanup()
{
ModCollectionStore.RemoveUser(GameId);
foreach (var (from, aside) in movedAside)
{
if (Directory.Exists(aside))
Directory.Move(aside, from);
else if (File.Exists(aside))
File.Move(aside, from);
}
}
private bool HasToolbarButton(string tooltip)
=> GetTree().Root.FindChildren("*", "Button", true, false).OfType