From 81594808f64c7b504ba69de988b8a631b85443a2 Mon Sep 17 00:00:00 2001 From: iisaev Date: Wed, 21 Oct 2026 19:18:26 +0200 Subject: [PATCH] Studio: backgrounds dropped during the game are picked up by the Resources button Layer gets backgrounds/; StudioBackgrounds re-reads the file list and the background cache and shows the current background (with _depth/_albedo) anew. The Resources button sits on the transport bar (every tab); a new scene picks its background from a list with the source mod. studio_live covers drop, reload, replace and export. --- locale/en.properties | 9 ++ locale/ru.properties | 9 ++ manifest.json | 2 +- scenes/studio_panel.tscn | 4 + src/Model/StudioAssetReload.cs | 15 +++ src/Pages/StudioMapPage.cs | 12 ++- src/Runtime/StudioBackgrounds.cs | 101 +++++++++++++++++++ src/Runtime/StudioLayer.cs | 9 +- src/Runtime/StudioLive.cs | 64 ++++++++++-- src/StudioPanel.cs | 44 +++++++- test_suites/live/manifest.json | 2 +- test_suites/live/src/StudioLiveDriverNode.cs | 80 ++++++++++++--- 12 files changed, 318 insertions(+), 33 deletions(-) create mode 100644 src/Model/StudioAssetReload.cs create mode 100644 src/Runtime/StudioBackgrounds.cs diff --git a/locale/en.properties b/locale/en.properties index 3f78c86..e0b9d2a 100644 --- a/locale/en.properties +++ b/locale/en.properties @@ -156,3 +156,12 @@ studio/log_text_checkpoint_restored = back to the checkpoint in {0} studio/drag_next_line = Drag to another line to run it next studio/next_line_failed = The dialog cannot continue from this line studio/log_text_next_line = next line moved to {0} +studio/reload_assets = Resources +studio/reload_assets_tip = Re-read backgrounds dropped during the game into backgrounds/ of a mod or of the layer ({0}) +studio/map_assets_hint = Put new backgrounds into backgrounds/ of your mod or of the layer ({0}), then press "Resources" on the bar above. A _depth next to it feeds the lighting. +studio/assets_reloaded = Resources re-read: {0} backgrounds, nothing new +studio/assets_reloaded_new = Resources re-read: {0} backgrounds, new: {1} +studio/log_text_assets_reloaded = resources re-read: {0} backgrounds, {1} new +studio/background_current = Backgrounds of the current scene +studio/backgrounds = Backgrounds +studio/background_item = background {0} · {1} diff --git a/locale/ru.properties b/locale/ru.properties index b7efa1b..d049f18 100644 --- a/locale/ru.properties +++ b/locale/ru.properties @@ -156,3 +156,12 @@ studio/log_text_checkpoint_restored = возврат к контрольной studio/drag_next_line = Перетащите на другую строку, чтобы она выполнилась следующей studio/next_line_failed = С этой строки диалог продолжить не может studio/log_text_next_line = следующая строка перенесена на {0} +studio/reload_assets = Ресурсы +studio/reload_assets_tip = Перечитать фоны, положенные во время игры в backgrounds/ мода или слоя ({0}) +studio/map_assets_hint = Новые фоны кладите в backgrounds/ своего мода или слоя ({0}), затем нажмите «Ресурсы» на панели сверху. Рядом можно положить <фон>_depth для освещения. +studio/assets_reloaded = Ресурсы перечитаны: фонов {0}, новых нет +studio/assets_reloaded_new = Ресурсы перечитаны: фонов {0}, новые: {1} +studio/log_text_assets_reloaded = ресурсы перечитаны: фонов {0}, новых {1} +studio/background_current = Фоны текущей сцены +studio/backgrounds = Фоны +studio/background_item = фон {0} · {1} diff --git a/manifest.json b/manifest.json index 6c2cb4b..997f123 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "Scenario Studio", "version": "0.1.0", "author": "ct", - "description": "In-game scenario debugger and live editor: a non-modal dock over the running game with pause, step, breakpoints and restarts (with state rollback), the current scenario line, scene candidates with the reason they did or did not start, editable game state, an event log and the scene map. Edits go into folder mods or into a per-game studio layer over read-only mods, which can be exported as a mod. Author mode shows the script, novice mode the story moment.", + "description": "In-game scenario debugger and live editor: a non-modal dock over the running game with pause, step, breakpoints and restarts (with state rollback), the current scenario line, scene candidates with the reason they did or did not start, editable game state, an event log and the scene map. Edits go into folder mods or into a per-game studio layer over read-only mods, which can be exported as a mod; backgrounds dropped into a mod or the layer during the game are picked up with the \"Resources\" button of the transport bar. Author mode shows the script, novice mode the story moment.", "category": "tools, editor", "sources": [ "src/**/*.cs" diff --git a/scenes/studio_panel.tscn b/scenes/studio_panel.tscn index d4cc476..c51c937 100644 --- a/scenes/studio_panel.tscn +++ b/scenes/studio_panel.tscn @@ -126,6 +126,10 @@ layout_mode = 2 size_flags_horizontal = 3 mouse_filter = 2 +[node name="ReloadButton" type="Button" parent="Frame/Dock/Column/Transport/Row"] +layout_mode = 2 +focus_mode = 0 + [node name="RestartButton" type="MenuButton" parent="Frame/Dock/Column/Transport/Row"] layout_mode = 2 focus_mode = 0 diff --git a/src/Model/StudioAssetReload.cs b/src/Model/StudioAssetReload.cs new file mode 100644 index 0000000..8f1a413 --- /dev/null +++ b/src/Model/StudioAssetReload.cs @@ -0,0 +1,15 @@ +namespace Cthangover.Mods.ScenarioStudio +{ + using System.Collections.Generic; + + /// + /// What the "Resources" button found: how many backgrounds the game has now and + /// which of them appeared since the last read (files dropped in while the + /// game ran). A replaced picture is not "added" — it is simply shown anew. + /// + public sealed class StudioAssetReload + { + public int Total { get; set; } + public List Added { get; set; } = new(); + } +} diff --git a/src/Pages/StudioMapPage.cs b/src/Pages/StudioMapPage.cs index 542b54f..f079872 100644 --- a/src/Pages/StudioMapPage.cs +++ b/src/Pages/StudioMapPage.cs @@ -12,10 +12,13 @@ namespace Cthangover.Mods.ScenarioStudio /// leads there. Exits can be taken directly (a debugging teleport); new /// scenes are created in the studio layer, the current scenario can be /// linked to any scene, and the layer can be exported as a regular mod. + /// The hint under the buttons tells where to drop new backgrounds (a mod or + /// the layer's backgrounds/); the "Resources" button of the transport + /// bar re-reads them, so a new scene can be built on a new picture. /// public static class StudioMapPage { - public static void Build(VBoxContainer content, StudioSession session, Action go, Action newScene, Action link, Action export) + public static void Build(VBoxContainer content, StudioSession session, string layerPath, Action go, Action newScene, Action link, Action export) { var visited = false; foreach (var node in session.Nodes) @@ -57,6 +60,13 @@ namespace Cthangover.Mods.ScenarioStudio actions.AddChild(StudioActionButton.Create("studio/map_link", link)); actions.AddChild(StudioActionButton.Create("studio/map_export", export)); content.AddChild(actions); + + var backgrounds = System.IO.Path.Combine(layerPath, "backgrounds"); + // Arbitrary wrap: the layer path is one long "word" and must not widen the dock. + var hint = UiKit.Hint(string.Format(TranslationServer.Translate("studio/map_assets_hint"), backgrounds), UiTokens.FontSmall); + hint.AutowrapMode = TextServer.AutowrapMode.Arbitrary; + hint.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + content.AddChild(hint); } private static Control BuildNode(StudioSceneNode node, Action go) diff --git a/src/Runtime/StudioBackgrounds.cs b/src/Runtime/StudioBackgrounds.cs new file mode 100644 index 0000000..c3ef30a --- /dev/null +++ b/src/Runtime/StudioBackgrounds.cs @@ -0,0 +1,101 @@ +namespace Cthangover.Mods.ScenarioStudio +{ + using System; + using System.Collections.Generic; + using System.IO; + using Cthangover.Core.Actions; + using Cthangover.Core.Factories.Impls; + using Cthangover.Core.Mods; + using Cthangover.Core.Mods.Resolvers; + using Cthangover.Core.Scenes; + using Cthangover.Core.UI; + using Godot; + + /// + /// Backgrounds as the studio offers them: ids of the backgrounds file + /// group of every mod and of the studio layer (the layer wins, see + /// ), without the _depth/_albedo + /// lighting maps that belong to a background. + /// + /// The author drops a picture into a folder mod or into the layer while the + /// game runs; both the file list of the group and the texture cache of + /// were filled before, so + /// drops both and shows the current background again — a replaced picture + /// (or its depth map) is visible at once, a new one can be picked for a scene. + /// + public static class StudioBackgrounds + { + private const string DepthSuffix = "_depth"; + private const string AlbedoSuffix = "_albedo"; + + /// Background ids (group-relative paths without extension), sorted. + public static List Ids() + { + var ids = new List(); + var files = Files.Collect(BackgroundFactory.Instance.GroupName); + if (files == null) + return ids; + + var seen = new HashSet(); + + foreach (var key in files.Keys) + { + if (!IsTexture(key)) + continue; + var id = key.Substring(0, key.Length - Path.GetExtension(key).Length); + if (!id.EndsWith(DepthSuffix, StringComparison.OrdinalIgnoreCase) && !id.EndsWith(AlbedoSuffix, StringComparison.OrdinalIgnoreCase) && seen.Add(id)) + ids.Add(id); + } + + ids.Sort(StringComparer.Ordinal); + return ids; + } + + /// The mod (or the layer) whose file the background is read from; null when unknown. + public static string SourceOf(string id) + { + var files = Files.Collect(BackgroundFactory.Instance.GroupName); + if (files == null) + return null; + foreach (var extension in ModConfig.Instance.TextureExtensions) + { + if (files.TryGetValue(id + extension, out var entry)) + return entry.ModId; + } + return null; + } + + /// + /// Re-reads the background files: drops the collected file lists and the + /// cached background textures, then puts the current background and its + /// lighting maps on screen again from the files as they are now. + /// + public static void Reload() + { + ModManager.Instance.GetResolver()?.ClearCache(); + BackgroundFactory.Instance.Clear(); + + var id = SceneContextNode.LastBackgroundID; + if (string.IsNullOrEmpty(id)) + return; + var view = SceneContextNode.FindNode("ViewBox") as IBackgroundView; + var texture = BackgroundFactory.Instance.Get(id); + if (view == null || texture == null) + return; + + view.SetBackgroundTexture(texture); + LightingBridge.SetupBackgroundMaps(BackgroundFactory.Instance.Get(id + DepthSuffix), BackgroundFactory.Instance.Get(id + AlbedoSuffix)); + } + + private static bool IsTexture(string key) + { + var extension = Path.GetExtension(key); + foreach (var known in ModConfig.Instance.TextureExtensions) + { + if (string.Equals(known, extension, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + } +} diff --git a/src/Runtime/StudioLayer.cs b/src/Runtime/StudioLayer.cs index c4a11d3..f33711a 100644 --- a/src/Runtime/StudioLayer.cs +++ b/src/Runtime/StudioLayer.cs @@ -15,10 +15,12 @@ namespace Cthangover.Mods.ScenarioStudio /// /// The studio's edit layer: a folder per game (user://studio/<game>/, /// or --studio-layer=<dir>) laid out like a mod (scenarios/, - /// scenes/) and mounted as content overlay through + /// scenes/, backgrounds/) and mounted as content overlay through /// . Scenarios written here override same-named /// scenarios of any mod, so a player can change content of read-only zip - /// mods without touching them; new scenes and scenarios live here too. + /// mods without touching them; new scenes and scenarios live here too, and + /// pictures the author drops into backgrounds/ become backgrounds + /// after . /// /// Edits of folder mods go into the mod itself (the author's workflow) /// unless --studio-write=layer forces the layer — tests use it to @@ -53,11 +55,12 @@ namespace Cthangover.Mods.ScenarioStudio RootPath = Path.Combine(ProjectSettings.GlobalizePath(dir), game); } - /// Creates the folder if needed, mounts it as overlay and refreshes scenario and scene tables. + /// Creates the folders if needed, mounts it as overlay and refreshes scenario and scene tables. public void Mount() { Directory.CreateDirectory(Path.Combine(RootPath, "scenarios")); Directory.CreateDirectory(Path.Combine(RootPath, "scenes")); + Directory.CreateDirectory(Path.Combine(RootPath, "backgrounds")); ModOverlays.Mount(Id, new FolderModFileProvider(RootPath, Id)); RefreshContent(); GameLogger.Log("UI", $"Scenario studio: layer '{Id}' mounted at {RootPath}"); diff --git a/src/Runtime/StudioLive.cs b/src/Runtime/StudioLive.cs index 4351b78..2caebfa 100644 --- a/src/Runtime/StudioLive.cs +++ b/src/Runtime/StudioLive.cs @@ -24,8 +24,9 @@ namespace Cthangover.Mods.ScenarioStudio /// , the per-scenario s /// and the rollback of the running scenario, and /// performs every action of the dock: apply edits, the restart modes, run a - /// scene candidate, go to a scene, create scenes and scenarios, change - /// state values, export the layer. + /// scene candidate, go to a scene, create scenes and scenarios, re-read + /// backgrounds dropped in while the game runs, change state values, export + /// the layer. /// /// grows on every change the dock may want to show; /// the dock polls it instead of subscribing, so a burst of dialog actions @@ -452,18 +453,35 @@ namespace Cthangover.Mods.ScenarioStudio // ── new content ──────────────────────────────────────────── - /// Creates a scene in the layer (backgrounds of the current scene) with a first scenario; returns false on a bad name. - public bool CreateScene(string name) + /// The scene or mod id typed by the author, trimmed and lowercased; null when it is not one (latin letters, digits, _). + public static string ValidName(string name) { name = (name ?? string.Empty).Trim().ToLowerInvariant(); - if (!ModIdPattern.IsMatch(name)) + return ModIdPattern.IsMatch(name) ? name : null; + } + + /// + /// Creates a scene in the layer with a first scenario; returns false on a bad name. + /// The scene shows , or the backgrounds of the current scene when it is null. + /// + public bool CreateScene(string name, string background = null) + { + name = ValidName(name); + if (name == null) return false; - var current = SceneName; - var defs = AppServices.Current.SceneDefs.CollectAll(); List backgrounds = null; - if (current != null && defs != null && defs.TryGetValue(current, out var list) && list.Count > 0) - backgrounds = list[0].DefaultBackground; + if (background != null) + { + backgrounds = new List { background }; + } + else + { + var current = SceneName; + var defs = AppServices.Current.SceneDefs.CollectAll(); + if (current != null && defs != null && defs.TryGetValue(current, out var list) && list.Count > 0) + backgrounds = list[0].DefaultBackground; + } var scenario = Layer.CreateScene(name, backgrounds, StudioScriptEdits.NewLine()); StudioLayer.RefreshContent(); @@ -484,11 +502,35 @@ namespace Cthangover.Mods.ScenarioStudio return name; } + /// + /// Re-reads the backgrounds after the author dropped pictures into a mod or + /// the layer (): the current background + /// is shown from its file again, new ones can be picked for a scene. + /// + public StudioAssetReload ReloadAssets() + { + var before = new HashSet(StudioBackgrounds.Ids()); + StudioBackgrounds.Reload(); + + var result = new StudioAssetReload(); + foreach (var id in StudioBackgrounds.Ids()) + { + result.Total++; + if (!before.Contains(id)) + result.Added.Add(id); + } + + Journal.Add(StudioLogCategory.Edit, new StudioText("studio/log_text_assets_reloaded", result.Total, result.Added.Count)); + GameLogger.Log("UI", $"Scenario studio: backgrounds re-read, {result.Total} in total, new: [{string.Join(", ", result.Added)}]"); + Touch(); + return result; + } + /// Exports the layer as mod ; null on a bad id. public string Export(string modId) { - modId = (modId ?? string.Empty).Trim().ToLowerInvariant(); - if (!ModIdPattern.IsMatch(modId)) + modId = ValidName(modId); + if (modId == null) return null; var path = Layer.Export(modId); Journal.Add(StudioLogCategory.Edit, new StudioText("studio/log_text_exported", modId)); diff --git a/src/StudioPanel.cs b/src/StudioPanel.cs index 418417a..936fa41 100644 --- a/src/StudioPanel.cs +++ b/src/StudioPanel.cs @@ -33,6 +33,8 @@ namespace Cthangover.Mods.ScenarioStudio private const long TargetEnd = 0; private const long TargetLabelBase = 1000; private const long TargetSceneBase = 2000; + private const long BackgroundCurrent = 0; + private const long BackgroundBase = 1; private const int CheckpointSave = 100; private const int CheckpointRestore = 101; @@ -50,6 +52,8 @@ namespace Cthangover.Mods.ScenarioStudio private StudioCodeLine _promptLine; private List _pickLabels = new(); private List _pickScenes = new(); + private List _pickBackgrounds = new(); + private string _newSceneName; private PanelContainer _runState; private Label _runStateText; @@ -125,6 +129,10 @@ namespace Cthangover.Mods.ScenarioStudio _continueButton.Pressed += OnContinuePressed; _restartButton = GetNode(Root + "Transport/Row/RestartButton"); BuildRestartMenu(_restartButton); + var reload = GetNode