This commit is contained in:
cthangover committed 2026-09-23 17:55:57 +03:00
1 parent 8e6ad52ee4
commit de310e7295
10 files changed
+201 -138

No files matched your search

+1
View File
@@ -80,6 +80,7 @@ tools/scenario_editor/no_backgrounds = No backgrounds
tools/scenario_editor/status_modified = Modified (unsaved) tools/scenario_editor/status_modified = Modified (unsaved)
tools/scenario_editor/status_saved = Saved tools/scenario_editor/status_saved = Saved
tools/scenario_editor/save_error = Save error: {0} tools/scenario_editor/save_error = Save error: {0}
tools/scenario_editor/readonly_zip = File is provided by a zip mod and is read-only
tools/scenario_editor/unsaved_switch_text = You have unsaved changes. Switch file without saving? tools/scenario_editor/unsaved_switch_text = You have unsaved changes. Switch file without saving?
tools/unsaved_title = Unsaved changes tools/unsaved_title = Unsaved changes
+1
View File
@@ -81,6 +81,7 @@ tools/scenario_editor/no_backgrounds = Нет фонов
tools/scenario_editor/status_modified = Изменено (не сохранено) tools/scenario_editor/status_modified = Изменено (не сохранено)
tools/scenario_editor/status_saved = Сохранено tools/scenario_editor/status_saved = Сохранено
tools/scenario_editor/save_error = Ошибка сохранения: {0} tools/scenario_editor/save_error = Ошибка сохранения: {0}
tools/scenario_editor/readonly_zip = Файл из zip-мода доступен только для чтения
tools/scenario_editor/unsaved_switch_text = У вас есть несохранённые изменения. Сменить файл без сохранения? tools/scenario_editor/unsaved_switch_text = У вас есть несохранённые изменения. Сменить файл без сохранения?
tools/unsaved_title = Есть изменения tools/unsaved_title = Есть изменения
@@ -55,7 +55,6 @@ namespace Cthangover.Tools.InteractiveEditor
public InteractiveEditorWindow() : base("tools/interactive_editor/title") public InteractiveEditorWindow() : base("tools/interactive_editor/title")
{ {
Title = "Interactive Editor";
Size = new Vector2I(1280, 820); Size = new Vector2I(1280, 820);
var outer = CreateFillContainer(); var outer = CreateFillContainer();
+4 -1
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Cthangover.Core.UI.Lights; using Cthangover.Core.UI.Lights;
using Cthangover.Core.Utils;
using Cthangover.Tools.Services; using Cthangover.Tools.Services;
using Godot; using Godot;
@@ -366,8 +368,9 @@ namespace Cthangover.Core.UI.Tool.LightEditor
selectedIndex = -1; selectedIndex = -1;
RefreshAll(); RefreshAll();
} }
catch catch (Exception ex)
{ {
GameLogger.Log("LIGHT_EDITOR", $"Import JSON failed: {ex.Message}", LogLevel.Error);
} }
} }
} }
+25 -8
View File
@@ -10,7 +10,9 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
{ {
public sealed class TabData public sealed class TabData
{ {
public string FilePath; public string Key;
public string ModId;
public string RelativePath;
public string FileName; public string FileName;
public string SavedText; public string SavedText;
public bool IsDirty; public bool IsDirty;
@@ -31,8 +33,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
public TextEdit CodeEditor => _textEdit; public TextEdit CodeEditor => _textEdit;
public string CurrentText => _textEdit.Text; public string CurrentText => _textEdit.Text;
public string CurrentAbsolutePath => public string CurrentKey =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].FilePath : null; _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].Key : null;
public string CurrentModId =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].ModId : null;
public string CurrentRelativePath =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].RelativePath : null;
public bool HasActiveFile => _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count; public bool HasActiveFile => _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count;
public bool IsCurrentDirty => public bool IsCurrentDirty =>
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count && _tabs[_activeTabIndex].IsDirty; _activeTabIndex >= 0 && _activeTabIndex < _tabs.Count && _tabs[_activeTabIndex].IsDirty;
@@ -73,24 +79,35 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
AddChild(_graphView); AddChild(_graphView);
} }
public void OpenFile(string absolutePath) public void OpenFile(string key)
{ {
for (int i = 0; i < _tabs.Count; i++) for (int i = 0; i < _tabs.Count; i++)
{ {
if (_tabs[i].FilePath == absolutePath) if (_tabs[i].Key == key)
{ {
SwitchToTextTab(i); SwitchToTextTab(i);
return; return;
} }
} }
if (!ScenarioFileService.TryParseKey(key, out var modId, out var relativePath))
return;
try try
{ {
var text = ScenarioFileService.ReadAllText(absolutePath); var text = ScenarioFileService.ReadAllText(modId, relativePath);
if (text == null)
{
GameLogger.Log("SCENARIO_EDITOR", $"Failed to read scenario '{key}'", LogLevel.Error);
return;
}
var tab = new TabData var tab = new TabData
{ {
FilePath = absolutePath, Key = key,
FileName = Path.GetFileNameWithoutExtension(absolutePath), ModId = modId,
RelativePath = relativePath,
FileName = Path.GetFileNameWithoutExtension(relativePath),
SavedText = text, SavedText = text,
IsDirty = false IsDirty = false
}; };
+32 -11
View File
@@ -180,8 +180,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
private void OnTextTabSelected(string text) private void OnTextTabSelected(string text)
{ {
_filePathLabel.Text = _editTabs.CurrentAbsolutePath != null _filePathLabel.Text = _editTabs.CurrentKey != null
? ScenarioFileService.GetRelativePath(_editTabs.CurrentAbsolutePath) : ""; ? ScenarioFileService.GetDisplayPath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath) : "";
_saveBtn.Disabled = false; _saveBtn.Disabled = false;
_openFileBtn.Disabled = false; _openFileBtn.Disabled = false;
_playBtn.Disabled = false; _playBtn.Disabled = false;
@@ -221,17 +221,23 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
if (!_editTabs.HasActiveFile) if (!_editTabs.HasActiveFile)
return; return;
var path = _editTabs.CurrentAbsolutePath; var modId = _editTabs.CurrentModId;
var relativePath = _editTabs.CurrentRelativePath;
var text = _editTabs.CurrentText; var text = _editTabs.CurrentText;
try try
{ {
ScenarioFileService.WriteAllText(path, text); if (!ScenarioFileService.WriteAllText(modId, relativePath, text))
{
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
return;
}
_editTabs.SaveCurrentFile(); _editTabs.SaveCurrentFile();
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved"); _statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved");
_infoPanel.Update(text); _infoPanel.Update(text);
_thumbPanel.Load(text); _thumbPanel.Load(text);
_editTabs.RefreshGraph(); _editTabs.RefreshGraph();
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {path}"); GameLogger.Log("SCENARIO_EDITOR", $"Saved: {modId}/{relativePath}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -243,10 +249,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
private void OnOpenFilePressed() private void OnOpenFilePressed()
{ {
var path = _editTabs.CurrentAbsolutePath; if (_editTabs.CurrentKey == null)
if (path == null)
return; return;
var path = ScenarioFileService.GetAbsolutePath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath);
if (string.IsNullOrEmpty(path))
{
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
return;
}
GD.Print($"[ScenarioEditor] Opening: {path}"); GD.Print($"[ScenarioEditor] Opening: {path}");
var err = OS.ShellOpen(path); var err = OS.ShellOpen(path);
if (err != Error.Ok) if (err != Error.Ok)
@@ -267,10 +279,21 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
return; return;
} }
var path = _editTabs.CurrentAbsolutePath; var modId = _editTabs.CurrentModId;
var relativePath = _editTabs.CurrentRelativePath;
try try
{ {
ScenarioFileService.WriteAllText(path, text); if (ScenarioFileService.WriteAllText(modId, relativePath, text))
{
_editTabs.SaveCurrentFile();
}
else
{
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
GameLogger.Log("SCENARIO_EDITOR",
$"Test play: '{modId}/{relativePath}' is read-only, playing from the editor buffer",
LogLevel.Warning);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -279,8 +302,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
return; return;
} }
_editTabs.SaveCurrentFile();
_editLayout.Visible = false; _editLayout.Visible = false;
_playLayout.Visible = true; _playLayout.Visible = true;
+12 -11
View File
@@ -8,8 +8,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
public partial class ScenarioFileTreePanel : VBoxContainer public partial class ScenarioFileTreePanel : VBoxContainer
{ {
private Tree _fileTree; private Tree _fileTree;
private readonly Dictionary<string, string> _fileItemToPath = new(); private readonly Dictionary<string, string> _fileItemToKey = new();
private readonly Dictionary<string, TreeItem> _pathToTreeItem = new(); private readonly Dictionary<string, TreeItem> _keyToTreeItem = new();
private bool _isProgrammaticSelection; private bool _isProgrammaticSelection;
public event Action<string> FileSelected; public event Action<string> FileSelected;
@@ -25,7 +25,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
private void Populate() private void Populate()
{ {
_fileTree.Clear(); _fileTree.Clear();
_fileItemToPath.Clear(); _fileItemToKey.Clear();
_keyToTreeItem.Clear();
var root = _fileTree.CreateItem(); var root = _fileTree.CreateItem();
var mods = ScenarioFileService.ScanScenarioFiles(); var mods = ScenarioFileService.ScanScenarioFiles();
@@ -56,8 +57,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
var fileItem = _fileTree.CreateItem(parentItem); var fileItem = _fileTree.CreateItem(parentItem);
fileItem.SetText(0, file.Name); fileItem.SetText(0, file.Name);
fileItem.SetMetadata(0, Variant.From("scenario")); fileItem.SetMetadata(0, Variant.From("scenario"));
_fileItemToPath[GetItemKey(fileItem)] = file.AbsolutePath; _fileItemToKey[GetItemKey(fileItem)] = file.Key;
_pathToTreeItem[file.AbsolutePath] = fileItem; _keyToTreeItem[file.Key] = fileItem;
} }
} }
} }
@@ -68,16 +69,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
return item.GetInstanceId().ToString(); return item.GetInstanceId().ToString();
} }
public void SelectFile(string absolutePath) public void SelectFile(string key)
{ {
if (!_pathToTreeItem.TryGetValue(absolutePath, out var item)) if (!_keyToTreeItem.TryGetValue(key, out var item))
return; return;
_isProgrammaticSelection = true; _isProgrammaticSelection = true;
item.Select(0); item.Select(0);
_isProgrammaticSelection = false; _isProgrammaticSelection = false;
FileSelected?.Invoke(absolutePath); FileSelected?.Invoke(key);
} }
private void OnCellSelected() private void OnCellSelected()
@@ -93,11 +94,11 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario") if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario")
return; return;
var key = GetItemKey(item); var itemKey = GetItemKey(item);
if (!_fileItemToPath.TryGetValue(key, out var filePath)) if (!_fileItemToKey.TryGetValue(itemKey, out var fileKey))
return; return;
FileSelected?.Invoke(filePath); FileSelected?.Invoke(fileKey);
} }
} }
} }
+1 -1
View File
@@ -190,7 +190,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority)) foreach (var sc in nodeData.Scenarios.OrderBy(s => s.Priority))
{ {
var url = Uri.EscapeDataString(sc.FilePath); var url = Uri.EscapeDataString(sc.Key);
sb.AppendLine($" [{sc.Priority}] [url={url}]{sc.Name}[/url]"); sb.AppendLine($" [{sc.Priority}] [url={url}]{sc.Name}[/url]");
} }
+124 -104
View File
@@ -6,7 +6,6 @@ using Cthangover.Core.Scenarios;
using Cthangover.Core.Factories.Impls; using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
using Cthangover.Core.Mods; using Cthangover.Core.Mods;
using Godot;
namespace Cthangover.Tools.Services namespace Cthangover.Tools.Services
{ {
@@ -34,15 +33,24 @@ namespace Cthangover.Tools.Services
public List<string> BackgroundRefs { get; set; } = new(); public List<string> BackgroundRefs { get; set; } = new();
} }
/// <summary>Describes a single scenario file found during scanning.</summary> /// <summary>
/// Describes a single scenario file found during scanning. The file is
/// addressed by its owning mod and mod-relative path, so both folder
/// mods and zip mods are covered; <see cref="Key"/> is the stable
/// identity used by the editor UI.
/// </summary>
public sealed class ScenarioFileInfo public sealed class ScenarioFileInfo
{ {
/// <summary>Absolute path on disk.</summary> /// <summary>Identifier of the mod that provides the file.</summary>
public string AbsolutePath { get; set; } public string ModId { get; set; }
/// <summary>Mod-relative path, e.g. <c>scenarios/start.scenario</c>.</summary>
public string RelativePath { get; set; }
/// <summary>Filename without extension.</summary> /// <summary>Filename without extension.</summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>Directory path relative to the mod's <c>scenarios/</c> folder.</summary> /// <summary>Directory path relative to the mod's <c>scenarios/</c> folder.</summary>
public string RelativeDir { get; set; } public string RelativeDir { get; set; }
/// <summary>Stable UI identity: <c>modId:relativePath</c>.</summary>
public string Key => ScenarioFileService.MakeKey(ModId, RelativePath);
} }
/// <summary>A group of scenario files within the same subdirectory under <c>scenarios/</c>.</summary> /// <summary>A group of scenario files within the same subdirectory under <c>scenarios/</c>.</summary>
@@ -65,67 +73,107 @@ namespace Cthangover.Tools.Services
/// <summary> /// <summary>
/// Static service for discovering, reading, writing, and analysing scenario files. /// Static service for discovering, reading, writing, and analysing scenario files.
/// Scans all <c>mods/*/scenarios/**/*.scenario</c> files in the project, groups /// Scans every loaded mod's <c>scenarios/</c> folder through the mod file
/// them by mod and subdirectory, parses YAML-like metadata blocks, and extracts /// provider abstraction (<see cref="IModFileProvider"/>), so folder and zip
/// referenced background/scene/locale/quest identifiers. Used by /// mods are discovered identically. Writing is only possible for folder mods:
/// <see cref="ScenarioEditorWindow"/> for the file tree and info panel. /// zip mods are read-only, and <see cref="WriteAllText"/> reports that instead
/// of touching the filesystem. Used by <see cref="ScenarioEditorWindow"/> for
/// the file tree and info panel.
/// </summary> /// </summary>
public static class ScenarioFileService public static class ScenarioFileService
{ {
private const string ScenariosDir = "scenarios";
private const string ScenarioExtension = ".scenario";
private const string KeySeparator = ":";
/// <summary> /// <summary>
/// Recursively scans <c>res://mods/*/scenarios/</c> for <c>.scenario</c> files. /// Recursively scans every loaded mod's <c>scenarios/</c> folder for
/// Groups results by mod ID and subdirectory. Returns an empty list if the /// <c>.scenario</c> files via <see cref="IModFileProvider"/>. Groups
/// mods directory does not exist. /// results by mod ID and subdirectory. Returns an empty list when no
/// mods are loaded or none provide scenarios.
/// </summary> /// </summary>
public static List<ModScenarioTree> ScanScenarioFiles() public static List<ModScenarioTree> ScanScenarioFiles()
{ {
var result = new List<ModScenarioTree>(); var result = new List<ModScenarioTree>();
var roots = ModRegistry.AllModsRoots; var manager = ModManager.Instance;
foreach (var (modsPath, _) in roots) foreach (var mod in manager.Mods.Values.OrderBy(m => m.Id, StringComparer.OrdinalIgnoreCase))
{ {
if (!Directory.Exists(modsPath)) var files = new List<ScenarioFileInfo>();
CollectScenarioFiles(manager, mod.Id, ScenariosDir, files);
if (files.Count == 0)
continue; continue;
foreach (var modDir in Directory.GetDirectories(modsPath).OrderBy(d => Path.GetFileName(d))) var groups = files
{ .GroupBy(f => f.RelativeDir)
var modId = Path.GetFileName(modDir); .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
var scenariosDir = Path.Combine(modDir, "scenarios");
if (!Directory.Exists(scenariosDir))
continue;
var scenarioFiles = Directory.GetFiles(scenariosDir, "*.scenario", SearchOption.AllDirectories);
if (scenarioFiles.Length == 0)
continue;
var groups = scenarioFiles
.GroupBy(f =>
{
var relDir = Path.GetRelativePath(scenariosDir, Path.GetDirectoryName(f));
return relDir == "." ? "" : relDir.Replace('\\', '/');
})
.OrderBy(g => g.Key)
.Select(g => new ScenarioGroup .Select(g => new ScenarioGroup
{ {
DirectoryPath = g.Key, DirectoryPath = g.Key,
Files = g.OrderBy(f => Path.GetFileNameWithoutExtension(f)) Files = g.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase).ToList()
.Select(f => new ScenarioFileInfo
{
AbsolutePath = f,
Name = Path.GetFileNameWithoutExtension(f),
RelativeDir = g.Key
})
.ToList()
}) })
.ToList(); .ToList();
result.Add(new ModScenarioTree { ModId = modId, Groups = groups }); result.Add(new ModScenarioTree { ModId = mod.Id, Groups = groups });
}
} }
return result; return result;
} }
private static void CollectScenarioFiles(ModManager manager, string modId, string directory, List<ScenarioFileInfo> result)
{
foreach (var entry in manager.ListFiles(modId, directory))
{
if (entry.EndsWith("/"))
{
CollectScenarioFiles(manager, modId, entry.TrimEnd('/'), result);
continue;
}
if (!entry.EndsWith(ScenarioExtension, StringComparison.OrdinalIgnoreCase))
continue;
var path = entry.Replace('\\', '/');
var lastSlash = path.LastIndexOf('/');
var fileName = lastSlash >= 0 ? path.Substring(lastSlash + 1) : path;
var dirPath = lastSlash >= 0 ? path.Substring(0, lastSlash) : "";
var relativeDir = "";
if (dirPath.StartsWith(ScenariosDir + "/", StringComparison.OrdinalIgnoreCase))
relativeDir = dirPath.Substring(ScenariosDir.Length + 1);
else if (!dirPath.Equals(ScenariosDir, StringComparison.OrdinalIgnoreCase))
relativeDir = dirPath;
result.Add(new ScenarioFileInfo
{
ModId = modId,
RelativePath = path,
Name = Path.GetFileNameWithoutExtension(fileName),
RelativeDir = relativeDir
});
}
}
/// <summary>Builds the stable UI identity for a scenario file.</summary>
public static string MakeKey(string modId, string relativePath) => modId + KeySeparator + relativePath;
/// <summary>Splits a key produced by <see cref="MakeKey"/> back into mod ID and mod-relative path.</summary>
public static bool TryParseKey(string key, out string modId, out string relativePath)
{
modId = null;
relativePath = null;
if (string.IsNullOrEmpty(key))
return false;
var separator = key.IndexOf(KeySeparator, StringComparison.Ordinal);
if (separator <= 0 || separator >= key.Length - 1)
return false;
modId = key.Substring(0, separator);
relativePath = key.Substring(separator + 1);
return true;
}
/// <summary> /// <summary>
/// Parses the header block (before first <c>---</c> line) in YAML-like /// Parses the header block (before first <c>---</c> line) in YAML-like
/// <c>key: value</c> format to extract scene, priority, and condition. /// <c>key: value</c> format to extract scene, priority, and condition.
@@ -208,44 +256,53 @@ namespace Cthangover.Tools.Services
return refs; return refs;
} }
/// <summary>Reads the entire file at <paramref name="filePath"/> as a UTF-8 string.</summary> /// <summary>Reads a scenario file through its mod provider. Returns <c>null</c> when not found.</summary>
public static string ReadAllText(string filePath) public static string ReadAllText(string modId, string relativePath)
{ {
return File.ReadAllText(filePath); return ModManager.Instance.ReadFileText(modId, relativePath);
} }
/// <summary>Writes <paramref name="text"/> to <paramref name="filePath"/>, overwriting existing content.</summary> /// <summary>
public static void WriteAllText(string filePath, string text) /// Writes a scenario file back to disk. Returns <c>false</c> when the
/// owning mod is not filesystem-backed (zip mods are read-only).
/// </summary>
public static bool WriteAllText(string modId, string relativePath, string text)
{ {
File.WriteAllText(filePath, text); var absolutePath = ModManager.Instance.GetFileSystemPath(modId, relativePath);
if (string.IsNullOrEmpty(absolutePath))
return false;
File.WriteAllText(absolutePath, text);
return true;
} }
/// <summary>Converts an absolute file path to a path relative to <c>res://</c>, using forward slashes.</summary> /// <summary>Absolute filesystem path of a scenario file, or <c>null</c> for zip-backed mods.</summary>
public static string GetRelativePath(string absolutePath) public static string GetAbsolutePath(string modId, string relativePath)
{ {
var projectPath = GameEnvironment.IsRuntime ? ProjectSettings.GlobalizePath("res://") : ""; return ModManager.Instance.GetFileSystemPath(modId, relativePath);
return Path.GetRelativePath(projectPath, absolutePath).Replace('\\', '/'); }
/// <summary>Project-style display path (<c>mods/&lt;modId&gt;/&lt;relativePath&gt;</c>).</summary>
public static string GetDisplayPath(string modId, string relativePath)
{
return "mods/" + modId + "/" + relativePath;
} }
public static SceneGraphData BuildSceneGraph() public static SceneGraphData BuildSceneGraph()
{ {
var data = new SceneGraphData(); var data = new SceneGraphData();
var sceneNodes = new Dictionary<string, SceneGraphNode>(System.StringComparer.OrdinalIgnoreCase); var sceneNodes = new Dictionary<string, SceneGraphNode>(StringComparer.OrdinalIgnoreCase);
var edgeSet = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase); var edgeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var allFiles = ScanAllScenarioFiles(); foreach (var mod in ScanScenarioFiles())
foreach (var file in allFiles)
{ {
string text; foreach (var group in mod.Groups)
try
{ {
text = File.ReadAllText(file.AbsolutePath); foreach (var file in group.Files)
}
catch
{ {
var text = ReadAllText(file.ModId, file.RelativePath);
if (text == null)
continue; continue;
}
var metadata = ParseMetadata(text); var metadata = ParseMetadata(text);
if (string.IsNullOrEmpty(metadata.Scene) || metadata.Scene == "-") if (string.IsNullOrEmpty(metadata.Scene) || metadata.Scene == "-")
@@ -259,7 +316,7 @@ namespace Cthangover.Tools.Services
{ {
SceneName = metadata.Scene, SceneName = metadata.Scene,
ModId = file.ModId, ModId = file.ModId,
IsStartScene = string.Equals(metadata.Scene, "start_scene", System.StringComparison.OrdinalIgnoreCase) IsStartScene = string.Equals(metadata.Scene, "start_scene", StringComparison.OrdinalIgnoreCase)
}; };
sceneNodes[sceneKey] = node; sceneNodes[sceneKey] = node;
} }
@@ -269,7 +326,7 @@ namespace Cthangover.Tools.Services
{ {
Name = file.Name, Name = file.Name,
Priority = int.TryParse(metadata.Priority, out var p) ? p : 0, Priority = int.TryParse(metadata.Priority, out var p) ? p : 0,
FilePath = file.AbsolutePath Key = file.Key
}); });
var refs = ExtractReferences(text); var refs = ExtractReferences(text);
@@ -289,50 +346,13 @@ namespace Cthangover.Tools.Services
} }
} }
} }
}
}
foreach (var node in sceneNodes.Values) foreach (var node in sceneNodes.Values)
data.Nodes.Add(node); data.Nodes.Add(node);
return data; return data;
} }
private sealed class FlatScenarioFile
{
public string AbsolutePath;
public string Name;
public string ModId;
}
private static List<FlatScenarioFile> ScanAllScenarioFiles()
{
var result = new List<FlatScenarioFile>();
var roots = ModRegistry.AllModsRoots;
foreach (var (modsPath, _) in roots)
{
if (!Directory.Exists(modsPath))
continue;
foreach (var modDir in Directory.GetDirectories(modsPath))
{
var modId = Path.GetFileName(modDir);
var scenariosDir = Path.Combine(modDir, "scenarios");
if (!Directory.Exists(scenariosDir))
continue;
foreach (var file in Directory.GetFiles(scenariosDir, "*.scenario", SearchOption.AllDirectories))
{
result.Add(new FlatScenarioFile
{
AbsolutePath = file,
Name = Path.GetFileNameWithoutExtension(file),
ModId = modId
});
}
}
}
return result;
}
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Cthangover.Tools.Services
{ {
public string Name { get; set; } public string Name { get; set; }
public int Priority { get; set; } public int Priority { get; set; }
public string FilePath { get; set; } public string Key { get; set; }
} }
public sealed class SceneGraphNode public sealed class SceneGraphNode