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.
This commit is contained in:
1 parent
223bb8fe0d
commit
81594808f6
12 files changed
+318
-33
No files matched your search
@@ -0,0 +1,15 @@
|
||||
namespace Cthangover.Mods.ScenarioStudio
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class StudioAssetReload
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public List<string> Added { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -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 <c>backgrounds/</c>); the "Resources" button of the transport
|
||||
/// bar re-reads them, so a new scene can be built on a new picture.
|
||||
/// </summary>
|
||||
public static class StudioMapPage
|
||||
{
|
||||
public static void Build(VBoxContainer content, StudioSession session, Action<StudioActionButton> go, Action<StudioActionButton> newScene, Action<StudioActionButton> link, Action<StudioActionButton> export)
|
||||
public static void Build(VBoxContainer content, StudioSession session, string layerPath, Action<StudioActionButton> go, Action<StudioActionButton> newScene, Action<StudioActionButton> link, Action<StudioActionButton> 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<StudioActionButton> go)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Backgrounds as the studio offers them: ids of the <c>backgrounds</c> file
|
||||
/// group of every mod and of the studio layer (the layer wins, see
|
||||
/// <see cref="FileListResolver"/>), without the <c>_depth</c>/<c>_albedo</c>
|
||||
/// lighting maps that belong to a background.
|
||||
/// <para/>
|
||||
/// 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
|
||||
/// <see cref="BackgroundFactory"/> were filled before, so <see cref="Reload"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class StudioBackgrounds
|
||||
{
|
||||
private const string DepthSuffix = "_depth";
|
||||
private const string AlbedoSuffix = "_albedo";
|
||||
|
||||
/// <summary>Background ids (group-relative paths without extension), sorted.</summary>
|
||||
public static List<string> Ids()
|
||||
{
|
||||
var ids = new List<string>();
|
||||
var files = Files.Collect(BackgroundFactory.Instance.GroupName);
|
||||
if (files == null)
|
||||
return ids;
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>The mod (or the layer) whose file the background <paramref name="id"/> is read from; null when unknown.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static void Reload()
|
||||
{
|
||||
ModManager.Instance.GetResolver<FileListResolver>()?.ClearCache();
|
||||
BackgroundFactory.Instance.Clear();
|
||||
|
||||
var id = SceneContextNode.LastBackgroundID;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
return;
|
||||
var view = SceneContextNode.FindNode<Node>("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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,12 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
/// <summary>
|
||||
/// The studio's edit layer: a folder per game (<c>user://studio/<game>/</c>,
|
||||
/// or <c>--studio-layer=<dir></c>) laid out like a mod (<c>scenarios/</c>,
|
||||
/// <c>scenes/</c>) and mounted as content overlay <see cref="Id"/> through
|
||||
/// <c>scenes/</c>, <c>backgrounds/</c>) and mounted as content overlay <see cref="Id"/> through
|
||||
/// <see cref="ModOverlays"/>. 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 <c>backgrounds/</c> become backgrounds
|
||||
/// after <see cref="StudioBackgrounds.Reload"/>.
|
||||
/// <para/>
|
||||
/// Edits of folder mods go into the mod itself (the author's workflow)
|
||||
/// unless <c>--studio-write=layer</c> forces the layer — tests use it to
|
||||
@@ -53,11 +55,12 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
RootPath = Path.Combine(ProjectSettings.GlobalizePath(dir), game);
|
||||
}
|
||||
|
||||
/// <summary>Creates the folder if needed, mounts it as overlay and refreshes scenario and scene tables.</summary>
|
||||
/// <summary>Creates the folders if needed, mounts it as overlay and refreshes scenario and scene tables.</summary>
|
||||
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}");
|
||||
|
||||
+53
-11
@@ -24,8 +24,9 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
/// <see cref="StudioLayer"/>, the per-scenario <see cref="StudioWorkingCopy"/>s
|
||||
/// and the rollback <see cref="StudioSnapshot"/> 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.
|
||||
/// <para/>
|
||||
/// <see cref="Version"/> 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 ────────────────────────────────────────────
|
||||
|
||||
/// <summary>Creates a scene in the layer (backgrounds of the current scene) with a first scenario; returns false on a bad name.</summary>
|
||||
public bool CreateScene(string name)
|
||||
/// <summary>The scene or mod id typed by the author, trimmed and lowercased; null when it is not one (latin letters, digits, _).</summary>
|
||||
public static string ValidName(string name)
|
||||
{
|
||||
name = (name ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (!ModIdPattern.IsMatch(name))
|
||||
return ModIdPattern.IsMatch(name) ? name : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a scene in the layer with a first scenario; returns false on a bad name.
|
||||
/// The scene shows <paramref name="background"/>, or the backgrounds of the current scene when it is null.
|
||||
/// </summary>
|
||||
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<string> 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<string> { 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads the backgrounds after the author dropped pictures into a mod or
|
||||
/// the layer (<see cref="StudioBackgrounds.Reload"/>): the current background
|
||||
/// is shown from its file again, new ones can be picked for a scene.
|
||||
/// </summary>
|
||||
public StudioAssetReload ReloadAssets()
|
||||
{
|
||||
var before = new HashSet<string>(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;
|
||||
}
|
||||
|
||||
/// <summary>Exports the layer as mod <paramref name="modId"/>; null on a bad id.</summary>
|
||||
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));
|
||||
|
||||
+42
-2
@@ -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<string> _pickLabels = new();
|
||||
private List<string> _pickScenes = new();
|
||||
private List<string> _pickBackgrounds = new();
|
||||
private string _newSceneName;
|
||||
|
||||
private PanelContainer _runState;
|
||||
private Label _runStateText;
|
||||
@@ -125,6 +129,10 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
_continueButton.Pressed += OnContinuePressed;
|
||||
_restartButton = GetNode<MenuButton>(Root + "Transport/Row/RestartButton");
|
||||
BuildRestartMenu(_restartButton);
|
||||
var reload = GetNode<Button>(Root + "Transport/Row/ReloadButton");
|
||||
StudioStyles.Transport(reload, Tr("studio/reload_assets"));
|
||||
reload.TooltipText = string.Format(Tr("studio/reload_assets_tip"), System.IO.Path.Combine(Live.Layer.RootPath, "backgrounds"));
|
||||
reload.Pressed += OnReloadAssets;
|
||||
|
||||
_crumbs = GetNode<HFlowContainer>(Root + "Context/Column/Crumbs");
|
||||
_scope = GetNode<HFlowContainer>(Root + "Context/Column/Scope");
|
||||
@@ -325,7 +333,7 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
StudioLogPage.Build(_content, _session);
|
||||
break;
|
||||
case StudioTab.Map:
|
||||
StudioMapPage.Build(_content, _session, OnGoToScene, OnNewScene, OnLinkScene, OnExport);
|
||||
StudioMapPage.Build(_content, _session, Live.Layer.RootPath, OnGoToScene, OnNewScene, OnLinkScene, OnExport);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -667,7 +675,39 @@ namespace Cthangover.Mods.ScenarioStudio
|
||||
private void OnSceneNameEntered(string name)
|
||||
{
|
||||
_prompt = null;
|
||||
_status = Live.CreateScene(name) ? string.Format(Tr("studio/scene_created"), name.Trim().ToLowerInvariant()) : Tr("studio/bad_name");
|
||||
_newSceneName = StudioLive.ValidName(name);
|
||||
if (_newSceneName == null)
|
||||
{
|
||||
_status = Tr("studio/bad_name");
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = StudioPrompt.Pick(this, OnSceneBackgroundPicked);
|
||||
menu.AddItem(Tr("studio/background_current"), (int)BackgroundCurrent);
|
||||
_pickBackgrounds = StudioBackgrounds.Ids();
|
||||
menu.AddSeparator(Tr("studio/backgrounds"));
|
||||
for (var i = 0; i < _pickBackgrounds.Count; i++)
|
||||
menu.AddItem(string.Format(Tr("studio/background_item"), _pickBackgrounds[i], StudioBackgrounds.SourceOf(_pickBackgrounds[i])), (int)(BackgroundBase + i));
|
||||
_prompt = menu;
|
||||
StudioPrompt.Open(menu);
|
||||
}
|
||||
|
||||
private void OnSceneBackgroundPicked(long id)
|
||||
{
|
||||
_prompt = null;
|
||||
var index = (int)(id - BackgroundBase);
|
||||
var background = index >= 0 && index < _pickBackgrounds.Count ? _pickBackgrounds[index] : null;
|
||||
_status = Live.CreateScene(_newSceneName, background) ? string.Format(Tr("studio/scene_created"), _newSceneName) : Tr("studio/bad_name");
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnReloadAssets()
|
||||
{
|
||||
var reload = Live.ReloadAssets();
|
||||
_status = reload.Added.Count == 0
|
||||
? string.Format(Tr("studio/assets_reloaded"), reload.Total)
|
||||
: string.Format(Tr("studio/assets_reloaded_new"), reload.Total, string.Join(", ", reload.Added));
|
||||
Refresh();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user