upd
This commit is contained in:
1 parent
8e6ad52ee4
commit
de310e7295
10 files changed
+236
-173
No files matched your search
@@ -55,7 +55,6 @@ namespace Cthangover.Tools.InteractiveEditor
|
||||
|
||||
public InteractiveEditorWindow() : base("tools/interactive_editor/title")
|
||||
{
|
||||
Title = "Interactive Editor";
|
||||
Size = new Vector2I(1280, 820);
|
||||
|
||||
var outer = CreateFillContainer();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.UI.Lights;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Tools.Services;
|
||||
using Godot;
|
||||
|
||||
@@ -366,8 +368,9 @@ namespace Cthangover.Core.UI.Tool.LightEditor
|
||||
selectedIndex = -1;
|
||||
RefreshAll();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
GameLogger.Log("LIGHT_EDITOR", $"Import JSON failed: {ex.Message}", LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
{
|
||||
public sealed class TabData
|
||||
{
|
||||
public string FilePath;
|
||||
public string Key;
|
||||
public string ModId;
|
||||
public string RelativePath;
|
||||
public string FileName;
|
||||
public string SavedText;
|
||||
public bool IsDirty;
|
||||
@@ -31,8 +33,12 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
public TextEdit CodeEditor => _textEdit;
|
||||
public string CurrentText => _textEdit.Text;
|
||||
public string CurrentAbsolutePath =>
|
||||
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count ? _tabs[_activeTabIndex].FilePath : null;
|
||||
public string CurrentKey =>
|
||||
_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 IsCurrentDirty =>
|
||||
_activeTabIndex >= 0 && _activeTabIndex < _tabs.Count && _tabs[_activeTabIndex].IsDirty;
|
||||
@@ -73,24 +79,35 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
AddChild(_graphView);
|
||||
}
|
||||
|
||||
public void OpenFile(string absolutePath)
|
||||
public void OpenFile(string key)
|
||||
{
|
||||
for (int i = 0; i < _tabs.Count; i++)
|
||||
{
|
||||
if (_tabs[i].FilePath == absolutePath)
|
||||
if (_tabs[i].Key == key)
|
||||
{
|
||||
SwitchToTextTab(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ScenarioFileService.TryParseKey(key, out var modId, out var relativePath))
|
||||
return;
|
||||
|
||||
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
|
||||
{
|
||||
FilePath = absolutePath,
|
||||
FileName = Path.GetFileNameWithoutExtension(absolutePath),
|
||||
Key = key,
|
||||
ModId = modId,
|
||||
RelativePath = relativePath,
|
||||
FileName = Path.GetFileNameWithoutExtension(relativePath),
|
||||
SavedText = text,
|
||||
IsDirty = false
|
||||
};
|
||||
|
||||
@@ -180,8 +180,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
private void OnTextTabSelected(string text)
|
||||
{
|
||||
_filePathLabel.Text = _editTabs.CurrentAbsolutePath != null
|
||||
? ScenarioFileService.GetRelativePath(_editTabs.CurrentAbsolutePath) : "";
|
||||
_filePathLabel.Text = _editTabs.CurrentKey != null
|
||||
? ScenarioFileService.GetDisplayPath(_editTabs.CurrentModId, _editTabs.CurrentRelativePath) : "";
|
||||
_saveBtn.Disabled = false;
|
||||
_openFileBtn.Disabled = false;
|
||||
_playBtn.Disabled = false;
|
||||
@@ -221,17 +221,23 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
if (!_editTabs.HasActiveFile)
|
||||
return;
|
||||
|
||||
var path = _editTabs.CurrentAbsolutePath;
|
||||
var modId = _editTabs.CurrentModId;
|
||||
var relativePath = _editTabs.CurrentRelativePath;
|
||||
var text = _editTabs.CurrentText;
|
||||
try
|
||||
{
|
||||
ScenarioFileService.WriteAllText(path, text);
|
||||
if (!ScenarioFileService.WriteAllText(modId, relativePath, text))
|
||||
{
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/readonly_zip");
|
||||
return;
|
||||
}
|
||||
|
||||
_editTabs.SaveCurrentFile();
|
||||
_statusLabel.Text = TranslationServer.Translate("tools/scenario_editor/status_saved");
|
||||
_infoPanel.Update(text);
|
||||
_thumbPanel.Load(text);
|
||||
_editTabs.RefreshGraph();
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {path}");
|
||||
GameLogger.Log("SCENARIO_EDITOR", $"Saved: {modId}/{relativePath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -243,10 +249,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
private void OnOpenFilePressed()
|
||||
{
|
||||
var path = _editTabs.CurrentAbsolutePath;
|
||||
if (path == null)
|
||||
if (_editTabs.CurrentKey == null)
|
||||
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}");
|
||||
var err = OS.ShellOpen(path);
|
||||
if (err != Error.Ok)
|
||||
@@ -267,10 +279,21 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
}
|
||||
|
||||
var path = _editTabs.CurrentAbsolutePath;
|
||||
var modId = _editTabs.CurrentModId;
|
||||
var relativePath = _editTabs.CurrentRelativePath;
|
||||
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)
|
||||
{
|
||||
@@ -279,8 +302,6 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
return;
|
||||
}
|
||||
|
||||
_editTabs.SaveCurrentFile();
|
||||
|
||||
_editLayout.Visible = false;
|
||||
_playLayout.Visible = true;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
public partial class ScenarioFileTreePanel : VBoxContainer
|
||||
{
|
||||
private Tree _fileTree;
|
||||
private readonly Dictionary<string, string> _fileItemToPath = new();
|
||||
private readonly Dictionary<string, TreeItem> _pathToTreeItem = new();
|
||||
private readonly Dictionary<string, string> _fileItemToKey = new();
|
||||
private readonly Dictionary<string, TreeItem> _keyToTreeItem = new();
|
||||
private bool _isProgrammaticSelection;
|
||||
|
||||
public event Action<string> FileSelected;
|
||||
@@ -25,7 +25,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
private void Populate()
|
||||
{
|
||||
_fileTree.Clear();
|
||||
_fileItemToPath.Clear();
|
||||
_fileItemToKey.Clear();
|
||||
_keyToTreeItem.Clear();
|
||||
|
||||
var root = _fileTree.CreateItem();
|
||||
var mods = ScenarioFileService.ScanScenarioFiles();
|
||||
@@ -56,8 +57,8 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
var fileItem = _fileTree.CreateItem(parentItem);
|
||||
fileItem.SetText(0, file.Name);
|
||||
fileItem.SetMetadata(0, Variant.From("scenario"));
|
||||
_fileItemToPath[GetItemKey(fileItem)] = file.AbsolutePath;
|
||||
_pathToTreeItem[file.AbsolutePath] = fileItem;
|
||||
_fileItemToKey[GetItemKey(fileItem)] = file.Key;
|
||||
_keyToTreeItem[file.Key] = fileItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,16 +69,16 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
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;
|
||||
|
||||
_isProgrammaticSelection = true;
|
||||
item.Select(0);
|
||||
_isProgrammaticSelection = false;
|
||||
|
||||
FileSelected?.Invoke(absolutePath);
|
||||
FileSelected?.Invoke(key);
|
||||
}
|
||||
|
||||
private void OnCellSelected()
|
||||
@@ -93,11 +94,11 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
if (meta.VariantType != Variant.Type.String || meta.AsString() != "scenario")
|
||||
return;
|
||||
|
||||
var key = GetItemKey(item);
|
||||
if (!_fileItemToPath.TryGetValue(key, out var filePath))
|
||||
var itemKey = GetItemKey(item);
|
||||
if (!_fileItemToKey.TryGetValue(itemKey, out var fileKey))
|
||||
return;
|
||||
|
||||
FileSelected?.Invoke(filePath);
|
||||
FileSelected?.Invoke(fileKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor
|
||||
|
||||
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]");
|
||||
}
|
||||
|
||||
|
||||
+159
-139
@@ -6,7 +6,6 @@ using Cthangover.Core.Scenarios;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.Core.Mods;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.Tools.Services
|
||||
{
|
||||
@@ -34,15 +33,24 @@ namespace Cthangover.Tools.Services
|
||||
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
|
||||
{
|
||||
/// <summary>Absolute path on disk.</summary>
|
||||
public string AbsolutePath { get; set; }
|
||||
/// <summary>Identifier of the mod that provides the file.</summary>
|
||||
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>
|
||||
public string Name { get; set; }
|
||||
/// <summary>Directory path relative to the mod's <c>scenarios/</c> folder.</summary>
|
||||
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>
|
||||
@@ -65,67 +73,107 @@ namespace Cthangover.Tools.Services
|
||||
|
||||
/// <summary>
|
||||
/// Static service for discovering, reading, writing, and analysing scenario files.
|
||||
/// Scans all <c>mods/*/scenarios/**/*.scenario</c> files in the project, groups
|
||||
/// them by mod and subdirectory, parses YAML-like metadata blocks, and extracts
|
||||
/// referenced background/scene/locale/quest identifiers. Used by
|
||||
/// <see cref="ScenarioEditorWindow"/> for the file tree and info panel.
|
||||
/// Scans every loaded mod's <c>scenarios/</c> folder through the mod file
|
||||
/// provider abstraction (<see cref="IModFileProvider"/>), so folder and zip
|
||||
/// mods are discovered identically. Writing is only possible for folder mods:
|
||||
/// 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>
|
||||
public static class ScenarioFileService
|
||||
{
|
||||
private const string ScenariosDir = "scenarios";
|
||||
private const string ScenarioExtension = ".scenario";
|
||||
private const string KeySeparator = ":";
|
||||
|
||||
/// <summary>
|
||||
/// Recursively scans <c>res://mods/*/scenarios/</c> for <c>.scenario</c> files.
|
||||
/// Groups results by mod ID and subdirectory. Returns an empty list if the
|
||||
/// mods directory does not exist.
|
||||
/// Recursively scans every loaded mod's <c>scenarios/</c> folder for
|
||||
/// <c>.scenario</c> files via <see cref="IModFileProvider"/>. Groups
|
||||
/// results by mod ID and subdirectory. Returns an empty list when no
|
||||
/// mods are loaded or none provide scenarios.
|
||||
/// </summary>
|
||||
public static List<ModScenarioTree> ScanScenarioFiles()
|
||||
{
|
||||
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;
|
||||
|
||||
foreach (var modDir in Directory.GetDirectories(modsPath).OrderBy(d => Path.GetFileName(d)))
|
||||
{
|
||||
var modId = Path.GetFileName(modDir);
|
||||
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)
|
||||
var groups = files
|
||||
.GroupBy(f => f.RelativeDir)
|
||||
.OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => new ScenarioGroup
|
||||
{
|
||||
DirectoryPath = g.Key,
|
||||
Files = g.OrderBy(f => Path.GetFileNameWithoutExtension(f))
|
||||
.Select(f => new ScenarioFileInfo
|
||||
{
|
||||
AbsolutePath = f,
|
||||
Name = Path.GetFileNameWithoutExtension(f),
|
||||
RelativeDir = g.Key
|
||||
})
|
||||
.ToList()
|
||||
Files = g.OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase).ToList()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
result.Add(new ModScenarioTree { ModId = modId, Groups = groups });
|
||||
}
|
||||
result.Add(new ModScenarioTree { ModId = mod.Id, Groups = groups });
|
||||
}
|
||||
|
||||
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>
|
||||
/// Parses the header block (before first <c>---</c> line) in YAML-like
|
||||
/// <c>key: value</c> format to extract scene, priority, and condition.
|
||||
@@ -208,84 +256,95 @@ namespace Cthangover.Tools.Services
|
||||
return refs;
|
||||
}
|
||||
|
||||
/// <summary>Reads the entire file at <paramref name="filePath"/> as a UTF-8 string.</summary>
|
||||
public static string ReadAllText(string filePath)
|
||||
/// <summary>Reads a scenario file through its mod provider. Returns <c>null</c> when not found.</summary>
|
||||
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>
|
||||
public static void WriteAllText(string filePath, string text)
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public static string GetRelativePath(string absolutePath)
|
||||
/// <summary>Absolute filesystem path of a scenario file, or <c>null</c> for zip-backed mods.</summary>
|
||||
public static string GetAbsolutePath(string modId, string relativePath)
|
||||
{
|
||||
var projectPath = GameEnvironment.IsRuntime ? ProjectSettings.GlobalizePath("res://") : "";
|
||||
return Path.GetRelativePath(projectPath, absolutePath).Replace('\\', '/');
|
||||
return ModManager.Instance.GetFileSystemPath(modId, relativePath);
|
||||
}
|
||||
|
||||
/// <summary>Project-style display path (<c>mods/<modId>/<relativePath></c>).</summary>
|
||||
public static string GetDisplayPath(string modId, string relativePath)
|
||||
{
|
||||
return "mods/" + modId + "/" + relativePath;
|
||||
}
|
||||
|
||||
public static SceneGraphData BuildSceneGraph()
|
||||
{
|
||||
var data = new SceneGraphData();
|
||||
var sceneNodes = new Dictionary<string, SceneGraphNode>(System.StringComparer.OrdinalIgnoreCase);
|
||||
var edgeSet = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||||
var sceneNodes = new Dictionary<string, SceneGraphNode>(StringComparer.OrdinalIgnoreCase);
|
||||
var edgeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var allFiles = ScanAllScenarioFiles();
|
||||
|
||||
foreach (var file in allFiles)
|
||||
foreach (var mod in ScanScenarioFiles())
|
||||
{
|
||||
string text;
|
||||
try
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
text = File.ReadAllText(file.AbsolutePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var metadata = ParseMetadata(text);
|
||||
if (string.IsNullOrEmpty(metadata.Scene) || metadata.Scene == "-")
|
||||
continue;
|
||||
|
||||
var sceneKey = metadata.Scene.ToLowerInvariant();
|
||||
|
||||
if (!sceneNodes.TryGetValue(sceneKey, out var node))
|
||||
{
|
||||
node = new SceneGraphNode
|
||||
foreach (var file in group.Files)
|
||||
{
|
||||
SceneName = metadata.Scene,
|
||||
ModId = file.ModId,
|
||||
IsStartScene = string.Equals(metadata.Scene, "start_scene", System.StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
sceneNodes[sceneKey] = node;
|
||||
}
|
||||
var text = ReadAllText(file.ModId, file.RelativePath);
|
||||
if (text == null)
|
||||
continue;
|
||||
|
||||
node.ScenarioCount++;
|
||||
node.Scenarios.Add(new ScenarioGraphLink
|
||||
{
|
||||
Name = file.Name,
|
||||
Priority = int.TryParse(metadata.Priority, out var p) ? p : 0,
|
||||
FilePath = file.AbsolutePath
|
||||
});
|
||||
var metadata = ParseMetadata(text);
|
||||
if (string.IsNullOrEmpty(metadata.Scene) || metadata.Scene == "-")
|
||||
continue;
|
||||
|
||||
var refs = ExtractReferences(text);
|
||||
foreach (var target in refs.SwitchTargets)
|
||||
{
|
||||
var srcKey = metadata.Scene.ToLowerInvariant();
|
||||
var dstKey = target.ToLowerInvariant();
|
||||
var edgeKey = srcKey + "|" + dstKey;
|
||||
if (!edgeSet.Contains(edgeKey))
|
||||
{
|
||||
edgeSet.Add(edgeKey);
|
||||
data.Edges.Add(new SceneGraphEdge
|
||||
var sceneKey = metadata.Scene.ToLowerInvariant();
|
||||
|
||||
if (!sceneNodes.TryGetValue(sceneKey, out var node))
|
||||
{
|
||||
FromScene = metadata.Scene,
|
||||
ToScene = target
|
||||
node = new SceneGraphNode
|
||||
{
|
||||
SceneName = metadata.Scene,
|
||||
ModId = file.ModId,
|
||||
IsStartScene = string.Equals(metadata.Scene, "start_scene", StringComparison.OrdinalIgnoreCase)
|
||||
};
|
||||
sceneNodes[sceneKey] = node;
|
||||
}
|
||||
|
||||
node.ScenarioCount++;
|
||||
node.Scenarios.Add(new ScenarioGraphLink
|
||||
{
|
||||
Name = file.Name,
|
||||
Priority = int.TryParse(metadata.Priority, out var p) ? p : 0,
|
||||
Key = file.Key
|
||||
});
|
||||
|
||||
var refs = ExtractReferences(text);
|
||||
foreach (var target in refs.SwitchTargets)
|
||||
{
|
||||
var srcKey = metadata.Scene.ToLowerInvariant();
|
||||
var dstKey = target.ToLowerInvariant();
|
||||
var edgeKey = srcKey + "|" + dstKey;
|
||||
if (!edgeSet.Contains(edgeKey))
|
||||
{
|
||||
edgeSet.Add(edgeKey);
|
||||
data.Edges.Add(new SceneGraphEdge
|
||||
{
|
||||
FromScene = metadata.Scene,
|
||||
ToScene = target
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,44 +354,5 @@ namespace Cthangover.Tools.Services
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace Cthangover.Tools.Services
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public string FilePath { get; set; }
|
||||
public string Key { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SceneGraphNode
|
||||
|
||||
Reference in new issue
Block a user