From 972df5ccff9f37f1dc052593e3c53db5b890a583 Mon Sep 17 00:00:00 2001 From: iisaev Date: Thu, 22 Oct 2026 17:09:38 +0200 Subject: [PATCH] Scene editor for logical scenes; scenario editor Save refreshes the running game; novel_flow itest; mod cover --- locale/ui_en.properties | 57 ++ locale/ui_ru.properties | 57 ++ manifest.json | 5 +- mod.jpg | Bin 0 -> 83550 bytes src/ScenarioEditor/ScenarioEditorWindow.cs | 27 +- src/SceneEditor/SceneBackgroundTile.cs | 69 ++ src/SceneEditor/SceneEditorToolBoxButton.cs | 17 + src/SceneEditor/SceneEditorToolProvider.cs | 19 + src/SceneEditor/SceneEditorWindow.cs | 743 ++++++++++++++++++ src/Services/ModResourceService.cs | 19 + src/Services/ScenarioFileService.cs | 15 + src/Services/SceneFileEntry.cs | 89 +++ src/Services/SceneFileService.cs | 92 +++ test_suites/novel_flow/manifest.json | 10 + .../novel_flow/src/NovelFlowTestDriver.cs | 359 +++++++++ .../src/NovelFlowTestInitializer.cs | 37 + .../windows/src/ToolWindowsExercises.cs | 80 ++ 17 files changed, 1692 insertions(+), 3 deletions(-) create mode 100644 mod.jpg create mode 100644 src/SceneEditor/SceneBackgroundTile.cs create mode 100644 src/SceneEditor/SceneEditorToolBoxButton.cs create mode 100644 src/SceneEditor/SceneEditorToolProvider.cs create mode 100644 src/SceneEditor/SceneEditorWindow.cs create mode 100644 src/Services/SceneFileEntry.cs create mode 100644 src/Services/SceneFileService.cs create mode 100644 test_suites/novel_flow/manifest.json create mode 100644 test_suites/novel_flow/src/NovelFlowTestDriver.cs create mode 100644 test_suites/novel_flow/src/NovelFlowTestInitializer.cs diff --git a/locale/ui_en.properties b/locale/ui_en.properties index 9ffc073..9d7e8f9 100644 --- a/locale/ui_en.properties +++ b/locale/ui_en.properties @@ -248,3 +248,60 @@ tools/hub/hint = Pick an editor — windows open over this screen. A scenario ca tools/hub/editors = Editors tools/hub/empty = No editors are installed. tools/hub/main_menu = Main menu +tools/scene_editor/title = Scene editor +tools/scene_editor/hint = A scene is a place: backgrounds, ambient sound and scenarios. Ctrl+S saves · double-click a background in the list to add it +tools/scene_editor/save_tip = Save the scene file into the chosen mod (Ctrl+S); the game sees the change at once +tools/scene_editor/new_scene = + New scene +tools/scene_editor/new_scene_tip = Create a new place: name, mod, backgrounds. Scenarios can be added later; a scene may also have none +tools/scene_editor/delete = Delete +tools/scene_editor/delete_tip = Delete the chosen mod's scene file +tools/scene_editor/delete_confirm = Delete this scene's file from the chosen mod? The scene's scenarios stay. +tools/scene_editor/go = ▶ Go to scene +tools/scene_editor/go_tip = Switch the game to this scene +tools/scene_editor/rescan_tip = Re-read scenes, backgrounds and sounds (after adding files to mods) +tools/scene_editor/scenes = Scenes +tools/scene_editor/backgrounds = Scene backgrounds (one is picked at random on entry) +tools/scene_editor/all_backgrounds = All backgrounds +tools/scene_editor/add_background = + Add background to scene +tools/scene_editor/add_background_tip = Add the shown background to the scene (or double-click it in the list) +tools/scene_editor/remove_background = Remove the background from the scene +tools/scene_editor/properties = Properties +tools/scene_editor/name = Name +tools/scene_editor/name_tip = Scene id: latin letters, digits and _; switch_scene and scenario headers refer to it. Set only when creating +tools/scene_editor/mod = Save to +tools/scene_editor/mod_tip = Mod the scene file is written to (folder mods only) +tools/scene_editor/ambient = Sound +tools/scene_editor/ambient_tip = Ambient sound playing in the scene +tools/scene_editor/listen = ▶ +tools/scene_editor/listen_tip = Listen to the ambient sound +tools/scene_editor/stop = ■ +tools/scene_editor/default_scenario = Default scenario +tools/scene_editor/default_scenario_tip = Plays when no other scenario of the scene is left +tools/scene_editor/scenarios = Scenarios of the scene +tools/scene_editor/open_scenario = Open +tools/scene_editor/open_scenario_tip = Open the selected scenario in the scenario editor (or double-click) +tools/scene_editor/new_scenario = + New scenario +tools/scene_editor/new_scenario_tip = Create a scenario of this scene in the chosen mod and open it +tools/scene_editor/none = — none — +tools/scene_editor/effective = In game: {0} (from mod {1}) +tools/scene_editor/adds_to = The scene is defined in: {0}. Saving creates a file in the chosen mod that adds to it. +tools/scene_editor/also_in = Also added to by: {0} +tools/scene_editor/no_writable_mod = No folder mod to save the scene to. +tools/scene_editor/bad_name = Set the scene name: latin letters, digits and _ (starting with a letter). +tools/scene_editor/name_taken = Scene "{0}" already exists; pick it in the list. +tools/scene_editor/need_background = Add at least one background to the scene. +tools/scene_editor/created = Scene "{0}" created. +tools/scene_editor/deleted = Deleted: {0} +tools/scene_editor/opened = Scene: {0} +tools/scene_editor/new_started = New scene: set the name and the mod, add backgrounds. +tools/scene_editor/already_there = Background {0} is already in the scene. +tools/scene_editor/go_failed = The game does not know scene "{0}"; save it first. +tools/scene_editor/went = Switched to scene {0}. +tools/scene_editor/rescanned = Scenes, backgrounds and sounds re-read. +tools/scene_editor/scenario_created = Scenario {0} created. +tools/scene_editor/no_scenarios = The scene has no scenarios yet +tools/scene_editor/no_backgrounds = The scene has no backgrounds: pick one in the list below and add it. +tools/scene_editor/pick_scene = Pick a scene on the left or create a new one. +tools/scene_editor/preview_hint = Pick a background to see it. +tools/scene_editor/not_in_scene = (not in the scene) +tools/scene_editor/new_line_text = A new scene. Replace this line with your text. diff --git a/locale/ui_ru.properties b/locale/ui_ru.properties index 2f255fc..d27f4ae 100644 --- a/locale/ui_ru.properties +++ b/locale/ui_ru.properties @@ -248,3 +248,60 @@ tools/hub/hint = Выберите редактор — окна открываю tools/hub/editors = Редакторы tools/hub/empty = Не установлено ни одного редактора. tools/hub/main_menu = Главное меню +tools/scene_editor/title = Редактор сцен +tools/scene_editor/hint = Сцена — это место: фоны, звук окружения и сценарии. Ctrl+S — сохранить · двойной клик по фону в списке — добавить в сцену +tools/scene_editor/save_tip = Сохранить файл сцены в выбранный мод (Ctrl+S); игра увидит изменения сразу +tools/scene_editor/new_scene = + Новая сцена +tools/scene_editor/new_scene_tip = Создать новое место: имя, мод, фоны. Сценарии можно добавить потом — сцена может обходиться и без них +tools/scene_editor/delete = Удалить +tools/scene_editor/delete_tip = Удалить файл сцены выбранного мода +tools/scene_editor/delete_confirm = Удалить файл этой сцены из выбранного мода? Сценарии сцены останутся на месте. +tools/scene_editor/go = ▶ Перейти в сцену +tools/scene_editor/go_tip = Переключить игру на эту сцену +tools/scene_editor/rescan_tip = Перечитать сцены, фоны и звуки (после добавления файлов в моды) +tools/scene_editor/scenes = Сцены +tools/scene_editor/backgrounds = Фоны сцены (при входе выбирается один случайно) +tools/scene_editor/all_backgrounds = Все фоны +tools/scene_editor/add_background = + Добавить фон в сцену +tools/scene_editor/add_background_tip = Добавить показанный фон в сцену (или двойной клик по нему в списке) +tools/scene_editor/remove_background = Убрать фон из сцены +tools/scene_editor/properties = Свойства +tools/scene_editor/name = Имя +tools/scene_editor/name_tip = Идентификатор сцены: латиница, цифры и _; на него ссылаются switch_scene и заголовки сценариев. Задаётся только при создании +tools/scene_editor/mod = Сохранять в +tools/scene_editor/mod_tip = Мод, в который пишется файл сцены (только моды-папки) +tools/scene_editor/ambient = Звук +tools/scene_editor/ambient_tip = Звук окружения, который играет в сцене +tools/scene_editor/listen = ▶ +tools/scene_editor/listen_tip = Прослушать звук окружения +tools/scene_editor/stop = ■ +tools/scene_editor/default_scenario = Сценарий по умолч. +tools/scene_editor/default_scenario_tip = Играет, когда в сцене не осталось других сценариев +tools/scene_editor/scenarios = Сценарии сцены +tools/scene_editor/open_scenario = Открыть +tools/scene_editor/open_scenario_tip = Открыть выбранный сценарий в редакторе сценариев (или двойной клик) +tools/scene_editor/new_scenario = + Новый сценарий +tools/scene_editor/new_scenario_tip = Создать сценарий этой сцены в выбранном моде и открыть его +tools/scene_editor/none = — нет — +tools/scene_editor/effective = В игре: {0} (из мода {1}) +tools/scene_editor/adds_to = Сцена определена в: {0}. Сохранение создаст в выбранном моде файл, который дополнит её. +tools/scene_editor/also_in = Сцену дополняют также: {0} +tools/scene_editor/no_writable_mod = Нет мода-папки, куда можно сохранить сцену. +tools/scene_editor/bad_name = Задайте имя сцены: латиница, цифры и _ (начинается с буквы). +tools/scene_editor/name_taken = Сцена «{0}» уже есть — выберите её в списке. +tools/scene_editor/need_background = Добавьте в сцену хотя бы один фон. +tools/scene_editor/created = Сцена «{0}» создана. +tools/scene_editor/deleted = Удалено: {0} +tools/scene_editor/opened = Сцена: {0} +tools/scene_editor/new_started = Новая сцена: задайте имя, мод и добавьте фоны. +tools/scene_editor/already_there = Фон {0} уже есть в сцене. +tools/scene_editor/go_failed = Игра не знает сцену «{0}» — сохраните её. +tools/scene_editor/went = Переход в сцену {0}. +tools/scene_editor/rescanned = Сцены, фоны и звуки перечитаны. +tools/scene_editor/scenario_created = Создан сценарий {0}. +tools/scene_editor/no_scenarios = У сцены пока нет сценариев +tools/scene_editor/no_backgrounds = В сцене нет фонов — выберите фон в списке ниже и добавьте его. +tools/scene_editor/pick_scene = Выберите сцену слева или создайте новую. +tools/scene_editor/preview_hint = Выберите фон, чтобы посмотреть его. +tools/scene_editor/not_in_scene = (не в сцене) +tools/scene_editor/new_line_text = Новая сцена. Замените эту строку своим текстом. diff --git a/manifest.json b/manifest.json index 087ce15..955917f 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "Editor Tools", "version": "1.0.0", "author": "ct", - "description": "Developer toolbox of ToolBox windows: scenario graph editor, map editor with zones, scene builder with inspector, interactive and light editors, and the mod catalog export window.", + "description": "Developer toolbox of ToolBox windows: scenario graph editor, scene editor (create and change the places of a novel: backgrounds, ambient sound, default scenario, the scene's scenarios), map editor with zones, scene builder with inspector, interactive and light editors, and the mod catalog export window.", "category": "tools, editor", "sources": [ "src/**/*.cs" @@ -11,6 +11,7 @@ "depends": ["dialog_base>=1", "interactive_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1"], "test_suites": [ {"id": "windows", "activate": ["--itest=tools_windows"]}, - {"id": "ingame_play", "activate": ["--itest=ingame_play"]} + {"id": "ingame_play", "activate": ["--itest=ingame_play"]}, + {"id": "novel_flow", "activate": ["--itest=novel_flow"]} ] } diff --git a/mod.jpg b/mod.jpg new file mode 100644 index 0000000000000000000000000000000000000000..09b914db2f7e1294d27a6f478454bcac08737e23 GIT binary patch literal 83550 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ΝL&>vmCVv>|e^vpyy+l$G|*XLIo! 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 LNfmDBfAjwXoOpens with the graph view so the editor starts on the cross-scene map. + /// Opens with the graph view so the editor starts on the cross-scene map, or on the file asked for by . public override void _Ready() { _editTabs.SetViewToGraph(); + if (_pendingKey != null) + OpenScenario(_pendingKey); + } + + /// + /// Opens a scenario file (key mod:path) in a text tab and selects it in the + /// tree — for other tools, e.g. the scene editor's "open" button. Before the window + /// is in the tree the key waits for , which would otherwise + /// switch the view back to the graph. + /// + public void OpenScenario(string key) + { + if (!IsInsideTree()) + { + _pendingKey = key; + return; + } + + _pendingKey = null; + _fileTreePanel.SelectFile(key); + _editTabs.OpenFile(key); } /// Returns from play-test mode as soon as the in-game test play ends. @@ -222,6 +244,9 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor } _editTabs.SaveCurrentFile(); + // The running game caches scenario headers and texts: drop them, so a new or + // edited scenario plays on the next entry into its scene without a mod reload. + SceneFileService.RefreshGame(); SetStatus(string.Format(ToolUi.Tr("tools/common/saved"), ScenarioFileService.GetDisplayPath(modId, relativePath)), ToolStatusKind.Ok); _infoPanel.Update(text); _thumbPanel.Load(text); diff --git a/src/SceneEditor/SceneBackgroundTile.cs b/src/SceneEditor/SceneBackgroundTile.cs new file mode 100644 index 0000000..3697254 --- /dev/null +++ b/src/SceneEditor/SceneBackgroundTile.cs @@ -0,0 +1,69 @@ +using System; +using Cthangover.Core.UI.Theming; +using Cthangover.Mods.Interface.Theming; +using Cthangover.Tools.Common; +using Cthangover.Tools.Services; +using Godot; + +namespace Cthangover.Tools.SceneEditor +{ + /// + /// One background of the edited scene in : a thumbnail + /// (click shows it in the preview), the background id and the mod whose scene file + /// adds it, and a remove button only for backgrounds of the file being edited — + /// another mod's background can only be hidden by editing that mod. + /// + public partial class SceneBackgroundTile : PanelContainer + { + private readonly string _background; + private readonly Action _picked; + private readonly Action _removed; + + /// Builds the tile; shows the ✕ button, the accent frame. + public SceneBackgroundTile(string background, string modId, bool removable, bool selected, + Action picked, Action removed) + { + _background = background; + _picked = picked; + _removed = removed; + AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Surface1, + selected ? UiTokens.Accent : UiTokens.Line)); + + var column = new VBoxContainer(); + column.AddThemeConstantOverride("separation", UiTokens.Space1); + AddChild(column); + + var thumb = new TextureButton + { + TextureNormal = ModResourceService.LoadBackgroundTexture(background), + IgnoreTextureSize = true, + StretchMode = TextureButton.StretchModeEnum.KeepAspectCentered, + CustomMinimumSize = SceneEditorWindow.ThumbSize, + TooltipText = background + }; + thumb.Pressed += OnPicked; + column.AddChild(thumb); + + var row = new HBoxContainer(); + row.AddThemeConstantOverride("separation", UiTokens.Space1); + column.AddChild(row); + var caption = ToolUi.Hint(background + " · " + modId); + caption.TooltipText = background + " (" + modId + ")"; + caption.MouseFilter = MouseFilterEnum.Pass; + caption.SizeFlagsHorizontal = SizeFlags.ExpandFill; + caption.CustomMinimumSize = new Vector2(SceneEditorWindow.ThumbSize.X - 32, 0); + caption.TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis; + row.AddChild(caption); + if (removable) + { + var remove = UiKit.IconButton("✕", ToolUi.Tr("tools/scene_editor/remove_background")); + remove.Pressed += OnRemoved; + row.AddChild(remove); + } + } + + private void OnPicked() => _picked?.Invoke(_background); + + private void OnRemoved() => _removed?.Invoke(_background); + } +} diff --git a/src/SceneEditor/SceneEditorToolBoxButton.cs b/src/SceneEditor/SceneEditorToolBoxButton.cs new file mode 100644 index 0000000..74030f2 --- /dev/null +++ b/src/SceneEditor/SceneEditorToolBoxButton.cs @@ -0,0 +1,17 @@ +using Cthangover.Core.UI.Tool; + +namespace Cthangover.Tools.SceneEditor +{ + /// Toolbox button that opens the scene editor. Always visible. + public class SceneEditorToolBoxButton : IToolBoxButton + { + /// Matches . + public string ToolId => "scene_editor"; + /// No custom icon — uses text label. + public string IconPath => ""; + /// Translation key for the button label. + public string LocaleKey => "tools/scene_editor/title"; + /// Always visible in the toolbox. + public bool IsVisible() => true; + } +} diff --git a/src/SceneEditor/SceneEditorToolProvider.cs b/src/SceneEditor/SceneEditorToolProvider.cs new file mode 100644 index 0000000..c95d8dc --- /dev/null +++ b/src/SceneEditor/SceneEditorToolProvider.cs @@ -0,0 +1,19 @@ +using Cthangover.Core.UI.Tool; +using Cthangover.Mods.Interface.Tool; +using Godot; + +namespace Cthangover.Tools.SceneEditor +{ + /// Registers the scene editor as a tool window (hub, main menu tools, in-game toolbox). + public class SceneEditorToolProvider : IToolProvider + { + /// Unique tool identifier. + public string Id => "scene_editor"; + /// Translation key for the tool's display name. + public string LocaleKey => "tools/scene_editor/title"; + /// Creates the editor window instance. + public Window CreateWindow() => new SceneEditorWindow(); + /// Adds the launcher button to the main menu tool block. + public void AddMenuButton(Container container) => ToolMenuButton.Attach(container, this); + } +} diff --git a/src/SceneEditor/SceneEditorWindow.cs b/src/SceneEditor/SceneEditorWindow.cs new file mode 100644 index 0000000..989fdc0 --- /dev/null +++ b/src/SceneEditor/SceneEditorWindow.cs @@ -0,0 +1,743 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Cthangover.Core.Audio; +using Cthangover.Core.Mods; +using Cthangover.Core.Scenes; +using Cthangover.Core.UI.Theming; +using Cthangover.Core.UI.Tool.ScenarioEditor; +using Cthangover.Mods.Dialog; +using Cthangover.Mods.Interface.Theming; +using Cthangover.Mods.Interface.Tool; +using Cthangover.Tools.Common; +using Cthangover.Tools.Services; +using Godot; + +namespace Cthangover.Tools.SceneEditor +{ + /// + /// Editor of logical scenes — the places of a novel: which backgrounds a scene shows + /// (one of them at random on every entry), its ambient sound, the scenario played + /// when nothing else starts, and the scenarios that happen there. Left: every scene + /// of the loaded mods; centre: the background preview, the scene's backgrounds and a + /// searchable list of all backgrounds to add; right: properties and the scene's + /// scenarios (open one in the scenario editor, or start a new one). + /// + /// A scene is the merge of the scenes/<name>.json files of all mods + /// (backgrounds add up, a later ambient or default scenario wins). Scenarios are optional: + /// a place may hold only interactives, so a new scene starts without any. The editor writes + /// one of them — the file of the mod chosen in "Save to": the scene's own file when + /// that mod defines it, otherwise a new file that adds to a foreign scene without + /// touching the foreign mod. Backgrounds of the other files are shown locked, with + /// their mod. After saving the game re-reads scenes (), + /// so a new scene can be entered at once with "Go to scene". + /// + public partial class SceneEditorWindow : ToolWindow + { + private const int ThumbWidth = 160; + private const int ThumbHeight = 90; + + private readonly List _files = new(); + private readonly List _writableMods = new(); + private readonly List _sounds = new(); + private readonly List _scenarioKeys = new(); + private readonly List _defaultChoices = new(); + + private string _sceneName; + private bool _isNew; + private SceneFileEntry _target; + private string _preview; + private bool _suppress; + private bool _ambientPlaying; + private string _pendingMod; + private string _pendingScene; + + private FilteredList _sceneList; + private TextureRect _previewRect; + private Label _previewCaption; + private HFlowContainer _backgroundRow; + private FilteredList _picker; + private Button _addBackgroundBtn; + private LineEdit _nameEdit; + private OptionButton _modSelect; + private Label _modHint; + private OptionButton _ambientSelect; + private Button _ambientPlayBtn; + private Label _ambientHint; + private OptionButton _defaultSelect; + private Label _defaultHint; + private ItemList _scenarioList; + private Button _openScenarioBtn; + private Button _newScenarioBtn; + private Button _deleteBtn; + private Button _goBtn; + + public SceneEditorWindow() : base("tools/scene_editor/title") + { + CthangoverTheme.Apply(this); + BuildUI(); + LoadSources(); + SetHint(ToolUi.Tr("tools/scene_editor/hint")); + RefreshAll(); + } + + // ── UI construction ──────────────────────────────────────── + + private void BuildUI() + { + var outer = CreateFillContainer(); + AddChild(outer); + + var toolbar = CreateToolbar(); + outer.AddChild(toolbar); + toolbar.AddChild(ToolUi.Button("tools/common/save", Save, ButtonKind.Primary, "tools/scene_editor/save_tip")); + toolbar.AddChild(ToolUi.Button("tools/scene_editor/new_scene", NewScene, ButtonKind.Secondary, "tools/scene_editor/new_scene_tip")); + _deleteBtn = ToolUi.Button("tools/scene_editor/delete", AskDelete, ButtonKind.Danger, "tools/scene_editor/delete_tip"); + toolbar.AddChild(_deleteBtn); + toolbar.AddChild(ToolUi.Divider()); + _goBtn = ToolUi.Button("tools/scene_editor/go", GoToScene, ButtonKind.Secondary, "tools/scene_editor/go_tip"); + toolbar.AddChild(_goBtn); + toolbar.AddChild(UiKit.Spacer()); + toolbar.AddChild(ToolUi.Button("tools/scenario_editor/rescan", Rescan, ButtonKind.Secondary, "tools/scene_editor/rescan_tip")); + + var main = new HSplitContainer + { + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill + }; + outer.AddChild(main); + + var left = new VBoxContainer(); + main.AddChild(CreateSidebar(left, 260)); + left.AddChild(ToolUi.Section("tools/scene_editor/scenes")); + _sceneList = new FilteredList(200); + _sceneList.SizeFlagsVertical = Control.SizeFlags.ExpandFill; + _sceneList.Chosen += OnScenePicked; + left.AddChild(_sceneList); + + var rest = new HSplitContainer + { + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill + }; + main.AddChild(rest); + rest.AddChild(BuildCentre()); + + var right = new VBoxContainer(); + right.AddThemeConstantOverride("separation", UiTokens.Space2); + rest.AddChild(CreateSidebar(right, 340)); + BuildProperties(right); + } + + private Control BuildCentre() + { + var column = new VBoxContainer + { + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill + }; + column.AddThemeConstantOverride("separation", UiTokens.Space2); + + var frame = new PanelContainer + { + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, + SizeFlagsVertical = Control.SizeFlags.ExpandFill, + CustomMinimumSize = new Vector2(0, 260) + }; + frame.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusMd, UiTokens.Well, UiTokens.Line)); + column.AddChild(frame); + _previewRect = new TextureRect + { + ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, + StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered, + MouseFilter = Control.MouseFilterEnum.Ignore + }; + frame.AddChild(_previewRect); + _previewCaption = ToolUi.Hint(string.Empty); + column.AddChild(_previewCaption); + + column.AddChild(ToolUi.Section("tools/scene_editor/backgrounds")); + _backgroundRow = new HFlowContainer(); + _backgroundRow.AddThemeConstantOverride("h_separation", UiTokens.Space2); + _backgroundRow.AddThemeConstantOverride("v_separation", UiTokens.Space2); + column.AddChild(_backgroundRow); + + column.AddChild(ToolUi.Section("tools/scene_editor/all_backgrounds")); + _picker = new FilteredList(120); + _picker.Chosen += ShowPreview; + _picker.Activated += AddBackground; + column.AddChild(_picker); + _addBackgroundBtn = ToolUi.Button("tools/scene_editor/add_background", AddPreviewed, ButtonKind.Secondary, "tools/scene_editor/add_background_tip"); + column.AddChild(_addBackgroundBtn); + return column; + } + + private void BuildProperties(VBoxContainer column) + { + column.AddChild(ToolUi.Section("tools/scene_editor/properties")); + _nameEdit = ToolUi.Field(column, "tools/scene_editor/name", ToolUi.Text("my_scene", OnNameChanged), "tools/scene_editor/name_tip"); + _modSelect = ToolUi.Field(column, "tools/scene_editor/mod", new OptionButton(), "tools/scene_editor/mod_tip"); + _modSelect.ItemSelected += OnModPicked; + _modHint = ToolUi.Hint(string.Empty); + column.AddChild(_modHint); + + var ambientRow = new HBoxContainer(); + ambientRow.AddThemeConstantOverride("separation", UiTokens.Space1); + _ambientSelect = new OptionButton { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, FitToLongestItem = false }; + _ambientSelect.ItemSelected += OnAmbientPicked; + ambientRow.AddChild(_ambientSelect); + _ambientPlayBtn = ToolUi.Button("tools/scene_editor/listen", ToggleAmbientPreview, ButtonKind.Secondary, "tools/scene_editor/listen_tip"); + ambientRow.AddChild(_ambientPlayBtn); + ToolUi.Field(column, "tools/scene_editor/ambient", ambientRow, "tools/scene_editor/ambient_tip"); + _ambientHint = ToolUi.Hint(string.Empty); + column.AddChild(_ambientHint); + + _defaultSelect = ToolUi.Field(column, "tools/scene_editor/default_scenario", + new OptionButton { FitToLongestItem = false }, "tools/scene_editor/default_scenario_tip"); + _defaultSelect.ItemSelected += OnDefaultPicked; + _defaultHint = ToolUi.Hint(string.Empty); + column.AddChild(_defaultHint); + + column.AddChild(ToolUi.Section("tools/scene_editor/scenarios")); + _scenarioList = new ItemList { CustomMinimumSize = new Vector2(0, 140), SizeFlagsVertical = Control.SizeFlags.ExpandFill }; + _scenarioList.ItemActivated += OnScenarioActivated; + column.AddChild(_scenarioList); + + var row = new HBoxContainer(); + row.AddThemeConstantOverride("separation", UiTokens.Space1); + column.AddChild(row); + _openScenarioBtn = ToolUi.Button("tools/scene_editor/open_scenario", OpenSelectedScenario, ButtonKind.Secondary, "tools/scene_editor/open_scenario_tip"); + _openScenarioBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + row.AddChild(_openScenarioBtn); + _newScenarioBtn = ToolUi.Button("tools/scene_editor/new_scenario", NewScenario, ButtonKind.Secondary, "tools/scene_editor/new_scenario_tip"); + _newScenarioBtn.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + row.AddChild(_newScenarioBtn); + } + + // ── loading ──────────────────────────────────────────────── + + private void LoadSources() + { + _files.Clear(); + _files.AddRange(SceneFileService.Scan()); + _writableMods.Clear(); + _writableMods.AddRange(ModFileService.ListWritableMods()); + _sounds.Clear(); + _sounds.AddRange(ModResourceService.GetSoundIds()); + + _picker.SetItems(ModResourceService.GetBackgroundIds()); + _modSelect.Clear(); + foreach (var mod in _writableMods) + _modSelect.AddItem(mod); + _ambientSelect.Clear(); + _ambientSelect.AddItem(ToolUi.Tr("tools/scene_editor/none")); + foreach (var sound in _sounds) + _ambientSelect.AddItem(sound); + RebuildSceneList(); + } + + private void RebuildSceneList() + { + var items = new List<(string Id, string Text, Texture2D Icon)>(); + foreach (var group in _files.GroupBy(f => f.Name).OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)) + items.Add((group.Key, group.Key + " · " + string.Join(", ", group.Select(f => f.ModId).Distinct()), null)); + _sceneList.SetItems(items); + _sceneList.Select(_isNew ? null : _sceneName); + } + + private void OnScenePicked(string name) + { + if (name == _sceneName && !_isNew) + return; + _pendingScene = name; + RunDiscardingChanges(OpenPendingScene, KeepSceneSelection); + } + + private void OpenPendingScene() => OpenScene(_pendingScene); + + private void KeepSceneSelection() => _sceneList.Select(_isNew ? null : _sceneName); + + private void OpenScene(string name) + { + _sceneName = name; + _isNew = false; + var target = FilesOf(name).LastOrDefault(f => _writableMods.Contains(f.ModId))?.ModId + ?? _writableMods.FirstOrDefault(); + LoadTarget(target); + _preview = Merged().FirstOrDefault().Background; + MarkClean(); + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/opened"), name)); + RefreshAll(); + } + + /// Takes the chosen mod's file of the current scene as the edited one (a copy), or a new empty file. + private void LoadTarget(string modId) + { + if (modId == null) + { + _target = null; + return; + } + + var own = FilesOf(_sceneName).FirstOrDefault(f => f.ModId == modId); + _target = new SceneFileEntry + { + ModId = modId, + RelativePath = own?.RelativePath ?? SceneFileEntry.PathFor(_sceneName ?? string.Empty), + Name = _sceneName, + Backgrounds = new List(own?.Backgrounds ?? new List()), + Ambient = own?.Ambient ?? string.Empty, + DefaultScenario = own?.DefaultScenario ?? string.Empty, + }; + } + + private IEnumerable FilesOf(string name) => _files.Where(f => f.Name == name); + + /// + /// The scene as the game will merge it after saving: the other mods' files in load + /// order with the edited file at its mod's place. Yields each background with the + /// file it comes from. + /// + private IEnumerable<(string Background, SceneFileEntry File)> Merged() + { + foreach (var file in MergedFiles()) + { + foreach (var background in file.Backgrounds) + yield return (background, file); + } + } + + private List MergedFiles() + { + var order = ModRegistry.Instance.GetOrderedModIds(); + var result = FilesOf(_sceneName).Where(f => _target == null || f.ModId != _target.ModId).ToList(); + if (_target != null) + result.Add(_target); + return result.OrderBy(f => Rank(order, f.ModId)).ToList(); + } + + private static int Rank(IReadOnlyList order, string modId) + { + for (var i = 0; i < order.Count; i++) + { + if (order[i] == modId) + return i; + } + return order.Count; + } + + // ── editing ──────────────────────────────────────────────── + + private void NewScene() + { + RunDiscardingChanges(StartNewScene); + } + + private void StartNewScene() + { + _sceneName = string.Empty; + _isNew = true; + LoadTarget(_writableMods.FirstOrDefault()); + _preview = null; + _sceneList.Select(null); + SetDirty(); + SetStatus(ToolUi.Tr("tools/scene_editor/new_started")); + RefreshAll(); + _nameEdit.GrabFocus(); + } + + private void OnNameChanged(string text) + { + if (_suppress || !_isNew || _target == null) + return; + _sceneName = SceneFileService.ValidName(text) ?? string.Empty; + _target.Name = _sceneName; + _target.RelativePath = SceneFileEntry.PathFor(_sceneName); + SetDirty(); + } + + private void OnModPicked(long index) + { + if (_suppress) + return; + var mod = index >= 0 && index < _writableMods.Count ? _writableMods[(int)index] : null; + if (_isNew) + { + if (_target != null) + _target.ModId = mod; + RefreshAll(); + return; + } + _pendingMod = mod; + RunDiscardingChanges(SwitchTargetMod, RefreshAll); + } + + private void SwitchTargetMod() + { + LoadTarget(_pendingMod); + MarkClean(); + RefreshAll(); + } + + private void OnAmbientPicked(long index) + { + if (_suppress || _target == null) + return; + _target.Ambient = index > 0 ? _sounds[(int)index - 1] : string.Empty; + StopAmbientPreview(); + SetDirty(); + RefreshAll(); + } + + private void OnDefaultPicked(long index) + { + if (_suppress || _target == null) + return; + _target.DefaultScenario = index > 0 ? _defaultChoices[(int)index - 1] : string.Empty; + SetDirty(); + RefreshAll(); + } + + private void ShowPreview(string background) + { + _preview = background; + RefreshPreview(); + } + + private void AddPreviewed() => AddBackground(_preview); + + private void AddBackground(string background) + { + if (_target == null || string.IsNullOrEmpty(background)) + return; + if (Merged().Any(m => m.Background == background)) + { + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/already_there"), background)); + return; + } + _target.Backgrounds.Add(background); + _preview = background; + SetDirty(); + RefreshAll(); + } + + private void RemoveBackground(string background) + { + if (_target == null || !_target.Backgrounds.Remove(background)) + return; + if (_preview == background) + _preview = Merged().FirstOrDefault().Background; + SetDirty(); + RefreshAll(); + } + + // ── saving, deleting, going ──────────────────────────────── + + /// Ctrl+S. + protected override void OnSaveShortcut() => Save(); + + private void Save() + { + if (_target == null || !ModFileService.IsWritable(_target.ModId)) + { + SetStatus(ToolUi.Tr("tools/scene_editor/no_writable_mod"), ToolStatusKind.Error); + return; + } + if (string.IsNullOrEmpty(_sceneName)) + { + SetStatus(ToolUi.Tr("tools/scene_editor/bad_name"), ToolStatusKind.Error); + return; + } + if (_isNew && _files.Any(f => f.Name == _sceneName)) + { + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/name_taken"), _sceneName), ToolStatusKind.Error); + return; + } + if (!Merged().Any()) + { + SetStatus(ToolUi.Tr("tools/scene_editor/need_background"), ToolStatusKind.Error); + return; + } + + if (!SceneFileService.Write(_target)) + { + SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); + return; + } + + var created = _isNew; + SceneFileService.RefreshGame(); + var name = _sceneName; + var mod = _target.ModId; + LoadSources(); + _sceneName = name; + _isNew = false; + LoadTarget(mod); + MarkClean(); + _sceneList.Select(name); + RefreshAll(); + SetStatus(string.Format(ToolUi.Tr(created ? "tools/scene_editor/created" : "tools/common/saved"), + created ? name : mod + "/" + _target.RelativePath), ToolStatusKind.Ok); + } + + private void AskDelete() + { + if (_target == null || _isNew || !FilesOf(_sceneName).Any(f => f.ModId == _target.ModId)) + return; + ShowConfirm("tools/scene_editor/delete", "tools/scene_editor/delete_confirm", Delete); + } + + private void Delete() + { + if (!SceneFileService.Delete(_target)) + { + SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); + return; + } + + var path = _target.ModId + "/" + _target.RelativePath; + SceneFileService.RefreshGame(); + LoadSources(); + if (FilesOf(_sceneName).Any()) + { + OpenScene(_sceneName); + } + else + { + _sceneName = null; + _target = null; + MarkClean(); + RefreshAll(); + } + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/deleted"), path), ToolStatusKind.Ok); + } + + private void GoToScene() + { + if (_isNew || string.IsNullOrEmpty(_sceneName)) + return; + var manager = GetTree().Root.GetNodeOrNull("SceneManager"); + if (manager == null || !manager.HasScene(_sceneName)) + { + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/go_failed"), _sceneName), ToolStatusKind.Error); + return; + } + StopAmbientPreview(); + // Like the studio's teleport: a dialog left running would keep the box, and the + // entered scene's own scenario would wait behind it (or the old one would go on). + SceneContextNode.FindNode("DialogBox")?.Abort(); + manager.SwitchScene(_sceneName); + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/went"), _sceneName), ToolStatusKind.Ok); + } + + private void Rescan() + { + RunDiscardingChanges(RescanNow); + } + + private void RescanNow() + { + var name = _isNew ? null : _sceneName; + LoadSources(); + if (name != null && FilesOf(name).Any()) + OpenScene(name); + else + RefreshAll(); + SetStatus(ToolUi.Tr("tools/scene_editor/rescanned")); + } + + // ── scenarios ────────────────────────────────────────────── + + private void NewScenario() + { + if (_target == null || _isNew || string.IsNullOrEmpty(_sceneName)) + return; + var key = ScenarioFileService.CreateForScene(_target.ModId, _sceneName, NewLine()); + if (key == null) + { + SetStatus(ToolUi.Tr("tools/common/readonly"), ToolStatusKind.Error); + return; + } + SceneFileService.RefreshGame(); + RefreshAll(); + OpenScenarioEditor(key); + SetStatus(string.Format(ToolUi.Tr("tools/scene_editor/scenario_created"), key), ToolStatusKind.Ok); + } + + private void OnScenarioActivated(long index) => OpenSelectedScenario(); + + private void OpenSelectedScenario() + { + var selected = _scenarioList.GetSelectedItems(); + if (selected.Length == 0 || selected[0] >= _scenarioKeys.Count) + return; + OpenScenarioEditor(_scenarioKeys[selected[0]]); + } + + private static void OpenScenarioEditor(string key) + { + var window = new ScenarioEditorWindow(); + window.OpenScenario(key); + ShowWindow(window); + } + + private static string NewLine() => "text \"" + ToolUi.Tr("tools/scene_editor/new_line_text").Replace("\"", "\\\"") + "\""; + + // ── ambient preview ──────────────────────────────────────── + + private void ToggleAmbientPreview() + { + if (_ambientPlaying) + { + StopAmbientPreview(); + RefreshAll(); + return; + } + + var ambient = EffectiveAmbient().Ambient; + var audio = GetTree().Root.GetNodeOrNull("AudioService") as IAudioService; + if (audio == null || string.IsNullOrEmpty(ambient)) + return; + audio.PlayAmbient(ambient); + _ambientPlaying = true; + RefreshAll(); + } + + private void StopAmbientPreview() + { + if (!_ambientPlaying) + return; + _ambientPlaying = false; + (GetTree()?.Root.GetNodeOrNull("AudioService") as IAudioService)?.StopAmbient(); + } + + /// Stops a playing ambient preview when the window closes. + protected override void Cleanup() => StopAmbientPreview(); + + // ── refresh ──────────────────────────────────────────────── + + /// The ambient the game will play: the last non-empty one in load order, and the mod it comes from. + private (string Ambient, string ModId) EffectiveAmbient() + { + var file = MergedFiles().LastOrDefault(f => !string.IsNullOrEmpty(f.Ambient)); + return (file?.Ambient ?? string.Empty, file?.ModId); + } + + private (string Scenario, string ModId) EffectiveDefault() + { + var file = MergedFiles().LastOrDefault(f => !string.IsNullOrEmpty(f.DefaultScenario)); + return (file?.DefaultScenario ?? string.Empty, file?.ModId); + } + + private void RefreshAll() + { + _suppress = true; + var hasScene = _target != null && (_isNew || !string.IsNullOrEmpty(_sceneName)); + + _nameEdit.Text = _sceneName ?? string.Empty; + _nameEdit.Editable = _isNew; + _modSelect.Disabled = !hasScene; + _modSelect.Select(_target == null ? -1 : _writableMods.IndexOf(_target.ModId)); + _modHint.Text = ModHint(); + + _ambientSelect.Disabled = !hasScene; + _ambientSelect.Select(_target == null ? 0 : _sounds.IndexOf(_target.Ambient) + 1); + var ambient = EffectiveAmbient(); + _ambientHint.Text = ambient.ModId != null && ambient.ModId != _target?.ModId + ? string.Format(ToolUi.Tr("tools/scene_editor/effective"), ambient.Ambient, ambient.ModId) + : string.Empty; + _ambientPlayBtn.Text = ToolUi.Tr(_ambientPlaying ? "tools/scene_editor/stop" : "tools/scene_editor/listen"); + _ambientPlayBtn.Disabled = string.IsNullOrEmpty(ambient.Ambient) && !_ambientPlaying; + + RefreshScenarios(hasScene); + RefreshBackgrounds(); + RefreshPreview(); + + _deleteBtn.Disabled = _target == null || _isNew || !FilesOf(_sceneName).Any(f => f.ModId == _target.ModId); + _goBtn.Disabled = _isNew || string.IsNullOrEmpty(_sceneName); + _suppress = false; + } + + private string ModHint() + { + if (_writableMods.Count == 0) + return ToolUi.Tr("tools/scene_editor/no_writable_mod"); + if (_target == null || _isNew) + return string.Empty; + var others = FilesOf(_sceneName).Select(f => f.ModId).Where(m => m != _target.ModId).Distinct().ToList(); + var own = FilesOf(_sceneName).Any(f => f.ModId == _target.ModId); + if (!own) + return string.Format(ToolUi.Tr("tools/scene_editor/adds_to"), string.Join(", ", others)); + return others.Count > 0 ? string.Format(ToolUi.Tr("tools/scene_editor/also_in"), string.Join(", ", others)) : string.Empty; + } + + private void RefreshScenarios(bool hasScene) + { + _scenarioKeys.Clear(); + _defaultChoices.Clear(); + _scenarioList.Clear(); + if (hasScene && !_isNew) + { + foreach (var file in ScenarioFileService.ScanScenarioFiles().SelectMany(m => m.Groups).SelectMany(g => g.Files)) + { + var text = ScenarioFileService.ReadAllText(file.ModId, file.RelativePath); + if (text == null || ScenarioFileService.ParseMetadata(text).Scene != _sceneName) + continue; + _scenarioKeys.Add(file.Key); + _scenarioList.AddItem(file.ModId + ": " + file.RelativePath); + if (!_defaultChoices.Contains(file.RelativePath)) + _defaultChoices.Add(file.RelativePath); + } + } + if (_target != null && !string.IsNullOrEmpty(_target.DefaultScenario) && !_defaultChoices.Contains(_target.DefaultScenario)) + _defaultChoices.Add(_target.DefaultScenario); + + _defaultSelect.Clear(); + _defaultSelect.AddItem(ToolUi.Tr("tools/scene_editor/none")); + foreach (var path in _defaultChoices) + _defaultSelect.AddItem(path); + _defaultSelect.Select(_target == null ? 0 : _defaultChoices.IndexOf(_target.DefaultScenario) + 1); + _defaultSelect.Disabled = !hasScene || _isNew; + var effective = EffectiveDefault(); + _defaultHint.Text = effective.ModId != null && effective.ModId != _target?.ModId + ? string.Format(ToolUi.Tr("tools/scene_editor/effective"), effective.Scenario, effective.ModId) + : string.Empty; + + if (hasScene && !_isNew && _scenarioKeys.Count == 0) + _scenarioList.AddItem(ToolUi.Tr("tools/scene_editor/no_scenarios"), null, false); + _openScenarioBtn.Disabled = _scenarioKeys.Count == 0; + _newScenarioBtn.Disabled = !hasScene || _isNew || !ModFileService.IsWritable(_target?.ModId); + } + + private void RefreshBackgrounds() + { + foreach (var child in _backgroundRow.GetChildren()) + { + _backgroundRow.RemoveChild(child); + child.QueueFree(); + } + + var any = false; + foreach (var (background, file) in Merged()) + { + any = true; + _backgroundRow.AddChild(new SceneBackgroundTile(background, file.ModId, file == _target, background == _preview, + ShowPreview, RemoveBackground)); + } + if (!any) + _backgroundRow.AddChild(ToolUi.Hint(ToolUi.Tr(_target == null ? "tools/scene_editor/pick_scene" : "tools/scene_editor/no_backgrounds"))); + } + + private void RefreshPreview() + { + _previewRect.Texture = string.IsNullOrEmpty(_preview) ? null : ModResourceService.LoadBackgroundTexture(_preview); + _previewCaption.Text = string.IsNullOrEmpty(_preview) + ? ToolUi.Tr("tools/scene_editor/preview_hint") + : _preview + (Merged().Any(m => m.Background == _preview) ? string.Empty : " " + ToolUi.Tr("tools/scene_editor/not_in_scene")); + if (_addBackgroundBtn != null) + _addBackgroundBtn.Disabled = _target == null || string.IsNullOrEmpty(_preview) || Merged().Any(m => m.Background == _preview); + } + + internal static Vector2 ThumbSize => new(ThumbWidth, ThumbHeight); + } +} diff --git a/src/Services/ModResourceService.cs b/src/Services/ModResourceService.cs index a5570ec..0e4a7d3 100644 --- a/src/Services/ModResourceService.cs +++ b/src/Services/ModResourceService.cs @@ -46,6 +46,25 @@ namespace Cthangover.Tools.Services return ids; } + /// Ids of every sound (group sounds, path without extension): the choices for a scene's ambient. + public static List GetSoundIds() + { + var ids = new List(); + var exts = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var ext in ModConfig.Instance.AudioExtensions) + exts.Add(ext); + + foreach (var id in Files.Collect("sounds").Keys) + { + var ext = Path.GetExtension(id); + if (exts.Contains(ext)) + ids.Add(id.Substring(0, id.Length - ext.Length)); + } + + ids.Sort(StringComparer.OrdinalIgnoreCase); + return ids; + } + private static HashSet GetExt() { var set = new HashSet(); diff --git a/src/Services/ScenarioFileService.cs b/src/Services/ScenarioFileService.cs index 16fa032..a7e23ce 100644 --- a/src/Services/ScenarioFileService.cs +++ b/src/Services/ScenarioFileService.cs @@ -277,6 +277,21 @@ namespace Cthangover.Tools.Services return true; } + /// + /// Creates scenarios/<scene>[_n].scenario in a folder mod: a header bound to + /// and one line of text to replace. Returns the file key, or + /// null when the mod is not writable. + /// + public static string CreateForScene(string modId, string scene, string firstLine) + { + var path = $"{ScenariosDir}/{scene}{ScenarioExtension}"; + for (var n = 2; ModManager.Instance.ReadFileText(modId, path) != null; n++) + path = $"{ScenariosDir}/{scene}_{n}{ScenarioExtension}"; + + var text = $"scene: {scene}\npriority: 10\n---\n{firstLine}\nend\n"; + return ModFileService.WriteText(modId, path, text) ? MakeKey(modId, path) : null; + } + /// Absolute filesystem path of a scenario file, or null for zip-backed mods. public static string GetAbsolutePath(string modId, string relativePath) { diff --git a/src/Services/SceneFileEntry.cs b/src/Services/SceneFileEntry.cs new file mode 100644 index 0000000..21b4f3d --- /dev/null +++ b/src/Services/SceneFileEntry.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using Cthangover.Core.Scenes; +using Cthangover.Core.Utils; + +namespace Cthangover.Tools.Services +{ + /// + /// One mod's file of a logical scene (scenes/<name>.json). The game merges + /// the files of all mods into one (backgrounds add up, + /// a later non-empty ambient or default scenario wins); the scene editor works on + /// the files themselves, because only one of them — the chosen mod's — is written. + /// + public sealed class SceneFileEntry + { + /// Mod that owns the file. + public string ModId { get; set; } + + /// Mod-relative path of the file. + public string RelativePath { get; set; } + + /// Scene name (the logical scene id). + public string Name { get; set; } + + /// Backgrounds this file adds; the game shows one of the merged list at random. + public List Backgrounds { get; set; } = new(); + + /// Ambient sound id (group sounds), empty for none. + public string Ambient { get; set; } = string.Empty; + + /// Mod-relative path of the scenario played when nothing else starts, empty for none. + public string DefaultScenario { get; set; } = string.Empty; + + /// Default path of a scene file: scenes/<name>.json. + public static string PathFor(string name) => "scenes/" + name + ".json"; + + /// Reads a scene file with the game's own JSON options; null when it is not a scene. + public static SceneFileEntry Parse(string modId, string relativePath, string json) + { + SceneDefinition definition; + try + { + definition = JsonSerializer.Deserialize(json ?? string.Empty, JsonOptionsProvider.ReadOptions); + } + catch (JsonException) + { + return null; + } + + if (string.IsNullOrEmpty(definition?.Name)) + return null; + + return new SceneFileEntry + { + ModId = modId, + RelativePath = relativePath, + Name = definition.Name, + Backgrounds = definition.DefaultBackground ?? new List(), + Ambient = definition.DefaultAmbient ?? string.Empty, + DefaultScenario = definition.DefaultScenario ?? string.Empty, + }; + } + + /// + /// The file text in the style of the hand-written scene files (two-space indent, LF, + /// camelCase keys). Empty fields are left out, so a file of a mod that only adds a + /// background does not reset another mod's ambient or default scenario. + /// + public string ToJson() + { + var json = new StringBuilder("{\n \"name\": ").Append(Quote(Name)); + if (Backgrounds.Count > 0) + { + json.Append(",\n \"defaultBackground\": ["); + for (var i = 0; i < Backgrounds.Count; i++) + json.Append(i == 0 ? string.Empty : ", ").Append(Quote(Backgrounds[i])); + json.Append(']'); + } + if (!string.IsNullOrEmpty(Ambient)) + json.Append(",\n \"defaultAmbient\": ").Append(Quote(Ambient)); + if (!string.IsNullOrEmpty(DefaultScenario)) + json.Append(",\n \"defaultScenario\": ").Append(Quote(DefaultScenario)); + return json.Append("\n}\n").ToString(); + } + + private static string Quote(string value) => JsonSerializer.Serialize(value ?? string.Empty); + } +} diff --git a/src/Services/SceneFileService.cs b/src/Services/SceneFileService.cs new file mode 100644 index 0000000..906a2b7 --- /dev/null +++ b/src/Services/SceneFileService.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.RegularExpressions; +using Cthangover.Core.Bootstrap; +using Cthangover.Core.Mods; +using Cthangover.Core.Mods.Resolvers; +using Cthangover.Core.Scenes; + +namespace Cthangover.Tools.Services +{ + /// + /// Scene files (scenes/**.json) of the loaded mods for the scene editor: reading + /// them per mod in load order (the order the game merges them in), writing one, and + /// making the running game see the change at once. The game caches scene definitions + /// and scenario headers; drops those caches and lets + /// rescan, the same steps the scenario studio takes after + /// creating a scene in its layer, so a new scene can be entered without a mod reload. + /// + public static class SceneFileService + { + private const string ScenesDir = "scenes"; + private static readonly Regex NamePattern = new("^[a-z][a-z0-9_]*$"); + + /// Every scene file of the loaded mods and of the studio layer, in load order. + public static List Scan() + { + var result = new List(); + var manager = ModManager.Instance; + if (manager?.Mods == null) + return result; + + foreach (var modId in ModRegistry.Instance.GetOrderedModIds()) + { + if (manager.Mods.TryGetValue(modId, out var mod) && mod?.FileProvider != null) + Collect(mod.FileProvider, modId, ScenesDir, result); + } + foreach (var overlay in ModOverlays.All) + Collect(overlay.Value, overlay.Key, ScenesDir, result); + return result; + } + + /// The scene name typed by the author, trimmed and lowercased; null when it is not an id (latin letters, digits, _). + public static string ValidName(string name) + { + name = (name ?? string.Empty).Trim().ToLowerInvariant(); + return NamePattern.IsMatch(name) ? name : null; + } + + /// Writes the entry to its mod; false when the mod is not a folder mod. + public static bool Write(SceneFileEntry entry) + { + return ModFileService.WriteText(entry.ModId, entry.RelativePath, entry.ToJson()); + } + + /// Deletes the entry's file; false when the mod is not a folder mod or the file is gone. + public static bool Delete(SceneFileEntry entry) + { + var path = ModFileService.GetWritablePath(entry.ModId, entry.RelativePath); + if (path == null || !File.Exists(path)) + return false; + File.Delete(path); + return true; + } + + /// Makes the running game re-read scene files and scenario headers. + public static void RefreshGame() + { + ScenarioDefs.Invalidate(); + ModManager.Instance.GetResolver()?.ClearCache(); + AppServices.Current.SceneDefs.Clear(); + SceneManager.RescanScenes(); + } + + private static void Collect(IModFileProvider provider, string modId, string directory, List result) + { + foreach (var entry in provider.ListFiles(directory)) + { + if (entry.EndsWith("/")) + { + Collect(provider, modId, entry.TrimEnd('/'), result); + continue; + } + if (!entry.EndsWith(".json", System.StringComparison.OrdinalIgnoreCase)) + continue; + + var scene = SceneFileEntry.Parse(modId, entry, provider.ReadFileText(entry)); + if (scene != null) + result.Add(scene); + } + } + } +} diff --git a/test_suites/novel_flow/manifest.json b/test_suites/novel_flow/manifest.json new file mode 100644 index 0000000..a3cdd0c --- /dev/null +++ b/test_suites/novel_flow/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "Novel Flow Test", + "description": "End-to-end authoring test on the novel template with the editors added, using only the editors' UI: opens the scene editor from the in-game toolbar, creates a new scene with a background, writes its scenario (a way back to the town) in the scenario editor, adds a way to it to the town's scenario, goes to the town from the scene editor and walks town -> new scene -> town through the real dialog choices. Restores the game's files at the end. Activated by --itest=novel_flow; --shots= saves screenshots (windowed runs only).", + "sources": [ + "src/*.cs" + ], + "depends": [ + "my_novel_game>=1" + ] +} diff --git a/test_suites/novel_flow/src/NovelFlowTestDriver.cs b/test_suites/novel_flow/src/NovelFlowTestDriver.cs new file mode 100644 index 0000000..afae6b3 --- /dev/null +++ b/test_suites/novel_flow/src/NovelFlowTestDriver.cs @@ -0,0 +1,359 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Cthangover.Core.Bootstrap; +using Cthangover.Core.Scenes; +using Cthangover.Core.UI.Dialog; +using Cthangover.Core.UI.Tool.ScenarioEditor; +using Cthangover.Core.Utils; +using Cthangover.Mods.Dialog; +using Cthangover.Tools.Common; +using Cthangover.Tools.SceneEditor; +using Cthangover.Tools.Services; +using Godot; + +namespace Cthangover.Tools.Tests.NovelFlow +{ + /// + /// An author's session in the running novel template, through the editors' UI only: + /// + /// the toolbar's tools menu opens the scene editor; + /// New scene, a name, the game's mod, a background, Save; + /// New scenario of that scene opens the scenario editor; the scenario (a way back to + /// the town) is typed in and saved; + /// the town is picked in the scene editor, its scenario opened and a way to the new + /// scene typed in and saved; + /// Go to scene puts the game into the town, and the player walks town → new scene → + /// town by clicking the dialog's answers. + /// + /// Buttons, list rows, answers and text edits get the signals a mouse or keyboard would + /// raise. The game's files are restored at the end (town scenario text, new files deleted). + /// Result line: [TEST_RESULT] name=novel_flow passed=N failed=M, 150 s watchdog. + /// + public partial class NovelFlowTestDriver : Node + { + private const float TickSeconds = 0.3f; + private const float TimeoutSeconds = 150f; + private const string Mod = "my_novel_game"; + private const string Town = "my_novel_town"; + private const string Park = "my_novel_park"; + private const string TownScenario = "scenarios/my_novel_town.scenario"; + private const string ParkScenario = "scenarios/my_novel_park.scenario"; + private const string ToPark = "Go to the park"; + private const string ToTown = "Back to the town"; + // The town scenario before the test edits it; survives a run that is killed midway + // (the next run restores it first, so an interrupted run never leaves the park behind). + private const string Backup = "user://novel_flow_town.scenario.bak"; + + private const string ParkText = + "scene: " + Park + "\npriority: 10\n---\n" + + "text \"A quiet park behind the town. Wet leaves, an empty bench.\"\n" + + "select \"What next?\"\n" + + "option \"" + ToTown + "\" -> :town\n\n" + + ":town\nswitch_scene " + Town + "\nend\n"; + + private readonly string shotsDir; + private readonly List failures = new(); + private int passed; + private int step; + private int wait; + private double elapsed; + private string townOriginal; + private SceneEditorWindow sceneEditor; + + /// Creates the driver; null disables screenshots. + public NovelFlowTestDriver(string shotsDir) + { + this.shotsDir = shotsDir; + Name = "NovelFlowTestDriver"; + var timer = new Timer { WaitTime = TickSeconds, Autostart = true }; + AddChild(timer); + timer.Timeout += Tick; + } + + private SceneManager Scenes => GetTree().Root.GetNodeOrNull("SceneManager"); + + private void Tick() + { + if (step < 0) + return; + elapsed += TickSeconds; + if (elapsed > TimeoutSeconds) + { + Fail($"timeout at step {step}"); + Finish(); + return; + } + + try + { + RunStep(); + } + catch (System.Exception ex) + { + Fail($"step {step}: {ex.GetType().Name}: {ex.Message}"); + Finish(); + } + } + + private void RunStep() + { + switch (step) + { + case 0: + if (!Boot.IsReady || Scenes?.CurrentSceneName != Town || GodotSceneService.IsTransitioning) + return; + RestoreFromBackup(); + townOriginal = ModFileService.ReadText(Mod, TownScenario); + File.WriteAllText(ProjectSettings.GlobalizePath(Backup), townOriginal ?? string.Empty); + DeleteParkFiles(); + SceneFileService.RefreshGame(); + Check(townOriginal != null && !Scenes.HasScene(Park), "the novel runs in the town, no park yet"); + OpenToolFromToolbar("tools/scene_editor/title"); + Next(); + return; + case 1: + sceneEditor = Windows().FirstOrDefault(); + if (sceneEditor == null) + return; + Check(true, "the toolbar's tools menu opens the scene editor"); + Press(sceneEditor, "tools/scene_editor/new_scene"); + var name = sceneEditor.FindChildren("*", "", true, false).OfType().First(e => e.PlaceholderText == "my_scene"); + name.Text = Park; + name.EmitSignal(LineEdit.SignalName.TextChanged, Park); + var mods = sceneEditor.FindChildren("*", "", true, false).OfType() + .First(o => Enumerable.Range(0, o.ItemCount).Any(i => o.GetItemText(i) == Mod)); + var modIndex = Enumerable.Range(0, mods.ItemCount).First(i => mods.GetItemText(i) == Mod); + mods.Select(modIndex); + mods.EmitSignal(OptionButton.SignalName.ItemSelected, modIndex); + var picker = Lists(sceneEditor)[1]; + var background = Row(picker, "home/vestibule") >= 0 ? Row(picker, "home/vestibule") : 0; + picker.EmitSignal(ItemList.SignalName.ItemActivated, background); + Next(); + return; + case 2: + Shot("1_new_scene"); + Press(sceneEditor, "tools/common/save"); + Check(Scenes.HasScene(Park), "Save in the scene editor: the game knows the new scene at once"); + Check(File.Exists(ModFileService.GetWritablePath(Mod, "scenes/" + Park + ".json")), "the scene file is in the game's mod"); + Press(sceneEditor, "tools/scene_editor/new_scenario"); + Next(); + return; + case 3: + var parkEditor = EditorWith("scene: " + Park); + if (parkEditor == null) + return; + Check(true, "New scenario opens the scenario editor on the scene's new scenario"); + Type(parkEditor, ParkText); + Press(parkEditor, "tools/common/save"); + Check(ModFileService.ReadText(Mod, ParkScenario)?.Contains(ToTown) == true, "the park scenario is saved with the way back"); + Next(); + return; + case 4: + // A tick after typing, so the frame shows the new text. + Shot("2_park_scenario"); + PickScene(Town); + Next(); + return; + case 5: + var rows = sceneEditor.FindChildren("*", "", true, false).OfType().Last(); + var townRow = Row(rows, Mod + ": " + TownScenario); + Check(townRow >= 0, "the town's scenario is listed under the town in the scene editor"); + rows.Select(townRow); + rows.EmitSignal(ItemList.SignalName.ItemActivated, townRow); + Next(); + return; + case 6: + var townEditor = EditorWith("scene: " + Town); + if (townEditor == null) + return; + var text = Code(townEditor).Text; + text = text.Replace("option \"Go back home\"", "option \"" + ToPark + "\" -> :park\noption \"Go back home\"") + + "\n:park\nswitch_scene " + Park + "\nend\n"; + Type(townEditor, text); + Press(townEditor, "tools/common/save"); + Check(ModFileService.ReadText(Mod, TownScenario)?.Contains("switch_scene " + Park) == true, "the town scenario is saved with the way to the park"); + Next(); + return; + case 7: + Shot("3_town_scenario"); + Press(sceneEditor, "tools/scene_editor/go"); + Next(); + return; + case 8: + if (++wait < 3 || GodotSceneService.IsTransitioning) + return; + foreach (var window in GetTree().Root.GetChildren().OfType().ToList()) + window.EmitSignal(Window.SignalName.CloseRequested); + Next(); + return; + case 9: + if (!Answer(ToPark)) + return; + Shot("4_town_choice"); + Click(ToPark); + Next(); + return; + case 10: + if (Scenes.CurrentSceneName != Park || GodotSceneService.IsTransitioning || !Answer(ToTown)) + return; + Check(true, "choosing the new answer in the town leads into the park"); + Shot("5_park"); + Click(ToTown); + Next(); + return; + case 11: + if (Scenes.CurrentSceneName != Town || GodotSceneService.IsTransitioning || !Answer(ToPark)) + return; + Check(true, "the park's answer leads back to the town, which offers the park again"); + Shot("6_back_in_town"); + Finish(); + return; + } + } + + // ── the author's hands ───────────────────────────────────── + + /// The toolbar's tools button opens its menu; the tool is picked by its title. + private void OpenToolFromToolbar(string titleKey) + { + var menuTitle = TranslationServer.Translate("tools/menu_title").ToString(); + var button = GetTree().Root.FindChildren("*", "", true, false).OfType