Turn editor_tools into a package of the split tool mods
The code moved to tools_base, scenario_tools_base and the seven editor mods; editor_tools depends on them and keeps the tool windows suite. Docs list all seven tool box entries with a new tool box screenshot.
This commit is contained in:
1 parent
152096337b
commit
f5cf990d72
122 files changed
+149
-16268
No files matched your search
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "In-game Play-test Test",
|
||||
"description": "Integration test of the scenario editor opened inside a running game (the novel template with the editors added): plays a scenario from the editor and checks the play-test stays in the game until Stop, then Stop returns to the game with the editor visible — never to the main menu. Activated by --itest=ingame_play.",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": [
|
||||
"my_novel_game>=1"
|
||||
]
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Bootstrap;
|
||||
using Cthangover.Core.Games;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI.Tool.ScenarioEditor;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Mods.Interface.Tool;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Tests.InGamePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// The player's path that used to throw them out to the main menu: inside
|
||||
/// a running game (its dialog on screen) open the scenario editor, play a
|
||||
/// scenario, and expect the play-test to keep running — the teardown of the
|
||||
/// game's own dialog must not count as the end of the played scenario.
|
||||
/// Stop, and the played scenario leaving its scene (an option's
|
||||
/// <c>switch_scene</c>), must both come back into the game with the editor
|
||||
/// visible. Ends with
|
||||
/// <c>[TEST_RESULT] name=ingame_play passed=N failed=M</c> and the exit code.
|
||||
/// </summary>
|
||||
public partial class InGamePlayTestDriverNode : Node
|
||||
{
|
||||
private const string ResultName = "ingame_play";
|
||||
private const string GameModId = "my_novel_game";
|
||||
private const string ScenarioPath = "scenarios/my_novel_home.scenario";
|
||||
private const float TickSeconds = 0.5f;
|
||||
private const float TimeoutSeconds = 90f;
|
||||
private const int GameSettleTicks = 6;
|
||||
private const int PlaySettleTicks = 6;
|
||||
|
||||
private int _passed;
|
||||
private int _failed;
|
||||
private int _step;
|
||||
private int _settle;
|
||||
private double _elapsed;
|
||||
private bool _finished;
|
||||
private ScenarioEditorWindow _window;
|
||||
|
||||
/// <summary>Creates the driver ticking on its own timer.</summary>
|
||||
public InGamePlayTestDriverNode()
|
||||
{
|
||||
var timer = new Timer { WaitTime = TickSeconds, Autostart = true };
|
||||
AddChild(timer);
|
||||
timer.Timeout += Tick;
|
||||
}
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
_elapsed += TickSeconds;
|
||||
if (_elapsed >= TimeoutSeconds)
|
||||
{
|
||||
Check(false, $"timeout ({TimeoutSeconds:0}s) at step {_step}");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
var tree = GetTree();
|
||||
var scenes = tree.Root.GetNodeOrNull<GodotSceneService>("GodotSceneService");
|
||||
switch (_step)
|
||||
{
|
||||
case 0:
|
||||
if (!Boot.IsReady || !ActiveGame.IsActive || InMainMenu(scenes) || GodotSceneService.IsTransitioning)
|
||||
return;
|
||||
if (++_settle < GameSettleTicks)
|
||||
return;
|
||||
Check(SceneManager.GetCurrentSceneName() == "my_novel_town", $"game entered its town (scene '{SceneManager.GetCurrentSceneName()}')");
|
||||
_window = (ScenarioEditorWindow)ToolWindow.ShowWindow(new ScenarioEditorWindow());
|
||||
var tabs = _window.FindChildren("*", "", true, false).OfType<ScenarioEditTabs>().FirstOrDefault();
|
||||
tabs?.OpenFile(ScenarioFileService.MakeKey(GameModId, ScenarioPath));
|
||||
Check(tabs?.HasActiveFile == true, "scenario editor opened the home scenario");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
PressButton(_window, "tools/scenario_editor/play");
|
||||
_settle = 0;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (GodotSceneService.IsTransitioning)
|
||||
return;
|
||||
if (InMainMenu(scenes))
|
||||
{
|
||||
Check(false, "play-test threw the player out to the main menu");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (++_settle < PlaySettleTicks)
|
||||
return;
|
||||
Check(SceneManager.IsTestPlayActive, "play-test is still running while its dialog waits for the player");
|
||||
Check(!_window.Visible, "the editor is hidden during the play-test");
|
||||
Check(SceneManager.GetCurrentSceneName() == "my_novel_home", $"play-test entered the scenario's scene (scene '{SceneManager.GetCurrentSceneName()}')");
|
||||
PressButton(tree.Root, "tools/scenario_editor/stop");
|
||||
_settle = 0;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks)
|
||||
return;
|
||||
Check(!InMainMenu(scenes), "Stop returns into the game, not to the main menu");
|
||||
Check(IsInstanceValid(_window) && _window.Visible, "the editor is visible again after Stop");
|
||||
Check(!SceneManager.IsTestPlayActive, "Stop ends the play-test");
|
||||
Check(SceneManager.GetCurrentSceneName() == "my_novel_town", $"Stop returns to the scene the player was in (scene '{SceneManager.GetCurrentSceneName()}')");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
PressButton(_window, "tools/scenario_editor/play");
|
||||
_settle = 0;
|
||||
break;
|
||||
|
||||
case 5:
|
||||
if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks)
|
||||
return;
|
||||
// The played scenario's option "Back to the town" does exactly this.
|
||||
tree.Root.GetNodeOrNull<SceneManager>("SceneManager")?.SwitchScene("my_novel_town");
|
||||
_settle = 0;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks)
|
||||
return;
|
||||
Check(!InMainMenu(scenes), "a scene switch in the played scenario returns into the game, not to the main menu");
|
||||
Check(!SceneManager.IsTestPlayActive, "leaving the played scene ends the play-test");
|
||||
Check(IsInstanceValid(_window) && _window.Visible, "the editor is visible again after the played scenario left its scene");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
_step++;
|
||||
}
|
||||
|
||||
private static bool InMainMenu(GodotSceneService scenes) => scenes?.CurrentGodotSceneName == "mainmenu";
|
||||
|
||||
private static void PressButton(Node root, string textKey)
|
||||
{
|
||||
var text = ToolUi.Tr(textKey);
|
||||
var button = root.FindChildren("*", nameof(Button), true, false).OfType<Button>().FirstOrDefault(b => b.Text == text);
|
||||
button?.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
}
|
||||
|
||||
private void Check(bool condition, string label)
|
||||
{
|
||||
if (condition)
|
||||
_passed++;
|
||||
else
|
||||
_failed++;
|
||||
GameLogger.Log("TEST", $"InGamePlayTest: [{(condition ? "PASS" : "FAIL")}] {label}");
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
_finished = true;
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name={ResultName} passed={_passed} failed={_failed}");
|
||||
GetTree().Quit(_failed == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Tests.InGamePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry point of the in-game play-test GUI test, activated only by the
|
||||
/// exact user arg <c>--itest=ingame_play</c>. The driver is added deferred:
|
||||
/// resources-ready fires before the game's shell exists.
|
||||
/// </summary>
|
||||
public class InGamePlayTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool driverQueued;
|
||||
|
||||
/// <summary>Nothing to do per mod: the driver is queued on resources-ready.</summary>
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
/// <summary>Queues the driver once when the activation flag is present.</summary>
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
if (driverQueued || !OS.GetCmdlineUserArgs().Any(a => a == "--itest=ingame_play"))
|
||||
return;
|
||||
|
||||
driverQueued = true;
|
||||
GameLogger.Log("TEST", "InGamePlayTestInitializer: activating in-game play-test test");
|
||||
ModRoot.AddDeferred(new InGamePlayTestDriverNode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,7 +354,7 @@ namespace Cthangover.Tools.Tests.NovelFlow
|
||||
DeleteParkFiles();
|
||||
SceneManager.ReloadContent();
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=novel_flow passed={passed} failed={failures.Count}");
|
||||
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||
Cthangover.Core.Bootstrap.ApplicationExit.Quit(GetTree(), failures.Count == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": [
|
||||
"cthangover_game>=1"
|
||||
"cthangover_game>=1",
|
||||
"rpg_tileset_editor>=1",
|
||||
"village_pack>=1",
|
||||
"test_defaults_and_patches>=1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
namespace Cthangover.Tools.Tests.Windows
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Cthangover.Mods.Rpg;
|
||||
using Cthangover.Tools.Common;
|
||||
using Cthangover.Tools.RpgTilesetEditor;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
/// <summary>Edits a temporary tileset through the real window and verifies the saved and published data.</summary>
|
||||
public static class ToolWindowsAtlasExercise
|
||||
{
|
||||
private const string Mod = "test_defaults_and_patches";
|
||||
private const string FileName = "tilesets/tools_split_atlas.json";
|
||||
private const string TilesetId = "tools_split/atlas";
|
||||
private const string Fixture = "{\"Kind\":\"rpg_tileset\",\"Items\":[{\"Id\":\"tools_split/atlas\",\"Image\":\"village/objects\",\"SurfaceImage\":\"village/surfaces\",\"SurfaceSize\":128,\"TileSize\":32,\"Columns\":12,\"Tiles\":[{\"Index\":100,\"AtlasIndex\":0}],\"VisualGroups\":[]}]}";
|
||||
|
||||
/// <summary>Selection, object anchors, collision, surfaces, undo/redo and Save use public controls only.</summary>
|
||||
public static void Run(Window window, Action<bool, string> check)
|
||||
{
|
||||
var original = ModFileService.ReadText(Mod, FileName);
|
||||
var filePath = ModFileService.GetWritablePath(Mod, FileName);
|
||||
var folderExisted = Directory.Exists(Path.GetDirectoryName(filePath));
|
||||
try
|
||||
{
|
||||
check(ModFileService.WriteText(Mod, FileName, Fixture), "atlas: temporary fixture is writable");
|
||||
var selector = window.FindChildren("*", "", true, false).OfType<ModFileSelector>().Single();
|
||||
selector.Refresh();
|
||||
var mods = selector.GetChildren().OfType<OptionButton>().First();
|
||||
var at = Enumerable.Range(0, mods.ItemCount).First(i => mods.GetItemText(i) == Mod);
|
||||
mods.Select(at);
|
||||
mods.EmitSignal(OptionButton.SignalName.ItemSelected, (long)at);
|
||||
check(selector.CurrentMod == Mod && selector.CurrentPath == FileName, "atlas: selected the test file");
|
||||
var view = window.FindChildren("*", "", true, false).OfType<RpgTilesetAtlasView>().Single();
|
||||
check(view.GridSize.X > 1, "atlas: image loaded");
|
||||
view.Select(new Rect2I(0, 0, 2, 1));
|
||||
Press(window, "tools/rpg_tileset_editor/make_tiles");
|
||||
var solid = window.FindChildren("*", "", true, false).OfType<CheckBox>().First();
|
||||
solid.ButtonPressed = true;
|
||||
Press(window, "tools/rpg_tileset_editor/make_object");
|
||||
var anchors = window.FindChildren("*", "", true, false).OfType<SpinBox>().ToArray();
|
||||
anchors[0].Value = 1.25;
|
||||
anchors[1].Value = 1.5;
|
||||
Press(window, "tools/rpg_tileset_editor/undo");
|
||||
check(anchors[1].Value == 1, "atlas: undo restores the object anchor");
|
||||
Press(window, "tools/rpg_tileset_editor/redo");
|
||||
check(anchors[1].Value == 1.5, "atlas: redo restores the edited anchor");
|
||||
var tabs = window.FindChildren("*", "", true, false).OfType<TabContainer>().Single();
|
||||
tabs.CurrentTab = 1;
|
||||
Press(window, "tools/rpg_tileset_editor/add_surface");
|
||||
Press(window, "tools/common/save");
|
||||
var saved = RpgTilesetDocument.Load(ModFileService.ReadText(Mod, FileName));
|
||||
check(saved.Tileset.VisualGroups.Count == 1 && saved.Tileset.VisualGroups[0].Pattern.Length == 2, "atlas: object and logical tile ids saved");
|
||||
check(saved.Tileset.VisualGroups[0].AnchorX == 1.25 && saved.Tileset.VisualGroups[0].AnchorY == 1.5, "atlas: anchors saved");
|
||||
check(saved.Tileset.TileOf(100).Solid, "atlas: collision saved");
|
||||
check(saved.Tileset.Tiles.Any(t => t.Surface == 0), "atlas: surface saved");
|
||||
var published = RpgTilesetFactory.Instance.Get(TilesetId);
|
||||
check(published.VisualGroups.Count == 1 && published.VisualGroups[0].AnchorY == 1.5, "atlas: runtime cache sees saved changes");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (original != null)
|
||||
ModFileService.WriteText(Mod, FileName, original);
|
||||
else if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
var folder = Path.GetDirectoryName(filePath);
|
||||
if (!folderExisted && Directory.Exists(folder) && !Directory.EnumerateFileSystemEntries(folder).Any())
|
||||
Directory.Delete(folder);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Press(Window window, string key)
|
||||
=> window.FindChildren("*", "", true, false).OfType<Button>().First(b => b.Text == TranslationServer.Translate(key)).EmitSignal(BaseButton.SignalName.Pressed);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ namespace Cthangover.Tools.Tests.Windows
|
||||
case "map_editor":
|
||||
MapDrawing(window, check);
|
||||
break;
|
||||
case "rpg_tileset_editor":
|
||||
ToolWindowsAtlasExercise.Run(window, check);
|
||||
break;
|
||||
case "interactive_editor":
|
||||
InteractivePolygon(window, check);
|
||||
break;
|
||||
|
||||
@@ -68,6 +68,11 @@ namespace Cthangover.Tools.Tests.Windows
|
||||
|
||||
_tools.AddRange(ToolFactory.Instance.GetAll());
|
||||
Check(_tools.Count > 0, $"tools registered ({_tools.Count})");
|
||||
var toolbarTools = new HashSet<string>();
|
||||
foreach (var button in ToolBoxButtonFactory.Instance.GetVisible())
|
||||
toolbarTools.Add(button.ToolId);
|
||||
foreach (var tool in _tools)
|
||||
Check(toolbarTools.Contains(tool.Id), $"{tool.Id}: reachable from the in-game toolbar");
|
||||
OpenNext();
|
||||
return;
|
||||
}
|
||||
@@ -176,7 +181,7 @@ namespace Cthangover.Tools.Tests.Windows
|
||||
_finished = true;
|
||||
OS.RemoveLogger(_errors);
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name={ResultName} passed={_passed} failed={_failed}");
|
||||
GetTree().Quit(_failed == 0 ? 0 : 1);
|
||||
Cthangover.Core.Bootstrap.ApplicationExit.Quit(GetTree(), _failed == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
private static void Log(string message) => GameLogger.Log("TEST", message);
|
||||
|
||||
Reference in new issue
Block a user