From c33545acdf54088b04b669198b4c6051cc45c3df Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 Jan 2024 19:02:50 +0100 Subject: [PATCH] Rework game path selection a bit. --- .../ModEditWindow.Models.MdlTab.cs | 63 +++++++++------ .../UI/AdvancedWindow/ModEditWindow.Models.cs | 79 +++++++++++++------ 2 files changed, 93 insertions(+), 49 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index 20a4129d..93e674ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -12,27 +12,29 @@ public partial class ModEditWindow { private ModEditWindow _edit; - public readonly MdlFile Mdl; + public readonly MdlFile Mdl; private readonly List[] _attributes; - public List? GamePaths { get; private set ;} - public int GamePathIndex; - - public bool PendingIo { get; private set; } = false; + public List? GamePaths { get; private set; } + public int GamePathIndex; + + public bool PendingIo { get; private set; } = false; public string? IoException { get; private set; } = null; [GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] private static partial Regex CharaEquipmentRegex(); - [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)] + [GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", + RegexOptions.Compiled)] private static partial Regex CharaHumanRegex(); - [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", RegexOptions.Compiled)] + [GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", + RegexOptions.Compiled)] private static partial Regex CharaBodyRegex(); public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod) { - _edit = edit; + _edit = edit; Mdl = new MdlFile(bytes); _attributes = CreateAttributes(Mdl); @@ -54,21 +56,29 @@ public partial class ModEditWindow /// Mod within which the .mdl is resolved. private void FindGamePaths(string path, Mod mod) { + if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p)) + { + GamePaths = [p]; + return; + } + PendingIo = true; - var task = Task.Run(() => { + var task = Task.Run(() => + { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? - // NOTE: We're using case insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. + // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. return mod.AllSubMods - .SelectMany(submod => submod.Files.Concat(submod.FileSwaps)) + .SelectMany(m => m.Files.Concat(m.FileSwaps)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Select(kv => kv.Key) .ToList(); }); - task.ContinueWith(task => { - IoException = task.Exception?.ToString(); - PendingIo = false; - GamePaths = task.Result; + task.ContinueWith(t => + { + IoException = t.Exception?.ToString(); + PendingIo = false; + GamePaths = t.Result; }); } @@ -77,19 +87,23 @@ public partial class ModEditWindow public void Export(string outputPath, Utf8GamePath mdlPath) { SklbFile? sklb = null; - try { + try + { var sklbPath = GetSklbPath(mdlPath.ToString()); sklb = sklbPath != null ? ReadSklb(sklbPath) : null; - } catch (Exception exception) { + } + catch (Exception exception) + { IoException = exception?.ToString(); return; } PendingIo = true; _edit._models.ExportToGltf(Mdl, sklb, outputPath) - .ContinueWith(task => { + .ContinueWith(task => + { IoException = task.Exception?.ToString(); - PendingIo = false; + PendingIo = false; }); } @@ -114,7 +128,7 @@ public partial class ModEditWindow return type switch { "body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb", - _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), + _ => throw new Exception($"Currently unsupported human model type \"{type}\"."), }; } @@ -123,7 +137,7 @@ public partial class ModEditWindow if (match.Success) { var subCategory = match.Groups["SubCategory"].Value; - var set = match.Groups["Set"].Value; + var set = match.Groups["Set"].Value; return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb"; } @@ -137,16 +151,17 @@ public partial class ModEditWindow // TODO: if cross-collection lookups are turned off, this conversion can be skipped if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true)) throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path."); - + var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath); // TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so... var bytes = resolvedPath switch { - null => _edit._gameData.GetFile(sklbPath)?.Data, + null => _edit._gameData.GetFile(sklbPath)?.Data, FullPath path => File.ReadAllBytes(path.ToPath()), }; if (bytes == null) - throw new Exception($"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); + throw new Exception( + $"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?"); return new SklbFile(bytes); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index aa69953b..3891eb95 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -20,6 +20,8 @@ public partial class ModEditWindow private string _modelNewMaterial = string.Empty; private readonly List _subMeshAttributeTagWidgets = []; + private string _customPath = string.Empty; + private Utf8GamePath _customGamePath = Utf8GamePath.Empty; private bool DrawModelPanel(MdlTab tab, bool disabled) { @@ -51,10 +53,6 @@ public partial class ModEditWindow private void DrawExport(MdlTab tab, bool disabled) { - // IO on a disabled panel doesn't really make sense. - if (disabled) - return; - if (!ImGui.CollapsingHeader("Export")) return; @@ -70,16 +68,14 @@ public partial class ModEditWindow DrawGamePathCombo(tab); - if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo)) - { - var gamePath = tab.GamePaths[tab.GamePathIndex]; - - _fileDialog.OpenSavePicker( - "Save model as glTF.", - ".gltf", - Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), - ".gltf", - (valid, path) => { + var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count + ? tab.GamePaths[tab.GamePathIndex] + : _customGamePath; + if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", + tab.PendingIo || gamePath.IsEmpty)) + _fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()), + ".gltf", (valid, path) => + { if (!valid) return; @@ -88,27 +84,60 @@ public partial class ModEditWindow _mod!.ModPath.FullName, false ); - } if (tab.IoException != null) ImGuiUtil.TextWrapped(tab.IoException); - - return; } private void DrawGamePathCombo(MdlTab tab) { - using var combo = ImRaii.Combo("Game Path", tab.GamePaths![tab.GamePathIndex].ToString()); - if (!combo) - return; - - foreach (var (path, index) in tab.GamePaths.WithIndex()) + if (tab.GamePaths!.Count == 0) { - if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex)) - continue; + ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting."); + if (ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256)) + if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false)) + _customGamePath = Utf8GamePath.Empty; - tab.GamePathIndex = index; + return; } + + DrawComboButton(tab); + } + + private static void DrawComboButton(MdlTab tab) + { + const string label = "Game Path"; + var preview = tab.GamePaths![tab.GamePathIndex].ToString(); + var labelWidth = ImGui.CalcTextSize(label).X + ImGui.GetStyle().ItemInnerSpacing.X; + var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth; + if (tab.GamePaths!.Count == 1) + { + using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)); + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.FrameBg)) + .Push(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBgHovered)) + .Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBgActive)); + using var group = ImRaii.Group(); + ImGui.Button(preview, new Vector2(buttonWidth, 0)); + ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); + ImGui.TextUnformatted("Game Path"); + } + else + { + ImGui.SetNextItemWidth(buttonWidth); + using var combo = ImRaii.Combo("Game Path", preview); + if (combo.Success) + foreach (var (path, index) in tab.GamePaths.WithIndex()) + { + if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex)) + continue; + + tab.GamePathIndex = index; + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + ImGui.SetClipboardText(preview); + ImGuiUtil.HoverTooltip("Right-Click to copy to clipboard.", ImGuiHoveredFlags.AllowWhenDisabled); } private bool DrawModelMaterialDetails(MdlTab tab, bool disabled)