mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add more edit options, some small fixes.
This commit is contained in:
parent
65bbece9cf
commit
54460c39f3
13 changed files with 697 additions and 227 deletions
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ public unsafe partial class ResourceLoader
|
|||
return;
|
||||
}
|
||||
|
||||
// Got some incomprehensible null-dereference exceptions here when hot-reloading penumbra.
|
||||
try
|
||||
{
|
||||
var crc = ( uint )originalPath.Path.Crc32;
|
||||
var originalResource = FindResource( handle->Category, handle->FileType, crc );
|
||||
_debugList[ manipulatedPath.Value ] = new DebugData()
|
||||
|
|
@ -65,6 +68,11 @@ public unsafe partial class ResourceLoader
|
|||
ResolverInfo = resolverInfo,
|
||||
};
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( e.ToString() );
|
||||
}
|
||||
}
|
||||
|
||||
// Find a key in a StdMap.
|
||||
private static TValue* FindInMap< TKey, TValue >( StdMap< TKey, TValue >* map, in TKey key )
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
|
|
@ -93,10 +88,13 @@ public partial class Mod
|
|||
|
||||
|
||||
public void StartDuplicateCheck()
|
||||
{
|
||||
if( DuplicatesFinished )
|
||||
{
|
||||
DuplicatesFinished = false;
|
||||
Task.Run( CheckDuplicates );
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckDuplicates()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
|||
179
Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs
Normal file
179
Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = "_";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( file.FullName );
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true ) )
|
||||
{
|
||||
_editor.CurrentSwaps.Remove( gamePath );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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 )
|
||||
{
|
||||
|
|
@ -639,9 +607,7 @@ public partial class ConfigWindow
|
|||
{
|
||||
var ret = _currentEdit != oldValue;
|
||||
value = _currentEdit;
|
||||
_currentEdit = null;
|
||||
_currentField = None;
|
||||
_optionIndex = None;
|
||||
Reset();
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -664,9 +630,7 @@ public partial class ConfigWindow
|
|||
{
|
||||
var ret = _currentGroupPriority != oldValue;
|
||||
value = _currentGroupPriority.Value;
|
||||
_currentGroupPriority = null;
|
||||
_currentField = None;
|
||||
_optionIndex = None;
|
||||
Reset();
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
_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 );
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue