init mod
This commit is contained in:
commit
ee6fdc3576
32 files changed
+4117
No files matched your search
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Save Dormancy Test",
|
||||
"description": "Integration test mod: inside a game collection it saves, injects data of a mod the game runs without, loads, re-saves and purges — verifying per-game save folders, dormant slots carried forward, the joined-game hook and the purge action. Activated only by --itest=save_dormancy.",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": []
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
|
||||
namespace Mods.SaveDormancyTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Saveable of the test mod: records whether a load restored its slot or
|
||||
/// told it that it joined an existing game without data.
|
||||
/// </summary>
|
||||
public class SaveDormancyProbe : IModSaveable
|
||||
{
|
||||
public static bool Joined { get; set; }
|
||||
|
||||
public static bool Restored { get; set; }
|
||||
|
||||
public void OnSave(ModSlot slot)
|
||||
{
|
||||
slot.Data["probe"] = new DataBlob();
|
||||
}
|
||||
|
||||
public bool OnLoad(ModSlot slot, out string errorMessage)
|
||||
{
|
||||
Restored = true;
|
||||
errorMessage = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnJoinedExistingGame()
|
||||
{
|
||||
Joined = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Games;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.SaveDormancyTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the composition-change round trip once the game is running and
|
||||
/// prints <c>[TEST_RESULT] name=save_dormancy</c>: a save of the active
|
||||
/// game lands in its own folder; data of a mod the game runs without
|
||||
/// survives a load and the next save as a dormant slot; a mod without data
|
||||
/// in the save gets <c>OnJoinedExistingGame</c>; the purge drops dormant data.
|
||||
/// </summary>
|
||||
public partial class SaveDormancyTestDriverNode : Node
|
||||
{
|
||||
private const string SlotA = "itest_dormancy_a";
|
||||
private const string SlotB = "itest_dormancy_b";
|
||||
private const string AbsentMod = "recruiting";
|
||||
private const float TimeoutSeconds = 90f;
|
||||
|
||||
private readonly List<string> failures = new();
|
||||
private int passed;
|
||||
private double elapsed;
|
||||
private bool done;
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
|
||||
elapsed += delta;
|
||||
if (elapsed > TimeoutSeconds)
|
||||
{
|
||||
failures.Add("timeout waiting for the game");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Boot.IsReady || GameData.Instance?.Runtime == null || GetTree().CurrentScene == null || elapsed < 3)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add($"unhandled {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
Finish();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
Check("a game collection is active", ActiveGame.IsActive);
|
||||
Check($"'{AbsentMod}' is not part of the game", !ActiveGame.ModIds.Contains(AbsentMod));
|
||||
|
||||
SaveService.Save(SlotA);
|
||||
var pathA = FilePath(SlotA);
|
||||
Check("save lands in the game's folder", pathA.Contains($"saves{Path.DirectorySeparatorChar}{ActiveGame.Id}", StringComparison.Ordinal) || pathA.Contains($"saves/{ActiveGame.Id}", StringComparison.Ordinal));
|
||||
var package = Read(pathA);
|
||||
Check("save records the game id", package?.Game?.Id == ActiveGame.Id);
|
||||
Check("save records the composition", package?.Game?.Mods?.Count > 0);
|
||||
|
||||
// Simulate a save written while the game still had the mechanic, and
|
||||
// before this test mod joined the game.
|
||||
var absentSlot = new ModSlot { Metadata = new ModMetadata { ModId = AbsentMod, ModVersion = "1.1.0" } };
|
||||
absentSlot.Data["marker"] = new DataBlob(1, new Dictionary<string, string> { ["value"] = "42" });
|
||||
package.ModData[AbsentMod] = absentSlot;
|
||||
package.ModData.Remove("base_menu__save_dormancy");
|
||||
File.WriteAllText(pathA, JsonSerializer.Serialize(package, JsonOptionsProvider.WriteCompactOptions));
|
||||
|
||||
SaveDormancyProbe.Joined = false;
|
||||
SaveDormancyProbe.Restored = false;
|
||||
Check("load succeeds despite a missing mod", SaveService.Load(SlotA));
|
||||
Check("missing mod's data went dormant", SaveService.LastLoadDormancy?.Dormant.ContainsKey(AbsentMod) == true);
|
||||
Check("mod without data joined the game", SaveDormancyProbe.Joined && !SaveDormancyProbe.Restored);
|
||||
|
||||
SaveService.Save(SlotB);
|
||||
var resaved = Read(FilePath(SlotB));
|
||||
var carried = resaved?.Dormant != null && resaved.Dormant.TryGetValue(AbsentMod, out var slot) ? slot : null;
|
||||
Check("dormant slot carried into the next save", carried?.Metadata?.Dormant == true);
|
||||
Check("dormant data unchanged", carried?.Data != null && carried.Data.TryGetValue("marker", out var marker)
|
||||
&& marker.ReadJson<Dictionary<string, string>>().GetValueOrDefault("value") == "42");
|
||||
Check("live mods still saved", resaved?.ModData?.ContainsKey(SavePackage.CoreSlotKey) == true);
|
||||
|
||||
var message = ModSettingsRegistry.RunAction("base_menu", "purge_dormant");
|
||||
Check("purge action runs from the save settings", !string.IsNullOrEmpty(message));
|
||||
Check("purge removed dormant data from the save", Read(FilePath(SlotB))?.Dormant == null);
|
||||
|
||||
SaveService.DeleteSave(SlotA);
|
||||
SaveService.DeleteSave(SlotB);
|
||||
}
|
||||
|
||||
private static string FilePath(string slot)
|
||||
=> ProjectSettings.GlobalizePath($"{SaveService.SaveDirectory}/{slot}.json");
|
||||
|
||||
private static SavePackage Read(string path)
|
||||
=> File.Exists(path) ? JsonSerializer.Deserialize<SavePackage>(File.ReadAllText(path), JsonOptionsProvider.ReadOptions) : null;
|
||||
|
||||
private void Check(string name, bool ok)
|
||||
{
|
||||
if (ok)
|
||||
{
|
||||
passed++;
|
||||
GameLogger.Log("TEST", $"SaveDormancyTest: [OK] {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.Add(name);
|
||||
GameLogger.Log("TEST", $"SaveDormancyTest: [FAIL] {name}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
done = true;
|
||||
foreach (var failure in failures)
|
||||
GameLogger.Log("TEST", $"SaveDormancyTest: failed: {failure}");
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=save_dormancy 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.SaveDormancyTest
|
||||
{
|
||||
/// <summary>Queues <see cref="SaveDormancyTestDriverNode"/> only for <c>--itest=save_dormancy</c>.</summary>
|
||||
public class SaveDormancyTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool queued;
|
||||
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
if (queued || !OS.GetCmdlineUserArgs().Contains("--itest=save_dormancy"))
|
||||
return;
|
||||
|
||||
queued = true;
|
||||
GameLogger.Log("TEST", "SaveDormancyTest: activating");
|
||||
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new SaveDormancyTestDriverNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user