mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Change Skin Material Replacement to accept arbitrary suffix-strings for From and To.
This commit is contained in:
parent
947e40b1eb
commit
aa180dcdf6
2 changed files with 638 additions and 569 deletions
|
|
@ -11,10 +11,10 @@ using Penumbra.Mods;
|
||||||
using Penumbra.UI.Custom;
|
using Penumbra.UI.Custom;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.UI
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
public partial class SettingsInterface
|
||||||
{
|
{
|
||||||
public partial class SettingsInterface
|
|
||||||
{
|
|
||||||
private class ModPanel
|
private class ModPanel
|
||||||
{
|
{
|
||||||
private const string LabelModPanel = "selectedModInfo";
|
private const string LabelModPanel = "selectedModInfo";
|
||||||
|
|
@ -46,7 +46,7 @@ namespace Penumbra.UI
|
||||||
"Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!";
|
"Try to reduce unnecessary options or subdirectories to default options if possible.\nExperimental - use at own risk!";
|
||||||
|
|
||||||
private const float HeaderLineDistance = 10f;
|
private const float HeaderLineDistance = 10f;
|
||||||
private static readonly Vector4 GreyColor = new( 1f, 1f, 1f, 0.66f );
|
private static readonly Vector4 GreyColor = new(1f, 1f, 1f, 0.66f);
|
||||||
|
|
||||||
private readonly SettingsInterface _base;
|
private readonly SettingsInterface _base;
|
||||||
private readonly Selector _selector;
|
private readonly Selector _selector;
|
||||||
|
|
@ -58,6 +58,9 @@ namespace Penumbra.UI
|
||||||
private string _currentWebsite;
|
private string _currentWebsite;
|
||||||
private bool _validWebsite;
|
private bool _validWebsite;
|
||||||
|
|
||||||
|
private string _fromMaterial = string.Empty;
|
||||||
|
private string _toMaterial = string.Empty;
|
||||||
|
|
||||||
public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods )
|
public ModPanel( SettingsInterface ui, Selector s, HashSet< string > newMods )
|
||||||
{
|
{
|
||||||
_base = ui;
|
_base = ui;
|
||||||
|
|
@ -276,8 +279,8 @@ namespace Penumbra.UI
|
||||||
}
|
}
|
||||||
else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) )
|
else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) )
|
||||||
{
|
{
|
||||||
DirectoryInfo dir = Mod!.Data.BasePath;
|
var dir = Mod!.Data.BasePath;
|
||||||
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
|
DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName ));
|
||||||
|
|
||||||
if( newDir.Exists )
|
if( newDir.Exists )
|
||||||
{
|
{
|
||||||
|
|
@ -329,8 +332,8 @@ namespace Penumbra.UI
|
||||||
|
|
||||||
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
||||||
|
|
||||||
DirectoryInfo dir = Mod!.Data.BasePath;
|
var dir = Mod!.Data.BasePath;
|
||||||
DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) );
|
DirectoryInfo newDir = new(Path.Combine( dir.Parent!.FullName, _newName ));
|
||||||
ImGui.Text(
|
ImGui.Text(
|
||||||
$"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." );
|
$"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." );
|
||||||
var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 );
|
var buttonSize = ImGuiHelpers.ScaledVector2( 120, 0 );
|
||||||
|
|
@ -499,39 +502,32 @@ namespace Penumbra.UI
|
||||||
+ "Experimental - Use at own risk!" );
|
+ "Experimental - Use at own risk!" );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawMaterialChangeButtons()
|
private void DrawMaterialChangeRow()
|
||||||
{
|
{
|
||||||
if( ImGui.Button( "Skin Material B to D" ) )
|
ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale );
|
||||||
{
|
ImGui.InputTextWithHint( "##fromMaterial", "From Material Suffix...", ref _fromMaterial, 16 );
|
||||||
ModelChanger.ChangeMtrlBToD( Mod!.Data );
|
|
||||||
}
|
|
||||||
ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from B to D.\n"
|
|
||||||
+ "This is usually to convert Bibo+ models to T&F3 skins.\n"
|
|
||||||
+ "This overwrites .mdl files, use at your own risk!" );
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Skin Material D to B" ) )
|
using var font = ImGuiRaii.PushFont( UiBuilder.IconFont );
|
||||||
{
|
ImGui.Text( FontAwesomeIcon.LongArrowAltRight.ToIconString() );
|
||||||
ModelChanger.ChangeMtrlDToB( Mod!.Data );
|
font.Pop();
|
||||||
}
|
|
||||||
ImGuiCustom.HoverTooltip( "Change the skin material all models in this mod reference from D to B.\n"
|
|
||||||
+ "This is usually to convert T&F3 models to Bibo+ skins.\n"
|
|
||||||
+ "This overwrites .mdl files, use at your own risk!" );
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Skin Material A to E" ) )
|
ImGui.SetNextItemWidth( 150 * ImGuiHelpers.GlobalScale );
|
||||||
{
|
ImGui.InputTextWithHint( "##toMaterial", "To Material Suffix...", ref _toMaterial, 16 );
|
||||||
ModelChanger.ChangeMtrlAToE( Mod!.Data );
|
|
||||||
}
|
|
||||||
ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from A to E.\n"
|
|
||||||
+ "This overwrites .mdl files, use at your own risk!" );
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Skin Material E to A" ) )
|
var validStrings = ModelChanger.ValidStrings( _fromMaterial, _toMaterial );
|
||||||
|
using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !validStrings );
|
||||||
|
if( ImGui.Button( "Convert" ) && validStrings )
|
||||||
{
|
{
|
||||||
ModelChanger.ChangeMtrlEToA( Mod!.Data );
|
ModelChanger.ChangeModMaterials( Mod!.Data, _fromMaterial, _toMaterial );
|
||||||
}
|
}
|
||||||
ImGuiCustom.HoverTooltip( "Change the material all models in this mod reference from E to A.\n"
|
|
||||||
|
alpha.Pop();
|
||||||
|
|
||||||
|
ImGuiCustom.HoverTooltip(
|
||||||
|
"Change the skin material of all models in this mod reference "
|
||||||
|
+ "from the suffix given in the first text input to "
|
||||||
|
+ "the suffix given in the second input.\n"
|
||||||
|
+ "Enter only the suffix, e.g. 'd' or 'a' or 'bibo', not the whole path.\n"
|
||||||
+ "This overwrites .mdl files, use at your own risk!" );
|
+ "This overwrites .mdl files, use at your own risk!" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,7 +551,7 @@ namespace Penumbra.UI
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
DrawSplitButton();
|
DrawSplitButton();
|
||||||
|
|
||||||
DrawMaterialChangeButtons();
|
DrawMaterialChangeRow();
|
||||||
|
|
||||||
DrawSortOrder( Mod!.Data, _modManager, _selector );
|
DrawSortOrder( Mod!.Data, _modManager, _selector );
|
||||||
}
|
}
|
||||||
|
|
@ -600,5 +596,4 @@ namespace Penumbra.UI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
@ -9,10 +10,105 @@ namespace Penumbra.Util;
|
||||||
|
|
||||||
public static class ModelChanger
|
public static class ModelChanger
|
||||||
{
|
{
|
||||||
private const string SkinMaterialString = "/mt_c0201b0001_d.mtrl";
|
private static int FindSubSequence( byte[] main, byte[] sub, int from = 0 )
|
||||||
private static readonly byte[] SkinMaterial = Encoding.UTF8.GetBytes( SkinMaterialString );
|
{
|
||||||
|
if( sub.Length + from > main.Length )
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
public static int ChangeMtrl( FullPath file, byte from, byte to )
|
var length = main.Length - sub.Length;
|
||||||
|
for( var i = from; i < length; ++i )
|
||||||
|
{
|
||||||
|
var span = main.AsSpan( i, sub.Length );
|
||||||
|
if( span.SequenceEqual( sub ) )
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ConvertString( string text, out byte[] data )
|
||||||
|
{
|
||||||
|
data = Encoding.UTF8.GetBytes( text );
|
||||||
|
return data.Length == text.Length && !data.Any( b => b > 0b10000000 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ValidStrings( string from, string to )
|
||||||
|
=> from.Length != 0
|
||||||
|
&& to.Length != 0
|
||||||
|
&& from.Length < 16
|
||||||
|
&& to.Length < 16
|
||||||
|
&& from != to
|
||||||
|
&& Encoding.UTF8.GetByteCount( from ) == from.Length
|
||||||
|
&& Encoding.UTF8.GetByteCount( to ) == to.Length;
|
||||||
|
|
||||||
|
private static bool ConvertName( string name, out byte[] data )
|
||||||
|
{
|
||||||
|
if( name.Length != 0 )
|
||||||
|
{
|
||||||
|
return ConvertString( $"/mt_c0201b0001_{name}.mtrl", out data );
|
||||||
|
}
|
||||||
|
|
||||||
|
data = Array.Empty< byte >();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReplaceEqualSequences( byte[] main, byte[] subLhs, byte[] subRhs )
|
||||||
|
{
|
||||||
|
if( subLhs.SequenceEqual( subRhs ) )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
var replacements = 0;
|
||||||
|
while( ( i = FindSubSequence( main, subLhs, i ) ) > 0 )
|
||||||
|
{
|
||||||
|
subRhs.CopyTo( main.AsSpan( i ) );
|
||||||
|
i += subLhs.Length;
|
||||||
|
++replacements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReplaceSubSequences( ref byte[] main, byte[] subLhs, byte[] subRhs )
|
||||||
|
{
|
||||||
|
if( subLhs.Length == subRhs.Length )
|
||||||
|
{
|
||||||
|
return ReplaceEqualSequences( main, subLhs, subRhs );
|
||||||
|
}
|
||||||
|
|
||||||
|
var replacements = new List< int >( 4 );
|
||||||
|
for( var i = FindSubSequence( main, subLhs ); i >= 0; i = FindSubSequence( main, subLhs, i + subLhs.Length ) )
|
||||||
|
{
|
||||||
|
replacements.Add( i );
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = new byte[main.Length + ( subRhs.Length - subLhs.Length ) * replacements.Count];
|
||||||
|
|
||||||
|
var last = 0;
|
||||||
|
var totalLength = 0;
|
||||||
|
foreach( var i in replacements )
|
||||||
|
{
|
||||||
|
var length = i - last;
|
||||||
|
main.AsSpan( last, length ).CopyTo( ret.AsSpan( totalLength ) );
|
||||||
|
totalLength += length;
|
||||||
|
subRhs.CopyTo( ret.AsSpan( totalLength ) );
|
||||||
|
totalLength += subRhs.Length;
|
||||||
|
last = i + subLhs.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.AsSpan( last ).CopyTo( ret.AsSpan( totalLength ) );
|
||||||
|
|
||||||
|
main = ret;
|
||||||
|
return replacements.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int ChangeMtrl( FullPath file, byte[] from, byte[] to )
|
||||||
{
|
{
|
||||||
if( !file.Exists )
|
if( !file.Exists )
|
||||||
{
|
{
|
||||||
|
|
@ -22,53 +118,31 @@ public static class ModelChanger
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var text = File.ReadAllBytes( file.FullName );
|
var text = File.ReadAllBytes( file.FullName );
|
||||||
var replaced = 0;
|
var replaced = ReplaceSubSequences( ref text, from, to );
|
||||||
|
if( replaced > 0 )
|
||||||
var length = text.Length - SkinMaterial.Length;
|
|
||||||
SkinMaterial[ 15 ] = from;
|
|
||||||
for( var i = 0; i < length; ++i )
|
|
||||||
{
|
{
|
||||||
if( SkinMaterial.Where( ( t, j ) => text[ i + j ] != t ).Any() )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
text[ i + 15 ] = to;
|
|
||||||
i += SkinMaterial.Length;
|
|
||||||
++replaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( replaced == 0 )
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
File.WriteAllBytes( file.FullName, text );
|
File.WriteAllBytes( file.FullName, text );
|
||||||
|
}
|
||||||
|
|
||||||
return replaced;
|
return replaced;
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch( Exception e )
|
||||||
{
|
{
|
||||||
PluginLog.Error( $"Could not write .mdl data for file {file.FullName}, replacing {( char )from} with {( char )to}:\n{e}" );
|
PluginLog.Error( $"Could not write .mdl data for file {file.FullName}:\n{e}" );
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ChangeModMaterials( ModData mod, byte from, byte to )
|
public static bool ChangeModMaterials( ModData mod, string from, string to )
|
||||||
|
{
|
||||||
|
if( ValidStrings( from, to ) && ConvertName( from, out var lhs ) && ConvertName( to, out var rhs ) )
|
||||||
{
|
{
|
||||||
return mod.Resources.ModFiles
|
return mod.Resources.ModFiles
|
||||||
.Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) )
|
.Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) )
|
||||||
.All( file => ChangeMtrl( file, from, to ) >= 0 );
|
.All( file => ChangeMtrl( file, lhs, rhs ) >= 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool ChangeMtrlBToD( ModData mod )
|
PluginLog.Warning( $"{from} or {to} can not be valid material suffixes." );
|
||||||
=> ChangeModMaterials( mod, ( byte )'b', ( byte )'d' );
|
return false;
|
||||||
|
}
|
||||||
public static bool ChangeMtrlDToB( ModData mod )
|
|
||||||
=> ChangeModMaterials( mod, ( byte )'d', ( byte )'b' );
|
|
||||||
|
|
||||||
public static bool ChangeMtrlEToA( ModData mod )
|
|
||||||
=> ChangeModMaterials( mod, ( byte )'e', ( byte )'a' );
|
|
||||||
|
|
||||||
public static bool ChangeMtrlAToE( ModData mod )
|
|
||||||
=> ChangeModMaterials( mod, ( byte )'a', ( byte )'e' );
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue