Add more edit options, some small fixes.

This commit is contained in:
Ottermandias 2022-05-03 18:15:35 +02:00
parent 65bbece9cf
commit 54460c39f3
13 changed files with 697 additions and 227 deletions

View file

@ -163,7 +163,7 @@ public static class RaceEnumExtensions
ModelRace.AuRa => Race.AuRa.ToName(), ModelRace.AuRa => Race.AuRa.ToName(),
ModelRace.Hrothgar => Race.Hrothgar.ToName(), ModelRace.Hrothgar => Race.Hrothgar.ToName(),
ModelRace.Viera => Race.Viera.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.AuRa => "Au Ra",
Race.Hrothgar => "Hrothgar", Race.Hrothgar => "Hrothgar",
Race.Viera => "Viera", Race.Viera => "Viera",
_ => throw new ArgumentOutOfRangeException( nameof( race ), race, null ), _ => "Unknown",
}; };
} }
@ -191,7 +191,7 @@ public static class RaceEnumExtensions
Gender.Female => "Female", Gender.Female => "Female",
Gender.MaleNpc => "Male (NPC)", Gender.MaleNpc => "Male (NPC)",
Gender.FemaleNpc => "Female (NPC)", Gender.FemaleNpc => "Female (NPC)",
_ => throw new InvalidEnumArgumentException(), _ => "Unknown",
}; };
} }
@ -215,7 +215,7 @@ public static class RaceEnumExtensions
SubRace.Lost => "Lost", SubRace.Lost => "Lost",
SubRace.Rava => "Rava", SubRace.Rava => "Rava",
SubRace.Veena => "Veena", SubRace.Veena => "Veena",
_ => throw new InvalidEnumArgumentException(), _ => "Unknown",
}; };
} }

View file

@ -52,18 +52,26 @@ public unsafe partial class ResourceLoader
return; return;
} }
var crc = ( uint )originalPath.Path.Crc32; // Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra.
var originalResource = FindResource( handle->Category, handle->FileType, crc ); try
_debugList[ manipulatedPath.Value ] = new DebugData()
{ {
OriginalResource = ( Structs.ResourceHandle* )originalResource, var crc = ( uint )originalPath.Path.Crc32;
ManipulatedResource = handle, var originalResource = FindResource( handle->Category, handle->FileType, crc );
Category = handle->Category, _debugList[ manipulatedPath.Value ] = new DebugData()
Extension = handle->FileType, {
OriginalPath = originalPath.Clone(), OriginalResource = ( Structs.ResourceHandle* )originalResource,
ManipulatedPath = manipulatedPath.Value, ManipulatedResource = handle,
ResolverInfo = resolverInfo, 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. // Find a key in a StdMap.

View file

@ -48,11 +48,6 @@ public partial class Mod
_duplicates.Clear(); _duplicates.Clear();
} }
public void Cancel()
{
DuplicatesFinished = true;
}
private void HandleDuplicate( FullPath duplicate, FullPath remaining ) private void HandleDuplicate( FullPath duplicate, FullPath remaining )
{ {
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx ) void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
@ -94,8 +89,11 @@ public partial class Mod
public void StartDuplicateCheck() public void StartDuplicateCheck()
{ {
DuplicatesFinished = false; if( DuplicatesFinished )
Task.Run( CheckDuplicates ); {
DuplicatesFinished = false;
Task.Run( CheckDuplicates );
}
} }
private void CheckDuplicates() private void CheckDuplicates()

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Util; using Penumbra.Util;
@ -9,20 +10,23 @@ public partial class Mod
{ {
public partial class Editor public partial class Editor
{ {
private int _groupIdx = -1; public int GroupIdx { get; private set; } = -1;
private int _optionIdx = 0; public int OptionIdx { get; private set; } = 0;
private IModGroup? _modGroup; private IModGroup? _modGroup;
private SubMod _subMod; private SubMod _subMod;
public ISubMod CurrentOption
=> _subMod;
public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new();
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new(); public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
public readonly HashSet< MetaManipulation > CurrentManipulations = new(); public readonly HashSet< MetaManipulation > CurrentManipulations = new();
public void SetSubMod( int groupIdx, int optionIdx ) public void SetSubMod( int groupIdx, int optionIdx )
{ {
_groupIdx = groupIdx; GroupIdx = groupIdx;
_optionIdx = optionIdx; OptionIdx = optionIdx;
if( groupIdx >= 0 ) if( groupIdx >= 0 )
{ {
_modGroup = _mod.Groups[ groupIdx ]; _modGroup = _mod.Groups[ groupIdx ];
@ -34,8 +38,38 @@ public partial class Mod
_subMod = _mod._default; _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 ); 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 ); CurrentSwaps.SetTo( _subMod.FileSwaps );
}
public void ApplyManipulations()
{
Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() );
}
public void RevertManipulations()
{
CurrentManipulations.Clear(); CurrentManipulations.Clear();
CurrentManipulations.UnionWith( _subMod.Manipulations ); CurrentManipulations.UnionWith( _subMod.Manipulations );
} }

View file

@ -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<int> _materialIndices;
public bool Changed { get; private set; } = false;
public IReadOnlyList<string> CurrentMaterials
=> _currentMaterials;
private IEnumerable<string> 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<int> indices )
{
Path = path;
_file = file;
_materialIndices = indices;
_currentMaterials = DefaultMaterials.ToArray();
}
}
}
}

View file

@ -21,13 +21,17 @@ public partial class Mod
_missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) ); _missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) );
_unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) ); _unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) );
_subMod = _mod._default; _subMod = _mod._default;
ScanModels();
} }
public void Dispose() public void Cancel()
{ {
DuplicatesFinished = true; DuplicatesFinished = true;
} }
public void Dispose()
=> Cancel();
// Does not delete the base directory itself even if it is completely empty at the end. // Does not delete the base directory itself even if it is completely empty at the end.
private static void ClearEmptySubDirectories( DirectoryInfo baseDir ) private static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{ {
@ -49,7 +53,7 @@ public partial class Mod
{ {
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{ {
action( @group[ optionIdx ], groupIdx, optionIdx ); action( group[ optionIdx ], groupIdx, optionIdx );
} }
} }
} }

View file

@ -18,7 +18,7 @@ public partial class Mod
// - Containing no symbols invalid for FFXIV or windows paths. // - Containing no symbols invalid for FFXIV or windows paths.
internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
{ {
var name = Path.GetFileNameWithoutExtension( modListName ); var name = modListName;
if( name.Length == 0 ) if( name.Length == 0 )
{ {
name = "_"; name = "_";

View file

@ -23,6 +23,7 @@ public static class Colors
public const uint PressEnterWarningBg = 0xFF202080; public const uint PressEnterWarningBg = 0xFF202080;
public const uint RegexWarningBorder = 0xFF0000B0; public const uint RegexWarningBorder = 0xFF0000B0;
public const uint MetaInfoText = 0xAAFFFFFF; public const uint MetaInfoText = 0xAAFFFFFF;
public const uint RedTableBgTint = 0x40000080;
public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) public static (uint DefaultColor, string Name, string Description) Data( this ColorId color )
=> color switch => color switch

View file

@ -1,13 +1,18 @@
using System; using System;
using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -28,6 +33,11 @@ public class ModEditWindow : Window, IDisposable
_editor = new Mod.Editor( mod ); _editor = new Mod.Editor( mod );
_mod = mod; _mod = mod;
WindowName = $"{mod.Name}{WindowBaseLabel}"; WindowName = $"{mod.Name}{WindowBaseLabel}";
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = ImGuiHelpers.ScaledVector2( 800, 600 ),
MaximumSize = 4000 * Vector2.One,
};
} }
public void ChangeOption( int groupIdx, int optionIdx ) public void ChangeOption( int groupIdx, int optionIdx )
@ -50,6 +60,168 @@ public class ModEditWindow : Window, IDisposable
DrawMissingFilesTab(); DrawMissingFilesTab();
DrawUnusedFilesTab(); DrawUnusedFilesTab();
DrawDuplicatesTab(); 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() private void DrawMissingFilesTab()
@ -62,6 +234,7 @@ public class ModEditWindow : Window, IDisposable
if( _editor!.MissingPaths.Count == 0 ) if( _editor!.MissingPaths.Count == 0 )
{ {
ImGui.NewLine();
ImGui.TextUnformatted( "No missing files detected." ); ImGui.TextUnformatted( "No missing files detected." );
} }
else else
@ -71,6 +244,12 @@ public class ModEditWindow : Window, IDisposable
_editor.RemoveMissingPaths(); _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 ); using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One );
if( !table ) if( !table )
{ {
@ -113,7 +292,9 @@ public class ModEditWindow : Window, IDisposable
if( _editor.Duplicates.Count == 0 ) if( _editor.Duplicates.Count == 0 )
{ {
ImGui.NewLine();
ImGui.TextUnformatted( "No duplicates found." ); ImGui.TextUnformatted( "No duplicates found." );
return;
} }
if( ImGui.Button( "Delete and Redirect Duplicates" ) ) if( ImGui.Button( "Delete and Redirect Duplicates" ) )
@ -173,12 +354,68 @@ public class ModEditWindow : Window, IDisposable
foreach( var duplicate in set.Skip( 1 ) ) foreach( var duplicate in set.Skip( 1 ) )
{ {
ImGui.TableNextColumn(); 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 ); using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, 0x40000080 ); ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint );
ImGui.TableNextColumn(); 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 ) if( _editor!.UnusedFiles.Count == 0 )
{ {
ImGui.NewLine();
ImGui.TextUnformatted( "No unused files detected." ); ImGui.TextUnformatted( "No unused files detected." );
} }
else else
@ -202,12 +440,19 @@ public class ModEditWindow : Window, IDisposable
_editor.AddUnusedPathsToDefault(); _editor.AddUnusedPathsToDefault();
} }
ImGui.SameLine();
if( ImGui.Button( "Delete Unused Files from Filesystem" ) ) if( ImGui.Button( "Delete Unused Files from Filesystem" ) )
{ {
_editor.DeleteUnusedPaths(); _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 ) if( !table )
{ {
return; return;
@ -230,7 +475,14 @@ public class ModEditWindow : Window, IDisposable
return; 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 ) if( !list )
{ {
return; return;
@ -253,7 +505,30 @@ public class ModEditWindow : Window, IDisposable
return; 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 ) if( !list )
{ {
return; return;
@ -288,6 +563,8 @@ public class ModEditWindow : Window, IDisposable
} }
} }
private string _newSwapKey = string.Empty;
private string _newSwapValue = string.Empty;
private void DrawSwapTab() private void DrawSwapTab()
{ {
using var tab = ImRaii.TabItem( "File Swaps" ); using var tab = ImRaii.TabItem( "File Swaps" );
@ -296,19 +573,94 @@ public class ModEditWindow : Window, IDisposable
return; 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 ) if( !list )
{ {
return; 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(); 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.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() public ModEditWindow()

View file

@ -5,11 +5,11 @@ using System.IO;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Memory;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -84,9 +84,6 @@ public partial class ConfigWindow
MoveDirectory.Draw( _mod, buttonSize ); MoveDirectory.Draw( _mod, buttonSize );
MaterialSuffix.Draw( _mod, buttonSize );
ImGui.Dummy( _window._defaultSpace ); ImGui.Dummy( _window._defaultSpace );
} }
@ -135,13 +132,12 @@ public partial class ConfigWindow
Process.Start( new ProcessStartInfo( _mod.MetaFile.FullName ) { UseShellExecute = true } ); 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.ChangeMod( _mod );
_window.ModEditPopup.ChangeOption( -1, 0 ); _window.ModEditPopup.ChangeOption( -1, 0 );
_window.ModEditPopup.IsOpen = true; _window.ModEditPopup.IsOpen = true;
} }
ImGui.SameLine(); ImGui.SameLine();
fileExists = File.Exists( _mod.DefaultFile ); fileExists = File.Exists( _mod.DefaultFile );
tt = fileExists tt = fileExists
@ -171,6 +167,9 @@ public partial class ConfigWindow
{ {
private static string _newGroupName = string.Empty; private static string _newGroupName = string.Empty;
public static void Reset()
=> _newGroupName = string.Empty;
public static void Draw( ConfigWindow window, Mod mod ) public static void Draw( ConfigWindow window, Mod mod )
{ {
ImGui.SetNextItemWidth( window._inputTextWidth.X ); ImGui.SetNextItemWidth( window._inputTextWidth.X );
@ -183,66 +182,30 @@ public partial class ConfigWindow
tt, !nameValid, true ) ) tt, !nameValid, true ) )
{ {
Penumbra.ModManager.AddModGroup( mod, SelectType.Single, _newGroupName ); 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. // A text input for the new directory name and a button to apply the move.
private static class MoveDirectory private static class MoveDirectory
{ {
private static string? _currentModDirectory; private static string? _currentModDirectory;
private static Mod? _modForDirectory;
private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; 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 ) public static void Draw( Mod mod, Vector2 buttonSize )
{ {
ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X ); ImGui.SetNextItemWidth( buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X );
var tmp = _currentModDirectory ?? mod.ModPath.Name; 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 ) ) if( ImGui.InputText( "##newModMove", ref tmp, 64 ) )
{ {
_currentModDirectory = tmp; _currentModDirectory = tmp;
_modForDirectory = mod;
_state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); _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 ) if( ImGuiUtil.DrawDisabledButton( "Rename Mod Directory", buttonSize, tt, disabled ) && _currentModDirectory != null )
{ {
Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory ); Penumbra.ModManager.MoveModDirectory( mod.Index, _currentModDirectory );
_currentModDirectory = null; Reset();
_state = Mod.Manager.NewDirectoryState.Identical;
} }
ImGui.SameLine(); ImGui.SameLine();
@ -372,14 +334,6 @@ public partial class ConfigWindow
ImGui.SameLine(); 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 ) ) if( Input.Priority( "##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * ImGuiHelpers.GlobalScale ) )
{ {
Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority ); Penumbra.ModManager.ChangeGroupPriority( _mod, groupIdx, priority );
@ -407,6 +361,14 @@ public partial class ConfigWindow
_delayedActions.Enqueue( () => Penumbra.ModManager.MoveModGroup( _mod, groupIdx, groupIdx + 1 ) ); _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(); ImGui.SameLine();
var fileName = group.FileName( _mod.ModPath, groupIdx ); var fileName = group.FileName( _mod.ModPath, groupIdx );
var fileExists = File.Exists( fileName ); var fileExists = File.Exists( fileName );
@ -433,9 +395,17 @@ public partial class ConfigWindow
private static int _dragDropGroupIdx = -1; private static int _dragDropGroupIdx = -1;
private static int _dragDropOptionIdx = -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 ) 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 ) if( !table )
{ {
return; return;
@ -445,7 +415,6 @@ public partial class ConfigWindow
ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed, ImGui.TableSetupColumn( "name", ImGuiTableColumnFlags.WidthFixed,
panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale ); panel._window._inputTextWidth.X - 62 * ImGuiHelpers.GlobalScale );
ImGui.TableSetupColumn( "delete", ImGuiTableColumnFlags.WidthFixed, panel._window._iconButtonSize.X ); 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 ); ImGui.TableSetupColumn( "priority", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale );
var group = panel._mod.Groups[ groupIdx ]; var group = panel._mod.Groups[ groupIdx ];
@ -481,15 +450,6 @@ public partial class ConfigWindow
panel._delayedActions.Enqueue( () => Penumbra.ModManager.DeleteOption( panel._mod, groupIdx, optionIdx ) ); 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(); ImGui.TableNextColumn();
if( group.Type == SelectType.Multi ) if( group.Type == SelectType.Multi )
{ {
@ -621,8 +581,16 @@ public partial class ConfigWindow
// Temporary strings // Temporary strings
private static string? _currentEdit; private static string? _currentEdit;
private static int? _currentGroupPriority; private static int? _currentGroupPriority;
private static int _currentField = -1; private static int _currentField = None;
private static int _optionIndex = -1; 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 ) 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 ) if( ImGui.IsItemDeactivatedAfterEdit() && _currentEdit != null )
{ {
var ret = _currentEdit != oldValue; var ret = _currentEdit != oldValue;
value = _currentEdit; value = _currentEdit;
_currentEdit = null; Reset();
_currentField = None;
_optionIndex = None;
return ret; return ret;
} }
@ -663,10 +629,8 @@ public partial class ConfigWindow
if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null ) if( ImGui.IsItemDeactivatedAfterEdit() && _currentGroupPriority != null )
{ {
var ret = _currentGroupPriority != oldValue; var ret = _currentGroupPriority != oldValue;
value = _currentGroupPriority.Value; value = _currentGroupPriority.Value;
_currentGroupPriority = null; Reset();
_currentField = None;
_optionIndex = None;
return ret; return ret;
} }

View file

@ -40,15 +40,15 @@ public partial class ConfigWindow
} }
catch( Exception e ) catch( Exception e )
{ {
PluginLog.Error($"Exception thrown during ModPanel Render:\n{e}" ); PluginLog.Error( $"Exception thrown during ModPanel Render:\n{e}" );
PluginLog.Error($"{Penumbra.ModManager.Count} Mods\n" PluginLog.Error( $"{Penumbra.ModManager.Count} Mods\n"
+ $"{Penumbra.CollectionManager.Current.Name} Current Collection\n" + $"{Penumbra.CollectionManager.Current.Name} Current Collection\n"
+ $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n" + $"{Penumbra.CollectionManager.Current.Settings.Count} Settings\n"
+ $"{_selector.SortMode} Sort Mode\n" + $"{_selector.SortMode} Sort Mode\n"
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", Penumbra.CollectionManager.Current.Inheritance)} Inheritances\n" + $"{string.Join( ", ", Penumbra.CollectionManager.Current.Inheritance )} Inheritances\n"
+ $"{_selector.SelectedSettingCollection.Name} Collection\n"); + $"{_selector.SelectedSettingCollection.Name} Collection\n" );
} }
} }
@ -62,7 +62,7 @@ public partial class ConfigWindow
ImGui.SameLine(); ImGui.SameLine();
DrawInheritedCollectionButton( 3 * buttonSize ); DrawInheritedCollectionButton( 3 * buttonSize );
ImGui.SameLine(); 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 ) private static void DrawDefaultCollectionButton( Vector2 width )
@ -148,5 +148,28 @@ public partial class ConfigWindow
UpdateSettingsData( selector ); UpdateSettingsData( selector );
UpdateModData(); 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();
}
} }
} }

View file

@ -26,14 +26,15 @@ public sealed partial class ConfigWindow : Window, IDisposable
public ConfigWindow( Penumbra penumbra ) public ConfigWindow( Penumbra penumbra )
: base( GetLabel() ) : base( GetLabel() )
{ {
_penumbra = penumbra; _penumbra = penumbra;
_settingsTab = new SettingsTab( this ); _settingsTab = new SettingsTab( this );
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _selector = new ModFileSystemSelector( _penumbra.ModFileSystem );
_modPanel = new ModPanel( this ); _modPanel = new ModPanel( this );
_collectionsTab = new CollectionsTab( this ); _selector.SelectionChanged += _modPanel.OnSelectionChange;
_effectiveTab = new EffectiveTab(); _collectionsTab = new CollectionsTab( this );
_debugTab = new DebugTab( this ); _effectiveTab = new EffectiveTab();
_resourceTab = new ResourceTab( this ); _debugTab = new DebugTab( this );
_resourceTab = new ResourceTab( this );
Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true;
Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true;

View file

@ -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;
}
}