From 54460c39f3eea5ffbf9dac2dea6afda19ae03a87 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 3 May 2022 18:15:35 +0200 Subject: [PATCH] Add more edit options, some small fixes. --- Penumbra.GameData/Enums/Race.cs | 8 +- .../Interop/Loader/ResourceLoader.Debug.cs | 30 +- Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs | 12 +- Penumbra/Mods/Editor/Mod.Editor.Edit.cs | 42 +- .../Mods/Editor/Mod.Editor.MdlMaterials.cs | 179 +++++++++ Penumbra/Mods/Editor/Mod.Editor.cs | 8 +- Penumbra/Mods/Mod.Creation.cs | 2 +- Penumbra/UI/Classes/Colors.cs | 1 + Penumbra/UI/Classes/ModEditWindow.cs | 372 +++++++++++++++++- Penumbra/UI/ConfigWindow.ModPanel.Edit.cs | 124 +++--- Penumbra/UI/ConfigWindow.ModsTab.cs | 35 +- Penumbra/UI/ConfigWindow.cs | 17 +- Penumbra/Util/ModelChanger.cs | 94 ----- 13 files changed, 697 insertions(+), 227 deletions(-) create mode 100644 Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs delete mode 100644 Penumbra/Util/ModelChanger.cs diff --git a/Penumbra.GameData/Enums/Race.cs b/Penumbra.GameData/Enums/Race.cs index 47575a31..ba8f6337 100644 --- a/Penumbra.GameData/Enums/Race.cs +++ b/Penumbra.GameData/Enums/Race.cs @@ -163,7 +163,7 @@ public static class RaceEnumExtensions ModelRace.AuRa => Race.AuRa.ToName(), ModelRace.Hrothgar => Race.Hrothgar.ToName(), ModelRace.Viera => Race.Viera.ToName(), - _ => throw new ArgumentOutOfRangeException( nameof( modelRace ), modelRace, null ), + _ => Race.Unknown.ToName(), }; } @@ -179,7 +179,7 @@ public static class RaceEnumExtensions Race.AuRa => "Au Ra", Race.Hrothgar => "Hrothgar", Race.Viera => "Viera", - _ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), + _ => "Unknown", }; } @@ -191,7 +191,7 @@ public static class RaceEnumExtensions Gender.Female => "Female", Gender.MaleNpc => "Male (NPC)", Gender.FemaleNpc => "Female (NPC)", - _ => throw new InvalidEnumArgumentException(), + _ => "Unknown", }; } @@ -215,7 +215,7 @@ public static class RaceEnumExtensions SubRace.Lost => "Lost", SubRace.Rava => "Rava", SubRace.Veena => "Veena", - _ => throw new InvalidEnumArgumentException(), + _ => "Unknown", }; } diff --git a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs index e0dad891..3be1b0ec 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Debug.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Debug.cs @@ -52,18 +52,26 @@ public unsafe partial class ResourceLoader return; } - var crc = ( uint )originalPath.Path.Crc32; - var originalResource = FindResource( handle->Category, handle->FileType, crc ); - _debugList[ manipulatedPath.Value ] = new DebugData() + // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra. + try { - OriginalResource = ( Structs.ResourceHandle* )originalResource, - ManipulatedResource = handle, - Category = handle->Category, - Extension = handle->FileType, - OriginalPath = originalPath.Clone(), - ManipulatedPath = manipulatedPath.Value, - ResolverInfo = resolverInfo, - }; + var crc = ( uint )originalPath.Path.Crc32; + var originalResource = FindResource( handle->Category, handle->FileType, crc ); + _debugList[ manipulatedPath.Value ] = new DebugData() + { + OriginalResource = ( Structs.ResourceHandle* )originalResource, + ManipulatedResource = handle, + Category = handle->Category, + Extension = handle->FileType, + OriginalPath = originalPath.Clone(), + ManipulatedPath = manipulatedPath.Value, + ResolverInfo = resolverInfo, + }; + } + catch( Exception e ) + { + PluginLog.Error( e.ToString() ); + } } // Find a key in a StdMap. diff --git a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs index e8225db4..3f217fab 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Duplicates.cs @@ -48,11 +48,6 @@ public partial class Mod _duplicates.Clear(); } - public void Cancel() - { - DuplicatesFinished = true; - } - private void HandleDuplicate( FullPath duplicate, FullPath remaining ) { void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) @@ -94,8 +89,11 @@ public partial class Mod public void StartDuplicateCheck() { - DuplicatesFinished = false; - Task.Run( CheckDuplicates ); + if( DuplicatesFinished ) + { + DuplicatesFinished = false; + Task.Run( CheckDuplicates ); + } } private void CheckDuplicates() diff --git a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs index 65596021..e0378d26 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Edit.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Edit.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manipulations; using Penumbra.Util; @@ -9,20 +10,23 @@ public partial class Mod { public partial class Editor { - private int _groupIdx = -1; - private int _optionIdx = 0; + public int GroupIdx { get; private set; } = -1; + public int OptionIdx { get; private set; } = 0; private IModGroup? _modGroup; private SubMod _subMod; + public ISubMod CurrentOption + => _subMod; + public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); public readonly HashSet< MetaManipulation > CurrentManipulations = new(); public void SetSubMod( int groupIdx, int optionIdx ) { - _groupIdx = groupIdx; - _optionIdx = optionIdx; + GroupIdx = groupIdx; + OptionIdx = optionIdx; if( groupIdx >= 0 ) { _modGroup = _mod.Groups[ groupIdx ]; @@ -34,8 +38,38 @@ public partial class Mod _subMod = _mod._default; } + RevertFiles(); + RevertSwaps(); + RevertManipulations(); + } + + public void ApplyFiles() + { + Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, CurrentFiles.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + } + + public void RevertFiles() + { CurrentFiles.SetTo( _subMod.Files ); + } + + public void ApplySwaps() + { + Penumbra.ModManager.OptionSetFileSwaps( _mod, GroupIdx, OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) ); + } + + public void RevertSwaps() + { CurrentSwaps.SetTo( _subMod.FileSwaps ); + } + + public void ApplyManipulations() + { + Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() ); + } + + public void RevertManipulations() + { CurrentManipulations.Clear(); CurrentManipulations.UnionWith( _subMod.Manipulations ); } diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs new file mode 100644 index 00000000..5e87b2dd --- /dev/null +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public partial class Mod +{ + public partial class Editor + { + private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled); + private readonly List< MaterialInfo > _modelFiles = new(); + public IReadOnlyList< MaterialInfo > ModelFiles + => _modelFiles; + + // Non-ASCII encoding can not be used. + public static bool ValidString( string to ) + => to.Length != 0 + && to.Length < 16 + && Encoding.UTF8.GetByteCount( to ) == to.Length; + + public void SaveAllModels() + { + foreach( var info in _modelFiles ) + { + info.Save(); + } + } + + public void RestoreAllModels() + { + foreach( var info in _modelFiles ) + { + info.Restore(); + } + } + + // Go through the currently loaded files and replace all appropriate suffices. + // Does nothing if toSuffix is invalid. + // If raceCode is Unknown, apply to all raceCodes. + // If fromSuffix is empty, apply to all suffices. + public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown ) + { + if( !ValidString( toSuffix ) ) + return; + + foreach( var info in _modelFiles ) + { + for( var i = 0; i < info.Count; ++i ) + { + var (_, def) = info[ i ]; + var match = MaterialRegex.Match( def ); + if( match.Success + && ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value ) + && ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) ) + { + info.SetMaterial( $"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i ); + } + } + } + } + + // Find all model files in the mod that contain skin materials. + private void ScanModels() + { + _modelFiles.Clear(); + foreach( var (file, _) in AvailableFiles.Where( f => f.Item1.Extension == ".mdl" ) ) + { + try + { + var bytes = File.ReadAllBytes( file.FullName ); + var mdlFile = new MdlFile( bytes ); + var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) ) + .Select( p => p.Item2 ).ToArray(); + if( materials.Length > 0 ) + { + _modelFiles.Add( new MaterialInfo( file, mdlFile, materials ) ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.FullName} for materials:\n{e}" ); + } + } + } + + // A class that collects information about skin materials in a model file and handle changes on them. + public class MaterialInfo + { + public readonly FullPath Path; + private readonly MdlFile _file; + private readonly string[] _currentMaterials; + private readonly IReadOnlyList _materialIndices; + public bool Changed { get; private set; } = false; + + public IReadOnlyList CurrentMaterials + => _currentMaterials; + + private IEnumerable DefaultMaterials + => _materialIndices.Select( i => _file.Materials[i] ); + + public (string Current, string Default) this[int idx] + => (_currentMaterials[idx], _file.Materials[_materialIndices[idx]]); + + public int Count + => _materialIndices.Count; + + // Set the skin material to a new value and flag changes appropriately. + public void SetMaterial( string value, int materialIdx ) + { + var mat = _file.Materials[_materialIndices[materialIdx]]; + _currentMaterials[materialIdx] = value; + if( mat != value ) + { + Changed = true; + } + else + { + Changed = !_currentMaterials.SequenceEqual( DefaultMaterials ); + } + } + + // Save a changed .mdl file. + public void Save() + { + if( !Changed ) + { + return; + } + + foreach( var (idx, i) in _materialIndices.WithIndex() ) + { + _file.Materials[idx] = _currentMaterials[i]; + } + + try + { + File.WriteAllBytes( Path.FullName, _file.Write() ); + Changed = false; + } + catch( Exception e ) + { + Restore(); + PluginLog.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" ); + } + } + + // Revert all current changes. + public void Restore() + { + if( !Changed ) + return; + + foreach( var (idx, i) in _materialIndices.WithIndex() ) + { + _currentMaterials[i] = _file.Materials[idx]; + } + Changed = false; + } + + public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList indices ) + { + Path = path; + _file = file; + _materialIndices = indices; + _currentMaterials = DefaultMaterials.ToArray(); + } + } + + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.cs b/Penumbra/Mods/Editor/Mod.Editor.cs index 9007bb2d..d69c32f0 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.cs @@ -21,13 +21,17 @@ public partial class Mod _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); _subMod = _mod._default; + ScanModels(); } - public void Dispose() + public void Cancel() { DuplicatesFinished = true; } + public void Dispose() + => Cancel(); + // Does not delete the base directory itself even if it is completely empty at the end. private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) { @@ -49,7 +53,7 @@ public partial class Mod { for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) { - action( @group[ optionIdx ], groupIdx, optionIdx ); + action( group[ optionIdx ], groupIdx, optionIdx ); } } } diff --git a/Penumbra/Mods/Mod.Creation.cs b/Penumbra/Mods/Mod.Creation.cs index d3f8b33d..c6387fc6 100644 --- a/Penumbra/Mods/Mod.Creation.cs +++ b/Penumbra/Mods/Mod.Creation.cs @@ -18,7 +18,7 @@ public partial class Mod // - Containing no symbols invalid for FFXIV or windows paths. internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) { - var name = Path.GetFileNameWithoutExtension( modListName ); + var name = modListName; if( name.Length == 0 ) { name = "_"; diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 0a16758e..fd55b3bb 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -23,6 +23,7 @@ public static class Colors public const uint PressEnterWarningBg = 0xFF202080; public const uint RegexWarningBorder = 0xFF0000B0; public const uint MetaInfoText = 0xAAFFFFFF; + public const uint RedTableBgTint = 0x40000080; public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 46f19f69..630c07bf 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,13 +1,18 @@ using System; +using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; +using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using ImGuiNET; using OtterGui; using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI.Classes; @@ -28,6 +33,11 @@ public class ModEditWindow : Window, IDisposable _editor = new Mod.Editor( mod ); _mod = mod; WindowName = $"{mod.Name}{WindowBaseLabel}"; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = ImGuiHelpers.ScaledVector2( 800, 600 ), + MaximumSize = 4000 * Vector2.One, + }; } public void ChangeOption( int groupIdx, int optionIdx ) @@ -50,6 +60,168 @@ public class ModEditWindow : Window, IDisposable DrawMissingFilesTab(); DrawUnusedFilesTab(); DrawDuplicatesTab(); + DrawMaterialChangeTab(); + } + + // A row of three buttonSizes and a help marker that can be used for material suffix changing. + private static class MaterialSuffix + { + private static string _materialSuffixFrom = string.Empty; + private static string _materialSuffixTo = string.Empty; + private static GenderRace _raceCode = GenderRace.Unknown; + + private static string RaceCodeName( GenderRace raceCode ) + { + if( raceCode == GenderRace.Unknown ) + { + return "All Races and Genders"; + } + + var (gender, race) = raceCode.Split(); + return $"({raceCode.ToRaceCode()}) {race.ToName()} {gender.ToName()} "; + } + + private static void DrawRaceCodeCombo( Vector2 buttonSize ) + { + ImGui.SetNextItemWidth( buttonSize.X ); + using var combo = ImRaii.Combo( "##RaceCode", RaceCodeName( _raceCode ) ); + if( !combo ) + { + return; + } + + foreach( var raceCode in Enum.GetValues< GenderRace >() ) + { + if( ImGui.Selectable( RaceCodeName( raceCode ), _raceCode == raceCode ) ) + { + _raceCode = raceCode; + } + } + } + + public static void Draw( Mod.Editor editor, Vector2 buttonSize ) + { + DrawRaceCodeCombo( buttonSize ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( buttonSize.X ); + ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SameLine(); + var disabled = !Mod.Editor.ValidString( _materialSuffixTo ); + var tt = _materialSuffixTo.Length == 0 + ? "Please enter a target suffix." + : _materialSuffixFrom == _materialSuffixTo + ? "The source and target are identical." + : disabled + ? "The suffix is invalid." + : _materialSuffixFrom.Length == 0 + ? _raceCode == GenderRace.Unknown ? "Convert all skin material suffices to the target." + : "Convert all skin material suffices for the given race code to the target." + : _raceCode == GenderRace.Unknown + ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." + : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; + if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) + { + editor.ReplaceAllMaterials( _materialSuffixTo, _materialSuffixFrom, _raceCode ); + } + + var anyChanges = editor.ModelFiles.Any( m => m.Changed ); + if( ImGuiUtil.DrawDisabledButton( "Save All Changes", buttonSize, + anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges ) ) + { + editor.SaveAllModels(); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Revert All Changes", buttonSize, + anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges ) ) + { + editor.RestoreAllModels(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + } + } + + private void DrawMaterialChangeTab() + { + using var tab = ImRaii.TabItem( "Model Materials" ); + if( !tab ) + { + return; + } + + if( _editor!.ModelFiles.Count == 0 ) + { + ImGui.NewLine(); + ImGui.TextUnformatted( "No .mdl files detected." ); + } + else + { + ImGui.NewLine(); + MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + ImGui.NewLine(); + using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) + { + using var id = ImRaii.PushId( idx ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) + { + info.Save(); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true ) ) + { + info.Restore(); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + var tmp = info.CurrentMaterials[ 0 ]; + if( ImGui.InputText( "##0", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, 0 ); + } + + for( var i = 1; i < info.Count; ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + tmp = info.CurrentMaterials[ i ]; + if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, i ); + } + } + } + } } private void DrawMissingFilesTab() @@ -62,6 +234,7 @@ public class ModEditWindow : Window, IDisposable if( _editor!.MissingPaths.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No missing files detected." ); } else @@ -71,6 +244,12 @@ public class ModEditWindow : Window, IDisposable _editor.RemoveMissingPaths(); } + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); if( !table ) { @@ -113,7 +292,9 @@ public class ModEditWindow : Window, IDisposable if( _editor.Duplicates.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No duplicates found." ); + return; } if( ImGui.Button( "Delete and Redirect Duplicates" ) ) @@ -173,12 +354,68 @@ public class ModEditWindow : Window, IDisposable foreach( var duplicate in set.Skip( 1 ) ) { ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); + ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + } + } + } + + private void DrawOptionSelectHeader() + { + const string defaultOption = "Default Option"; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); + var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); + var isDefaultOption = _editor!.GroupIdx == -1 && _editor!.OptionIdx == 0; + if( ImGuiUtil.DrawDisabledButton( defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", + isDefaultOption ) ) + { + _editor.SetSubMod( -1, 0 ); + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( "Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false ) ) + { + _editor.SetSubMod( _editor.GroupIdx, _editor.OptionIdx ); + } + + ImGui.SameLine(); + + string GetLabel() + { + if( isDefaultOption ) + { + return defaultOption; + } + + var group = _mod!.Groups[ _editor!.GroupIdx ]; + return $"{group.Name}: {group[ _editor.OptionIdx ].Name}"; + } + + var groupLabel = GetLabel(); + using var combo = ImRaii.Combo( "##optionSelector", groupLabel, ImGuiComboFlags.NoArrowButton ); + if( !combo ) + { + return; + } + + if( ImGui.Selectable( $"{defaultOption}###-1_0", isDefaultOption ) ) + { + _editor.SetSubMod( -1, 0 ); + } + + foreach( var (group, groupIdx) in _mod!.Groups.WithIndex() ) + { + foreach( var (option, optionIdx) in group.WithIndex() ) + { + var name = $"{group.Name}: {option.Name}###{groupIdx}_{optionIdx}"; + if( ImGui.Selectable( name, groupIdx == _editor.GroupIdx && optionIdx == _editor.OptionIdx ) ) + { + _editor.SetSubMod( groupIdx, optionIdx ); + } } } } @@ -193,6 +430,7 @@ public class ModEditWindow : Window, IDisposable if( _editor!.UnusedFiles.Count == 0 ) { + ImGui.NewLine(); ImGui.TextUnformatted( "No unused files detected." ); } else @@ -202,12 +440,19 @@ public class ModEditWindow : Window, IDisposable _editor.AddUnusedPathsToDefault(); } + ImGui.SameLine(); if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) { _editor.DeleteUnusedPaths(); } - using var table = ImRaii.Table( "##unusedFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##table", 1, ImGuiTableFlags.RowBg ); if( !table ) { return; @@ -230,7 +475,14 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##files", 2 ); + DrawOptionSelectHeader(); + using var child = ImRaii.Child( "##files", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 2 ); if( !list ) { return; @@ -253,7 +505,30 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##meta", 3 ); + DrawOptionSelectHeader(); + + var setsEqual = _editor!.CurrentManipulations.SetEquals( _editor.CurrentOption.Manipulations ); + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplyManipulations(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertManipulations(); + } + + using var child = ImRaii.Child( "##meta", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 3 ); if( !list ) { return; @@ -288,6 +563,8 @@ public class ModEditWindow : Window, IDisposable } } + private string _newSwapKey = string.Empty; + private string _newSwapValue = string.Empty; private void DrawSwapTab() { using var tab = ImRaii.TabItem( "File Swaps" ); @@ -296,19 +573,94 @@ public class ModEditWindow : Window, IDisposable return; } - using var list = ImRaii.Table( "##swaps", 3 ); + DrawOptionSelectHeader(); + + var setsEqual = _editor!.CurrentSwaps.SetEquals( _editor.CurrentOption.FileSwaps ); + var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; + ImGui.NewLine(); + if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.ApplySwaps(); + } + + ImGui.SameLine(); + tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; + if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) + { + _editor.RevertSwaps(); + } + + using var child = ImRaii.Child( "##swaps", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg, -Vector2.One ); if( !list ) { return; } - foreach( var (gamePath, file) in _editor!.CurrentSwaps ) + var idx = 0; + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + var pathSize = ImGui.GetContentRegionAvail().X / 2 - iconSize.X; + ImGui.TableSetupColumn( "button", ImGuiTableColumnFlags.WidthFixed, iconSize.X ); + ImGui.TableSetupColumn( "source", ImGuiTableColumnFlags.WidthFixed, pathSize ); + ImGui.TableSetupColumn( "value", ImGuiTableColumnFlags.WidthFixed, pathSize ); + + foreach( var (gamePath, file) in _editor!.CurrentSwaps.ToList() ) { + using var id = ImRaii.PushId( idx++ ); ImGui.TableNextColumn(); - ConfigWindow.Text( gamePath.Path ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true ) ) + { + _editor.CurrentSwaps.Remove( gamePath ); + } + ImGui.TableNextColumn(); - ImGui.TextUnformatted( file.FullName ); + var tmp = gamePath.Path.ToString(); + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputText( "##key", ref tmp, Utf8GamePath.MaxGamePathLength ) + && Utf8GamePath.FromString( tmp, out var path ) + && !_editor.CurrentSwaps.ContainsKey( path ) ) + { + _editor.CurrentSwaps.Remove( gamePath ); + if( path.Length > 0 ) + { + _editor.CurrentSwaps[ path ] = file; + } + } + + ImGui.TableNextColumn(); + tmp = file.FullName; + ImGui.SetNextItemWidth( -1 ); + if( ImGui.InputText( "##value", ref tmp, Utf8GamePath.MaxGamePathLength ) && tmp.Length > 0 ) + { + _editor.CurrentSwaps[ gamePath ] = new FullPath( tmp ); + } } + + ImGui.TableNextColumn(); + var addable = Utf8GamePath.FromString( _newSwapKey, out var newPath ) + && newPath.Length > 0 + && _newSwapValue.Length > 0 + && _newSwapValue != _newSwapKey + && !_editor.CurrentSwaps.ContainsKey( newPath ); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, + true ) ) + { + _editor.CurrentSwaps[ newPath ] = new FullPath( _newSwapValue ); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##swapKey", "New Swap Source...", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( -1 ); + ImGui.InputTextWithHint( "##swapValue", "New Swap Target...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); } public ModEditWindow() diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 3ac1b344..7c149d19 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -5,11 +5,11 @@ using System.IO; using System.Numerics; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Memory; using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.UI; @@ -84,9 +84,6 @@ public partial class ConfigWindow MoveDirectory.Draw( _mod, buttonSize ); - - - MaterialSuffix.Draw( _mod, buttonSize ); ImGui.Dummy( _window._defaultSpace ); } @@ -135,13 +132,12 @@ public partial class ConfigWindow Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); } - if( ImGui.Button( "Edit Default Mod", reducedSize ) ) + if( ImGui.Button( "Edit Mod Details", reducedSize ) ) { _window.ModEditPopup.ChangeMod( _mod ); _window.ModEditPopup.ChangeOption( -1, 0 ); _window.ModEditPopup.IsOpen = true; } - ImGui.SameLine(); fileExists = File.Exists( _mod.DefaultFile ); tt = fileExists @@ -171,6 +167,9 @@ public partial class ConfigWindow { private static string _newGroupName = string.Empty; + public static void Reset() + => _newGroupName = string.Empty; + public static void Draw( ConfigWindow window, Mod mod ) { ImGui.SetNextItemWidth( window._inputTextWidth.X ); @@ -183,66 +182,30 @@ public partial class ConfigWindow tt, !nameValid, true ) ) { Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName ); - _newGroupName = string.Empty; + Reset(); } } } - // A row of three buttonSizes and a help marker that can be used for material suffix changing. - private static class MaterialSuffix - { - private static string _materialSuffixFrom = string.Empty; - private static string _materialSuffixTo = string.Empty; - - public static void Draw( Mod mod, Vector2 buttonSize ) - { - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); - ImGui.SameLine(); - var disabled = !ModelChanger.ValidStrings( _materialSuffixFrom, _materialSuffixTo ); - var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." - : _materialSuffixFrom == _materialSuffixTo ? "The source and target are identical." - : disabled ? "The suffices are not valid suffices." - : _materialSuffixFrom.Length == 0 ? "Convert all skin material suffices to the target." - : $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; - if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) - { - ModelChanger.ChangeModMaterials( mod, _materialSuffixFrom, _materialSuffixTo ); - } - - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" - + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" - + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); - } - } - // A text input for the new directory name and a button to apply the move. private static class MoveDirectory { private static string? _currentModDirectory; - private static Mod? _modForDirectory; private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + public static void Reset() + { + _currentModDirectory = null; + _state = Mod.Manager.NewDirectoryState.Identical; + } + public static void Draw( Mod mod, Vector2 buttonSize ) { ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); var tmp = _currentModDirectory ?? mod.ModPath.Name; - if( mod != _modForDirectory ) - { - tmp = mod.ModPath.Name; - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; - } - if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) { _currentModDirectory = tmp; - _modForDirectory = mod; _state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); } @@ -262,8 +225,7 @@ public partial class ConfigWindow if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null ) { Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); - _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; + Reset(); } ImGui.SameLine(); @@ -372,14 +334,6 @@ public partial class ConfigWindow ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, - "Edit group description.", false, true ) ) - { - _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); - } - - ImGui.SameLine(); - if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) ) { Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); @@ -407,6 +361,14 @@ public partial class ConfigWindow _delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); } + ImGui.SameLine(); + + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), _window._iconButtonSize, + "Edit group description.", false, true ) ) + { + _delayedActions.Enqueue( () => DescriptionEdit.OpenPopup( _mod, groupIdx ) ); + } + ImGui.SameLine(); var fileName = group.FileName( _mod.ModPath, groupIdx ); var fileExists = File.Exists( fileName ); @@ -433,9 +395,17 @@ public partial class ConfigWindow private static int _dragDropGroupIdx = -1; private static int _dragDropOptionIdx = -1; + public static void Reset() + { + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroupIdx = -1; + _dragDropOptionIdx = -1; + } + public static void Draw( ModPanel panel, int groupIdx ) { - using var table = ImRaii.Table( string.Empty, 5, ImGuiTableFlags.SizingFixedFit ); + using var table = ImRaii.Table( string.Empty, 4, ImGuiTableFlags.SizingFixedFit ); if( !table ) { return; @@ -445,7 +415,6 @@ public partial class ConfigWindow ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); - ImGui.TableSetupColumn( "edit", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale ); var group = panel._mod.Groups[ groupIdx ]; @@ -481,15 +450,6 @@ public partial class ConfigWindow panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); } - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Edit.ToIconString(), panel._window._iconButtonSize, - "Edit this option.", false, true ) ) - { - panel._window.ModEditPopup.ChangeMod( panel._mod ); - panel._window.ModEditPopup.ChangeOption( groupIdx, optionIdx ); - panel._window.ModEditPopup.IsOpen = true; - } - ImGui.TableNextColumn(); if( group.Type == SelectType.Multi ) { @@ -621,8 +581,16 @@ public partial class ConfigWindow // Temporary strings private static string? _currentEdit; private static int? _currentGroupPriority; - private static int _currentField = -1; - private static int _optionIndex = -1; + private static int _currentField = None; + private static int _optionIndex = None; + + public static void Reset() + { + _currentEdit = null; + _currentGroupPriority = null; + _currentField = None; + _optionIndex = None; + } public static bool Text( string label, int field, int option, string oldValue, out string value, uint maxLength, float width ) { @@ -638,10 +606,8 @@ public partial class ConfigWindow if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null ) { var ret = _currentEdit != oldValue; - value = _currentEdit; - _currentEdit = null; - _currentField = None; - _optionIndex = None; + value = _currentEdit; + Reset(); return ret; } @@ -663,10 +629,8 @@ public partial class ConfigWindow if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) { var ret = _currentGroupPriority != oldValue; - value = _currentGroupPriority.Value; - _currentGroupPriority = null; - _currentField = None; - _optionIndex = None; + value = _currentGroupPriority.Value; + Reset(); return ret; } diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index 52d26878..f09f1423 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -40,15 +40,15 @@ public partial class ConfigWindow } catch( Exception e ) { - PluginLog.Error($"Exception thrown during ModPanel Render:\n{e}" ); - PluginLog.Error($"{Penumbra.ModManager.Count} Mods\n" + PluginLog.Error( $"Exception thrown during ModPanel Render:\n{e}" ); + PluginLog.Error( $"{Penumbra.ModManager.Count} Mods\n" + $"{Penumbra.CollectionManager.Current.Name} Current Collection\n" + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + $"{_selector.SortMode} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" - + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", Penumbra.CollectionManager.Current.Inheritance)} Inheritances\n" - + $"{_selector.SelectedSettingCollection.Name} Collection\n"); + + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance )} Inheritances\n" + + $"{_selector.SelectedSettingCollection.Name} Collection\n" ); } } @@ -62,7 +62,7 @@ public partial class ConfigWindow ImGui.SameLine(); DrawInheritedCollectionButton( 3 * buttonSize ); ImGui.SameLine(); - DrawCollectionSelector( "##collection", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); + DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, ModCollection.Type.Current, false, null ); } private static void DrawDefaultCollectionButton( Vector2 width ) @@ -148,5 +148,28 @@ public partial class ConfigWindow UpdateSettingsData( selector ); UpdateModData(); } + + public void OnSelectionChange( Mod? old, Mod? mod, in ModFileSystemSelector.ModState _ ) + { + if( old == mod ) + { + return; + } + + if( mod == null ) + { + _window.ModEditPopup.IsOpen = false; + } + else if( _window.ModEditPopup.IsOpen ) + { + _window.ModEditPopup.ChangeMod( mod ); + } + + _currentPriority = null; + MoveDirectory.Reset(); + OptionTable.Reset(); + Input.Reset(); + AddOptionGroup.Reset(); + } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index 1652c414..793d42eb 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -26,14 +26,15 @@ public sealed partial class ConfigWindow : Window, IDisposable public ConfigWindow( Penumbra penumbra ) : base( GetLabel() ) { - _penumbra = penumbra; - _settingsTab = new SettingsTab( this ); - _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); - _modPanel = new ModPanel( this ); - _collectionsTab = new CollectionsTab( this ); - _effectiveTab = new EffectiveTab(); - _debugTab = new DebugTab( this ); - _resourceTab = new ResourceTab( this ); + _penumbra = penumbra; + _settingsTab = new SettingsTab( this ); + _selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); + _modPanel = new ModPanel( this ); + _selector.SelectionChanged += _modPanel.OnSelectionChange; + _collectionsTab = new CollectionsTab( this ); + _effectiveTab = new EffectiveTab(); + _debugTab = new DebugTab( this ); + _resourceTab = new ResourceTab( this ); Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs deleted file mode 100644 index 3c900bf4..00000000 --- a/Penumbra/Util/ModelChanger.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; -using Penumbra.GameData.Files; -using Penumbra.Mods; - -namespace Penumbra.Util; - -public static class ModelChanger -{ - public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; - public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); - - // Non-ASCII encoding can not be used. - public static bool ValidStrings( string from, string to ) - => to.Length != 0 - && from.Length < 16 - && to.Length < 16 - && from != to - && Encoding.UTF8.GetByteCount( from ) == from.Length - && Encoding.UTF8.GetByteCount( to ) == to.Length; - - - [Conditional( "FALSE" )] - private static void WriteBackup( string name, byte[] text ) - => File.WriteAllBytes( name + ".bak", text ); - - // Change material suffices for a single mdl file. - public static int ChangeMtrl( FullPath file, string from, string to ) - { - if( !file.Exists ) - { - return 0; - } - - try - { - var data = File.ReadAllBytes( file.FullName ); - var mdlFile = new MdlFile( data ); - - // If from is empty, match with any current material suffix, - // otherwise check for exact matches with from. - Func< string, bool > compare = MaterialRegex.IsMatch; - if( from.Length > 0 ) - { - from = string.Format( MaterialFormat, from ); - compare = s => s == from; - } - - to = string.Format( MaterialFormat, to ); - var replaced = 0; - for( var i = 0; i < mdlFile.Materials.Length; ++i ) - { - if( compare( mdlFile.Materials[ i ] ) ) - { - mdlFile.Materials[ i ] = to; - ++replaced; - } - } - - // Only rewrite the file if anything was changed. - if( replaced > 0 ) - { - WriteBackup( file.FullName, data ); - File.WriteAllBytes( file.FullName, mdlFile.Write() ); - } - - return replaced; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write .mdl data for file {file.FullName}:\n{e}" ); - return -1; - } - } - - public static bool ChangeModMaterials( Mod mod, string from, string to ) - { - if( ValidStrings( from, to ) ) - { - return mod.AllFiles - .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) - .All( file => ChangeMtrl( file, from, to ) >= 0 ); - } - - PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." ); - return false; - } -} \ No newline at end of file