Merge branch 'net6'

This commit is contained in:
Ottermandias 2022-08-24 18:16:07 +02:00
commit 0f2be88706
73 changed files with 3005 additions and 1852 deletions

View file

@ -21,15 +21,15 @@ jobs:
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\"
- name: Build
run: |
dotnet build --no-restore --configuration Release --nologo
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/net5.0-windows/* -DestinationPath Penumbra.zip
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Release/net5.0-windows/*
./Penumbra/bin/Release/*

View file

@ -20,7 +20,7 @@ jobs:
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@ -29,17 +29,17 @@ jobs:
- name: write version into json
run: |
$ver = '${{ github.ref }}' -replace 'refs/tags/',''
$path = './Penumbra/bin/Release/net5.0-windows/Penumbra.json'
$path = './Penumbra/bin/Release/Penumbra.json'
$content = get-content -path $path
$content = $content -replace '1.0.0.0',$ver
set-content -Path $path -Value $content
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Release/net5.0-windows/* -DestinationPath Penumbra.zip
run: Compress-Archive -Path Penumbra/bin/Release/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Release/net5.0-windows/*
./Penumbra/bin/Release/*
- name: Create Release
id: create_release
uses: actions/create-release@v1

View file

@ -20,7 +20,7 @@ jobs:
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |
@ -29,17 +29,17 @@ jobs:
- name: write version into json
run: |
$ver = '${{ github.ref }}' -replace 'refs/tags/t',''
$path = './Penumbra/bin/Debug/net5.0-windows/Penumbra.json'
$path = './Penumbra/bin/Debug/Penumbra.json'
$content = get-content -path $path
$content = $content -replace '1.0.0.0',$ver
set-content -Path $path -Value $content
- name: Archive
run: Compress-Archive -Path Penumbra/bin/Debug/net5.0-windows/* -DestinationPath Penumbra.zip
run: Compress-Archive -Path Penumbra/bin/Debug/* -DestinationPath Penumbra.zip
- name: Upload a Build Artifact
uses: actions/upload-artifact@v2.2.1
with:
path: |
./Penumbra/bin/Debug/net5.0-windows/*
./Penumbra/bin/Debug/*
- name: Create Release
id: create_release
uses: actions/create-release@v1

@ -1 +1 @@
Subproject commit ea6ebcc073412419a051ac73a697980da20233e2
Subproject commit 88bf221852d4a1ac26f5ffbfb5e497220aef75c4

View file

@ -35,7 +35,7 @@ public static unsafe partial class ByteStringFunctions
var path = ( byte* )Marshal.AllocHGlobal( length + 1 );
fixed( char* ptr = s )
{
Encoding.UTF8.GetBytes( ptr, length, path, length + 1 );
Encoding.UTF8.GetBytes( ptr, s.Length, path, length + 1 );
}
path[ length ] = 0;

View file

@ -36,7 +36,7 @@ public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf
return true;
}
var substring = s!.Replace( '/', '\\' ).TrimStart('\\');
var substring = s.Replace( '/', '\\' ).TrimStart('\\');
if( substring.Length > MaxRelPathLength )
{
return false;

View file

@ -0,0 +1,6 @@
namespace Penumbra.GameData.Files;
public interface IWritable
{
public byte[] Write();
}

View file

@ -2,12 +2,13 @@ using System;
using System.IO;
using System.Reflection;
using System.Text;
using Lumina.Data;
using Lumina.Data.Parsing;
using Lumina.Extensions;
namespace Penumbra.GameData.Files;
public partial class MdlFile
public partial class MdlFile : IWritable
{
public const uint NumVertices = 17;
public const uint FileHeaderSize = 0x44;
@ -87,7 +88,7 @@ public partial class MdlFile
public MdlFile( byte[] data )
{
using var stream = new MemoryStream( data );
using var r = new BinaryReader( stream );
using var r = new LuminaBinaryReader( stream );
var header = LoadModelFileHeader( r );
LodCount = header.LodCount;
@ -197,7 +198,7 @@ public partial class MdlFile
RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) );
}
private MdlStructs.ModelFileHeader LoadModelFileHeader( BinaryReader r )
private MdlStructs.ModelFileHeader LoadModelFileHeader( LuminaBinaryReader r )
{
var header = MdlStructs.ModelFileHeader.Read( r );
Version = header.Version;
@ -255,5 +256,4 @@ public partial class MdlFile
public unsafe uint StackSize
=> ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) );
}

View file

@ -0,0 +1,112 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace Penumbra.GameData.Files;
public partial class MtrlFile
{
public byte[] Write()
{
using var stream = new MemoryStream();
using( var w = new BinaryWriter( stream ) )
{
const int materialHeaderSize = 4 + 2 + 2 + 2 + 2 + 1 + 1 + 1 + 1;
w.BaseStream.Seek( materialHeaderSize, SeekOrigin.Begin );
ushort cumulativeStringOffset = 0;
foreach( var texture in Textures )
{
w.Write( cumulativeStringOffset );
w.Write( texture.Flags );
cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 );
}
foreach( var set in UvSets )
{
w.Write( cumulativeStringOffset );
w.Write( set.Index );
cumulativeStringOffset += ( ushort )( set.Name.Length + 1 );
}
foreach( var set in ColorSets )
{
w.Write( cumulativeStringOffset );
w.Write( set.Index );
cumulativeStringOffset += ( ushort )( set.Name.Length + 1 );
}
foreach( var text in Textures.Select( t => t.Path )
.Concat( UvSets.Select( c => c.Name ) )
.Concat( ColorSets.Select( c => c.Name ) )
.Append( ShaderPackage.Name ) )
{
w.Write( Encoding.UTF8.GetBytes( text ) );
w.Write( ( byte )'\0' );
}
w.Write( AdditionalData );
foreach( var row in ColorSets.Select( c => c.Rows ) )
{
w.Write( row.AsBytes() );
}
foreach( var row in ColorDyeSets.Select( c => c.Rows ) )
{
w.Write( row.AsBytes() );
}
w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) );
w.Write( ( ushort )ShaderPackage.ShaderKeys.Length );
w.Write( ( ushort )ShaderPackage.Constants.Length );
w.Write( ( ushort )ShaderPackage.Samplers.Length );
w.Write( ShaderPackage.Unk );
foreach( var key in ShaderPackage.ShaderKeys )
{
w.Write( key.Category );
w.Write( key.Value );
}
foreach( var constant in ShaderPackage.Constants )
{
w.Write( constant.Id );
w.Write( constant.Value );
}
foreach( var sampler in ShaderPackage.Samplers )
{
w.Write( sampler.SamplerId );
w.Write( sampler.Flags );
w.Write( sampler.TextureIndex );
w.Write( ( ushort )0 );
w.Write( ( byte )0 );
}
foreach( var value in ShaderPackage.ShaderValues )
{
w.Write( value );
}
WriteHeader( w, ( ushort )w.BaseStream.Position, cumulativeStringOffset );
}
return stream.ToArray();
}
private void WriteHeader( BinaryWriter w, ushort fileSize, ushort shaderPackageNameOffset )
{
w.BaseStream.Seek( 0, SeekOrigin.Begin );
w.Write( Version );
w.Write( fileSize );
w.Write( ( ushort )( ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size
+ ColorDyeSets.Length * ColorDyeSet.RowArray.NumRows * 2 ) );
w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) );
w.Write( shaderPackageNameOffset );
w.Write( ( byte )Textures.Length );
w.Write( ( byte )UvSets.Length );
w.Write( ( byte )ColorSets.Length );
w.Write( ( byte )AdditionalData.Length );
}
}

View file

@ -0,0 +1,373 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using Lumina.Data.Parsing;
using Lumina.Extensions;
namespace Penumbra.GameData.Files;
public partial class MtrlFile : IWritable
{
public struct UvSet
{
public string Name;
public ushort Index;
}
public unsafe struct ColorSet
{
public struct Row
{
public const int Size = 32;
private fixed ushort _data[16];
public Vector3 Diffuse
{
get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 ));
set
{
_data[ 0 ] = FromFloat( value.X );
_data[ 1 ] = FromFloat( value.Y );
_data[ 2 ] = FromFloat( value.Z );
}
}
public Vector3 Specular
{
get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 ));
set
{
_data[ 4 ] = FromFloat( value.X );
_data[ 5 ] = FromFloat( value.Y );
_data[ 6 ] = FromFloat( value.Z );
}
}
public Vector3 Emissive
{
get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 ));
set
{
_data[ 8 ] = FromFloat( value.X );
_data[ 9 ] = FromFloat( value.Y );
_data[ 10 ] = FromFloat( value.Z );
}
}
public Vector2 MaterialRepeat
{
get => new(ToFloat( 12 ), ToFloat( 15 ));
set
{
_data[ 12 ] = FromFloat( value.X );
_data[ 15 ] = FromFloat( value.Y );
}
}
public Vector2 MaterialSkew
{
get => new(ToFloat( 13 ), ToFloat( 14 ));
set
{
_data[ 13 ] = FromFloat( value.X );
_data[ 14 ] = FromFloat( value.Y );
}
}
public float SpecularStrength
{
get => ToFloat( 3 );
set => _data[ 3 ] = FromFloat( value );
}
public float GlossStrength
{
get => ToFloat( 7 );
set => _data[ 7 ] = FromFloat( value );
}
public ushort TileSet
{
get => (ushort) (ToFloat(11) * 64f);
set => _data[ 11 ] = FromFloat(value / 64f);
}
private float ToFloat( int idx )
=> ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] );
private static ushort FromFloat( float x )
=> BitConverter.HalfToUInt16Bits( ( Half )x );
}
public struct RowArray : IEnumerable< Row >
{
public const int NumRows = 16;
private fixed byte _rowData[NumRows * Row.Size];
public ref Row this[ int i ]
{
get
{
fixed( byte* ptr = _rowData )
{
return ref ( ( Row* )ptr )[ i ];
}
}
}
public IEnumerator< Row > GetEnumerator()
{
for( var i = 0; i < NumRows; ++i )
{
yield return this[ i ];
}
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ReadOnlySpan< byte > AsBytes()
{
fixed( byte* ptr = _rowData )
{
return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size );
}
}
}
public RowArray Rows;
public string Name;
public ushort Index;
}
public unsafe struct ColorDyeSet
{
public struct Row
{
private ushort _data;
public ushort Template
{
get => ( ushort )( _data >> 5 );
set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) );
}
public bool Diffuse
{
get => ( _data & 0x01 ) != 0;
set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE );
}
public bool Specular
{
get => ( _data & 0x02 ) != 0;
set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD );
}
public bool Emissive
{
get => ( _data & 0x04 ) != 0;
set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB );
}
public bool Gloss
{
get => ( _data & 0x08 ) != 0;
set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 );
}
public bool SpecularStrength
{
get => ( _data & 0x10 ) != 0;
set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF );
}
}
public struct RowArray : IEnumerable< Row >
{
public const int NumRows = 16;
private fixed ushort _rowData[NumRows];
public ref Row this[ int i ]
{
get
{
fixed( ushort* ptr = _rowData )
{
return ref ( ( Row* )ptr )[ i ];
}
}
}
public IEnumerator< Row > GetEnumerator()
{
for( var i = 0; i < NumRows; ++i )
{
yield return this[ i ];
}
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public ReadOnlySpan< byte > AsBytes()
{
fixed( ushort* ptr = _rowData )
{
return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) );
}
}
}
public RowArray Rows;
public string Name;
public ushort Index;
}
public struct Texture
{
public string Path;
public ushort Flags;
}
public struct Constant
{
public uint Id;
public uint Value;
}
public struct ShaderPackageData
{
public string Name;
public ShaderKey[] ShaderKeys;
public Constant[] Constants;
public Sampler[] Samplers;
public float[] ShaderValues;
public uint Unk;
}
public readonly uint Version;
public Texture[] Textures;
public UvSet[] UvSets;
public ColorSet[] ColorSets;
public ColorDyeSet[] ColorDyeSets;
public ShaderPackageData ShaderPackage;
public byte[] AdditionalData;
public MtrlFile( byte[] data )
{
using var stream = new MemoryStream( data );
using var r = new BinaryReader( stream );
Version = r.ReadUInt32();
r.ReadUInt16(); // file size
var dataSetSize = r.ReadUInt16();
var stringTableSize = r.ReadUInt16();
var shaderPackageNameOffset = r.ReadUInt16();
var textureCount = r.ReadByte();
var uvSetCount = r.ReadByte();
var colorSetCount = r.ReadByte();
var additionalDataSize = r.ReadByte();
Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets );
UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets );
ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets );
var strings = r.ReadBytes( stringTableSize );
for( var i = 0; i < textureCount; ++i )
{
Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] );
}
for( var i = 0; i < uvSetCount; ++i )
{
UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] );
}
for( var i = 0; i < colorSetCount; ++i )
{
ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] );
}
ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize
? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray()
: Array.Empty< ColorDyeSet >();
ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset );
AdditionalData = r.ReadBytes( additionalDataSize );
for( var i = 0; i < ColorSets.Length; ++i )
{
ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >();
}
for( var i = 0; i < ColorDyeSets.Length; ++i )
{
ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >();
}
var shaderValueListSize = r.ReadUInt16();
var shaderKeyCount = r.ReadUInt16();
var constantCount = r.ReadUInt16();
var samplerCount = r.ReadUInt16();
ShaderPackage.Unk = r.ReadUInt32();
ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount );
ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount );
ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount );
ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 );
}
private static Texture[] ReadTextureOffsets( BinaryReader r, int count, out ushort[] offsets )
{
var ret = new Texture[count];
offsets = new ushort[count];
for( var i = 0; i < count; ++i )
{
offsets[ i ] = r.ReadUInt16();
ret[ i ].Flags = r.ReadUInt16();
}
return ret;
}
private static UvSet[] ReadUvSetOffsets( BinaryReader r, int count, out ushort[] offsets )
{
var ret = new UvSet[count];
offsets = new ushort[count];
for( var i = 0; i < count; ++i )
{
offsets[ i ] = r.ReadUInt16();
ret[ i ].Index = r.ReadUInt16();
}
return ret;
}
private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets )
{
var ret = new ColorSet[count];
offsets = new ushort[count];
for( var i = 0; i < count; ++i )
{
offsets[ i ] = r.ReadUInt16();
ret[ i ].Index = r.ReadUInt16();
}
return ret;
}
private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset )
{
strings = strings[ offset.. ];
var end = strings.IndexOf( ( byte )'\0' );
return Encoding.UTF8.GetString( strings[ ..end ] );
}
}

View file

@ -14,9 +14,9 @@ public static class GameData
internal static ObjectIdentification? Identification;
internal static readonly GamePathParser GamePathParser = new();
public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage )
public static IObjectIdentifier GetIdentifier( DataManager dataManager )
{
Identification ??= new ObjectIdentification( dataManager, clientLanguage );
Identification ??= new ObjectIdentification( dataManager, dataManager.Language );
return Identification;
}

View file

@ -1,18 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<AssemblyTitle>Penumbra.GameData</AssemblyTitle>
<Company>absolute gangstas</Company>
<Product>Penumbra</Product>
<Copyright>Copyright © 2020</Copyright>
<Copyright>Copyright © 2022</Copyright>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<OutputPath>bin\$(Configuration)\</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@ -28,24 +30,26 @@
<MSBuildWarningsAsMessages>$(MSBuildWarningsAsMessages);MSB3277</MSBuildWarningsAsMessages>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3">
<Private>false</Private>
</PackageReference>
</ItemGroup>
</Project>

View file

@ -1,13 +1,9 @@
using System;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Util;
namespace Penumbra.GameData.Structs;
public unsafe struct CharacterArmorData
{
public fixed byte Data[40];
}
public readonly unsafe struct CharacterEquip
{
public static readonly CharacterEquip Null = new(null);
@ -26,7 +22,6 @@ public readonly unsafe struct CharacterEquip
public ref CharacterArmor this[ EquipSlot slot ]
=> ref _armor[ IndexOf( slot ) ];
public ref CharacterArmor Head
=> ref _armor[ 0 ];
@ -108,4 +103,13 @@ public readonly unsafe struct CharacterEquip
_ => throw new ArgumentOutOfRangeException( nameof( slot ), slot, null ),
};
}
public void Load( CharacterEquip source )
{
Functions.MemCpyUnchecked( _armor, source._armor, sizeof( CharacterArmor ) * 10 );
}
public bool Equals( CharacterEquip other )
=> Functions.MemCmpUnchecked( ( void* )_armor, ( void* )other._armor, sizeof( CharacterArmor ) * 10 ) == 0;
}

View file

@ -32,6 +32,14 @@ public readonly struct CharacterWeapon : IEquatable< CharacterWeapon >
Stain = stain;
}
public CharacterWeapon( ulong value )
{
Set = ( SetId )value;
Type = ( WeaponType )( value >> 16 );
Variant = ( ushort )( value >> 32 );
Stain = ( StainId )( value >> 48 );
}
public static readonly CharacterWeapon Empty = new(0, 0, 0, 0);
public bool Equals( CharacterWeapon other )

View file

@ -17,7 +17,7 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData >
}
}
public void Write( void* target )
public readonly void Write( void* target )
{
fixed( byte* ptr = Data )
{
@ -25,14 +25,14 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData >
}
}
public CustomizeData Clone()
public readonly CustomizeData Clone()
{
var ret = new CustomizeData();
Write( ret.Data );
return ret;
}
public bool Equals( CustomizeData other )
public readonly bool Equals( CustomizeData other )
{
fixed( byte* ptr = Data )
{
@ -40,6 +40,9 @@ public unsafe struct CustomizeData : IEquatable< CustomizeData >
}
}
public static bool Equals( CustomizeData* lhs, CustomizeData* rhs )
=> Functions.MemCmpUnchecked( lhs, rhs, Size ) == 0;
public override bool Equals( object? obj )
=> obj is CustomizeData other && Equals( other );

View file

@ -26,4 +26,10 @@ public readonly struct WeaponType : IEquatable< WeaponType >
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==( WeaponType lhs, WeaponType rhs )
=> lhs.Value == rhs.Value;
public static bool operator !=( WeaponType lhs, WeaponType rhs )
=> lhs.Value != rhs.Value;
}

View file

@ -26,6 +26,8 @@ public delegate void ModSettingChanged( ModSettingChange type, string collection
public delegate void CreatingCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize,
IntPtr equipData );
public delegate void CreatedCharacterBaseDelegate( IntPtr gameObject, ModCollection collection, IntPtr drawObject );
public enum PenumbraApiEc
{
Success = 0,
@ -49,6 +51,10 @@ public interface IPenumbraApi : IPenumbraApiBase
// Obtain the currently set mod directory from the configuration.
public string GetModDirectory();
// Fired whenever a mod directory change is finished.
// Gives the full path of the mod directory and whether Penumbra treats it as valid.
public event Action< string, bool >? ModDirectoryChanged;
// Obtain the entire current penumbra configuration as a json encoded string.
public string GetConfiguration();
@ -69,6 +75,10 @@ public interface IPenumbraApi : IPenumbraApiBase
// before the Draw Object is actually created, so customize and equipdata can be manipulated beforehand.
public event CreatingCharacterBaseDelegate? CreatingCharacterBase;
// Triggered after a character base was created if a corresponding gameObject could be found,
// so you can apply flag changes after finishing.
public event CreatedCharacterBaseDelegate? CreatedCharacterBase;
// Queue redrawing of all actors of the given name with the given RedrawType.
public void RedrawObject( string name, RedrawType setting );
@ -124,6 +134,9 @@ public interface IPenumbraApi : IPenumbraApiBase
// Obtain the game object associated with a given draw object and the name of the collection associated with this game object.
public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject );
// Obtain the parent game object index for an unnamed cutscene actor by its index.
public int GetCutsceneParentIndex( int actor );
// Obtain a list of all installed mods. The first string is their directory name, the second string is their mod name.
public IList< (string, string) > GetModList();

View file

@ -28,9 +28,11 @@ public class IpcTester : IDisposable
private readonly ICallGateSubscriber< object? > _disposed;
private readonly ICallGateSubscriber< string, object? > _preSettingsDraw;
private readonly ICallGateSubscriber< string, object? > _postSettingsDraw;
private readonly ICallGateSubscriber< string, bool, object? > _modDirectoryChanged;
private readonly ICallGateSubscriber< IntPtr, int, object? > _redrawn;
private readonly ICallGateSubscriber< ModSettingChange, string, string, bool, object? > _settingChanged;
private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreated;
private readonly ICallGateSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? > _characterBaseCreating;
private readonly ICallGateSubscriber< IntPtr, string, IntPtr, object? > _characterBaseCreated;
private readonly List< DateTimeOffset > _initializedList = new();
private readonly List< DateTimeOffset > _disposedList = new();
@ -45,15 +47,19 @@ public class IpcTester : IDisposable
_preSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPreSettingsDraw );
_postSettingsDraw = _pi.GetIpcSubscriber< string, object? >( PenumbraIpc.LabelProviderPostSettingsDraw );
_settingChanged = _pi.GetIpcSubscriber< ModSettingChange, string, string, bool, object? >( PenumbraIpc.LabelProviderModSettingChanged );
_characterBaseCreated =
_modDirectoryChanged = _pi.GetIpcSubscriber< string, bool, object? >( PenumbraIpc.LabelProviderModDirectoryChanged );
_characterBaseCreating =
_pi.GetIpcSubscriber< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >( PenumbraIpc.LabelProviderCreatingCharacterBase );
_characterBaseCreated = _pi.GetIpcSubscriber< IntPtr, string, IntPtr, object? >( PenumbraIpc.LabelProviderCreatedCharacterBase );
_initialized.Subscribe( AddInitialized );
_disposed.Subscribe( AddDisposed );
_redrawn.Subscribe( SetLastRedrawn );
_preSettingsDraw.Subscribe( UpdateLastDrawnMod );
_postSettingsDraw.Subscribe( UpdateLastDrawnMod );
_settingChanged.Subscribe( UpdateLastModSetting );
_characterBaseCreated.Subscribe( UpdateLastCreated );
_characterBaseCreating.Subscribe( UpdateLastCreated );
_characterBaseCreated.Subscribe( UpdateLastCreated2 );
_modDirectoryChanged.Subscribe( UpdateModDirectoryChanged );
}
public void Dispose()
@ -66,7 +72,9 @@ public class IpcTester : IDisposable
_preSettingsDraw.Unsubscribe( UpdateLastDrawnMod );
_postSettingsDraw.Unsubscribe( UpdateLastDrawnMod );
_settingChanged.Unsubscribe( UpdateLastModSetting );
_characterBaseCreated.Unsubscribe( UpdateLastCreated );
_characterBaseCreating.Unsubscribe( UpdateLastCreated );
_characterBaseCreated.Unsubscribe( UpdateLastCreated2 );
_modDirectoryChanged.Unsubscribe( UpdateModDirectoryChanged );
}
private void AddInitialized()
@ -131,11 +139,18 @@ public class IpcTester : IDisposable
private string _currentConfiguration = string.Empty;
private string _lastDrawnMod = string.Empty;
private DateTimeOffset _lastDrawnModTime;
private DateTimeOffset _lastDrawnModTime = DateTimeOffset.MinValue;
private void UpdateLastDrawnMod( string name )
=> ( _lastDrawnMod, _lastDrawnModTime ) = ( name, DateTimeOffset.Now );
private string _lastModDirectory = string.Empty;
private bool _lastModDirectoryValid = false;
private DateTimeOffset _lastModDirectoryTime = DateTimeOffset.MinValue;
private void UpdateModDirectoryChanged( string path, bool valid )
=> ( _lastModDirectory, _lastModDirectoryValid, _lastModDirectoryTime ) = ( path, valid, DateTimeOffset.Now );
private void DrawGeneral()
{
using var _ = ImRaii.TreeNode( "General IPC" );
@ -173,6 +188,10 @@ public class IpcTester : IDisposable
ImGui.TextUnformatted( $"{breaking}.{features:D4}" );
DrawIntro( PenumbraIpc.LabelProviderGetModDirectory, "Current Mod Directory" );
ImGui.TextUnformatted( _pi.GetIpcSubscriber< string >( PenumbraIpc.LabelProviderGetModDirectory ).InvokeFunc() );
DrawIntro( PenumbraIpc.LabelProviderModDirectoryChanged, "Last Mod Directory Change" );
ImGui.TextUnformatted( _lastModDirectoryTime > DateTimeOffset.MinValue
? $"{_lastModDirectory} ({( _lastModDirectoryValid ? "Valid" : "Invalid" )}) at {_lastModDirectoryTime}"
: "None" );
DrawIntro( PenumbraIpc.LabelProviderGetConfiguration, "Configuration" );
if( ImGui.Button( "Get" ) )
{
@ -201,7 +220,9 @@ public class IpcTester : IDisposable
private string _currentDrawObjectString = string.Empty;
private string _currentReversePath = string.Empty;
private IntPtr _currentDrawObject = IntPtr.Zero;
private int _currentCutsceneActor = 0;
private string _lastCreatedGameObjectName = string.Empty;
private IntPtr _lastCreatedDrawObject = IntPtr.Zero;
private DateTimeOffset _lastCreatedGameObjectTime = DateTimeOffset.MaxValue;
private unsafe void UpdateLastCreated( IntPtr gameObject, string _, IntPtr _2, IntPtr _3, IntPtr _4 )
@ -209,6 +230,15 @@ public class IpcTester : IDisposable
var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject;
_lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString();
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = IntPtr.Zero;
}
private unsafe void UpdateLastCreated2( IntPtr gameObject, string _, IntPtr drawObject )
{
var obj = ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )gameObject;
_lastCreatedGameObjectName = new Utf8String( obj->GetName() ).ToString();
_lastCreatedGameObjectTime = DateTimeOffset.Now;
_lastCreatedDrawObject = drawObject;
}
private void DrawResolve()
@ -231,6 +261,8 @@ public class IpcTester : IDisposable
: IntPtr.Zero;
}
ImGui.InputInt( "Cutscene Actor", ref _currentCutsceneActor, 0 );
using var table = ImRaii.Table( string.Empty, 3, ImGuiTableFlags.SizingFixedFit );
if( !table )
{
@ -263,6 +295,10 @@ public class IpcTester : IDisposable
ImGui.TextUnformatted( ptr == IntPtr.Zero ? $"No Actor Associated, {collection}" : $"{ptr:X}, {collection}" );
}
DrawIntro( PenumbraIpc.LabelProviderGetDrawObjectInfo, "Cutscene Parent" );
ImGui.TextUnformatted( _pi.GetIpcSubscriber< int, int >( PenumbraIpc.LabelProviderGetCutsceneParentIndex )
.InvokeFunc( _currentCutsceneActor ).ToString() );
DrawIntro( PenumbraIpc.LabelProviderReverseResolvePath, "Reversed Game Paths" );
if( _currentReversePath.Length > 0 )
{
@ -296,7 +332,9 @@ public class IpcTester : IDisposable
DrawIntro( PenumbraIpc.LabelProviderCreatingCharacterBase, "Last Drawobject created" );
if( _lastCreatedGameObjectTime < DateTimeOffset.Now )
{
ImGui.TextUnformatted( $"for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" );
ImGui.TextUnformatted( _lastCreatedDrawObject != IntPtr.Zero
? $"0x{_lastCreatedDrawObject:X} for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}"
: $"NULL for <{_lastCreatedGameObjectName}> at {_lastCreatedGameObjectTime}" );
}
}

View file

@ -6,7 +6,6 @@ using System.Linq;
using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Lumina.Data;
using Newtonsoft.Json;
using OtterGui;
@ -16,14 +15,13 @@ using Penumbra.GameData.Enums;
using Penumbra.Interop.Resolver;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Api;
public class PenumbraApi : IDisposable, IPenumbraApi
{
public (int, int) ApiVersion
=> ( 4, 11 );
=> ( 4, 12 );
private Penumbra? _penumbra;
private Lumina.GameData? _lumina;
@ -43,8 +41,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public event CreatingCharacterBaseDelegate? CreatingCharacterBase
{
add => _penumbra!.PathResolver.CreatingCharacterBase += value;
remove => _penumbra!.PathResolver.CreatingCharacterBase -= value;
add => PathResolver.DrawObjectState.CreatingCharacterBase += value;
remove => PathResolver.DrawObjectState.CreatingCharacterBase -= value;
}
public event CreatedCharacterBaseDelegate? CreatedCharacterBase
{
add => PathResolver.DrawObjectState.CreatedCharacterBase += value;
remove => PathResolver.DrawObjectState.CreatedCharacterBase -= value;
}
public bool Valid
@ -54,8 +58,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
_penumbra = penumbra;
_lumina = ( Lumina.GameData? )Dalamud.GameData.GetType()
.GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic )
?.GetValue( Dalamud.GameData );
.GetField( "gameData", BindingFlags.Instance | BindingFlags.NonPublic )
?.GetValue( Dalamud.GameData );
foreach( var collection in Penumbra.CollectionManager )
{
SubscribeToCollection( collection );
@ -86,6 +90,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return Penumbra.Config.ModDirectory;
}
public event Action< string, bool >? ModDirectoryChanged
{
add => Penumbra.ModManager.ModDirectoryChanged += value;
remove => Penumbra.ModManager.ModDirectoryChanged -= value;
}
public string GetConfiguration()
{
CheckInitialized();
@ -221,10 +231,16 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public (IntPtr, string) GetDrawObjectInfo( IntPtr drawObject )
{
CheckInitialized();
var (obj, collection) = _penumbra!.PathResolver.IdentifyDrawObject( drawObject );
var (obj, collection) = PathResolver.IdentifyDrawObject( drawObject );
return ( obj, collection.Name );
}
public int GetCutsceneParentIndex( int actor )
{
CheckInitialized();
return _penumbra!.PathResolver.CutsceneActor( actor );
}
public IList< (string, string) > GetModList()
{
CheckInitialized();
@ -429,7 +445,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
}
if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character )
|| Penumbra.TempMods.Collections.ContainsKey( character ) )
|| Penumbra.TempMods.Collections.ContainsKey( character ) )
{
return ( PenumbraApiEc.CharacterCollectionExists, string.Empty );
}
@ -475,7 +491,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
@ -512,7 +528,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{
CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{
return PenumbraApiEc.CollectionMissing;
}

View file

@ -26,6 +26,7 @@ public partial class PenumbraIpc : IDisposable
InitializeSettingProviders( pi );
InitializeTempProviders( pi );
ProviderInitialized?.SendMessage();
InvokeModDirectoryChanged( Penumbra.ModManager.BasePath.FullName, Penumbra.ModManager.Valid );
}
public void Dispose()
@ -44,20 +45,22 @@ public partial class PenumbraIpc : IDisposable
public partial class PenumbraIpc
{
public const string LabelProviderInitialized = "Penumbra.Initialized";
public const string LabelProviderDisposed = "Penumbra.Disposed";
public const string LabelProviderApiVersion = "Penumbra.ApiVersion";
public const string LabelProviderApiVersions = "Penumbra.ApiVersions";
public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory";
public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration";
public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw";
public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw";
public const string LabelProviderInitialized = "Penumbra.Initialized";
public const string LabelProviderDisposed = "Penumbra.Disposed";
public const string LabelProviderApiVersion = "Penumbra.ApiVersion";
public const string LabelProviderApiVersions = "Penumbra.ApiVersions";
public const string LabelProviderGetModDirectory = "Penumbra.GetModDirectory";
public const string LabelProviderModDirectoryChanged = "Penumbra.ModDirectoryChanged";
public const string LabelProviderGetConfiguration = "Penumbra.GetConfiguration";
public const string LabelProviderPreSettingsDraw = "Penumbra.PreSettingsDraw";
public const string LabelProviderPostSettingsDraw = "Penumbra.PostSettingsDraw";
internal ICallGateProvider< object? >? ProviderInitialized;
internal ICallGateProvider< object? >? ProviderDisposed;
internal ICallGateProvider< int >? ProviderApiVersion;
internal ICallGateProvider< (int Breaking, int Features) >? ProviderApiVersions;
internal ICallGateProvider< string >? ProviderGetModDirectory;
internal ICallGateProvider< string, bool, object? >? ProviderModDirectoryChanged;
internal ICallGateProvider< string >? ProviderGetConfiguration;
internal ICallGateProvider< string, object? >? ProviderPreSettingsDraw;
internal ICallGateProvider< string, object? >? ProviderPostSettingsDraw;
@ -116,6 +119,16 @@ public partial class PenumbraIpc
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetModDirectory}:\n{e}" );
}
try
{
ProviderModDirectoryChanged = pi.GetIpcProvider< string, bool, object? >( LabelProviderModDirectoryChanged );
Api.ModDirectoryChanged += InvokeModDirectoryChanged;
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderModDirectoryChanged}:\n{e}" );
}
try
{
ProviderGetConfiguration = pi.GetIpcProvider< string >( LabelProviderGetConfiguration );
@ -155,7 +168,17 @@ public partial class PenumbraIpc
ProviderApiVersions?.UnregisterFunc();
Api.PreSettingsPanelDraw -= InvokeSettingsPreDraw;
Api.PostSettingsPanelDraw -= InvokeSettingsPostDraw;
Api.ModDirectoryChanged -= InvokeModDirectoryChanged;
}
private void InvokeSettingsPreDraw( string modDirectory )
=> ProviderPreSettingsDraw!.SendMessage( modDirectory );
private void InvokeSettingsPostDraw( string modDirectory )
=> ProviderPostSettingsDraw!.SendMessage( modDirectory );
private void InvokeModDirectoryChanged( string modDirectory, bool valid )
=> ProviderModDirectoryChanged?.SendMessage( modDirectory, valid );
}
public partial class PenumbraIpc
@ -239,12 +262,6 @@ public partial class PenumbraIpc
private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex )
=> ProviderGameObjectRedrawn?.SendMessage( objectAddress, objectTableIndex );
private void InvokeSettingsPreDraw( string modDirectory )
=> ProviderPreSettingsDraw!.SendMessage( modDirectory );
private void InvokeSettingsPostDraw( string modDirectory )
=> ProviderPostSettingsDraw!.SendMessage( modDirectory );
private void DisposeRedrawProviders()
{
ProviderRedrawName?.UnregisterAction();
@ -261,17 +278,21 @@ public partial class PenumbraIpc
public const string LabelProviderResolveCharacter = "Penumbra.ResolveCharacterPath";
public const string LabelProviderResolvePlayer = "Penumbra.ResolvePlayerPath";
public const string LabelProviderGetDrawObjectInfo = "Penumbra.GetDrawObjectInfo";
public const string LabelProviderGetCutsceneParentIndex = "Penumbra.GetCutsceneParentIndex";
public const string LabelProviderReverseResolvePath = "Penumbra.ReverseResolvePath";
public const string LabelProviderReverseResolvePlayerPath = "Penumbra.ReverseResolvePlayerPath";
public const string LabelProviderCreatingCharacterBase = "Penumbra.CreatingCharacterBase";
public const string LabelProviderCreatedCharacterBase = "Penumbra.CreatedCharacterBase";
internal ICallGateProvider< string, string >? ProviderResolveDefault;
internal ICallGateProvider< string, string, string >? ProviderResolveCharacter;
internal ICallGateProvider< string, string >? ProviderResolvePlayer;
internal ICallGateProvider< IntPtr, (IntPtr, string) >? ProviderGetDrawObjectInfo;
internal ICallGateProvider< int, int >? ProviderGetCutsceneParentIndex;
internal ICallGateProvider< string, string, string[] >? ProviderReverseResolvePath;
internal ICallGateProvider< string, string[] >? ProviderReverseResolvePathPlayer;
internal ICallGateProvider< IntPtr, string, IntPtr, IntPtr, IntPtr, object? >? ProviderCreatingCharacterBase;
internal ICallGateProvider< IntPtr, string, IntPtr, object? >? ProviderCreatedCharacterBase;
private void InitializeResolveProviders( DalamudPluginInterface pi )
{
@ -315,6 +336,16 @@ public partial class PenumbraIpc
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetDrawObjectInfo}:\n{e}" );
}
try
{
ProviderGetCutsceneParentIndex = pi.GetIpcProvider< int, int >( LabelProviderGetCutsceneParentIndex );
ProviderGetCutsceneParentIndex.RegisterFunc( Api.GetCutsceneParentIndex );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCutsceneParentIndex}:\n{e}" );
}
try
{
ProviderReverseResolvePath = pi.GetIpcProvider< string, string, string[] >( LabelProviderReverseResolvePath );
@ -345,22 +376,40 @@ public partial class PenumbraIpc
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatingCharacterBase}:\n{e}" );
}
try
{
ProviderCreatedCharacterBase =
pi.GetIpcProvider< IntPtr, string, IntPtr, object? >( LabelProviderCreatedCharacterBase );
Api.CreatedCharacterBase += CreatedCharacterBaseEvent;
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreatedCharacterBase}:\n{e}" );
}
}
private void DisposeResolveProviders()
{
ProviderGetDrawObjectInfo?.UnregisterFunc();
ProviderGetCutsceneParentIndex?.UnregisterFunc();
ProviderResolveDefault?.UnregisterFunc();
ProviderResolveCharacter?.UnregisterFunc();
ProviderReverseResolvePath?.UnregisterFunc();
ProviderReverseResolvePathPlayer?.UnregisterFunc();
Api.CreatingCharacterBase -= CreatingCharacterBaseEvent;
Api.CreatedCharacterBase -= CreatedCharacterBaseEvent;
}
private void CreatingCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr modelId, IntPtr customize, IntPtr equipData )
{
ProviderCreatingCharacterBase?.SendMessage( gameObject, collection.Name, modelId, customize, equipData );
}
private void CreatedCharacterBaseEvent( IntPtr gameObject, ModCollection collection, IntPtr drawObject )
{
ProviderCreatedCharacterBase?.SendMessage( gameObject, collection.Name, drawObject );
}
}
public partial class PenumbraIpc

View file

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Mods;
using Penumbra.UI;
namespace Penumbra.Collections;
@ -196,7 +197,7 @@ public partial class ModCollection
var defaultIdx = GetIndexForCollectionName( defaultName );
if( defaultIdx < 0 )
{
PluginLog.Error( $"Last choice of Default Collection {defaultName} is not available, reset to {Empty.Name}." );
PluginLog.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." );
Default = Empty;
configChanged = true;
}
@ -210,7 +211,7 @@ public partial class ModCollection
var currentIdx = GetIndexForCollectionName( currentName );
if( currentIdx < 0 )
{
PluginLog.Error( $"Last choice of Current Collection {currentName} is not available, reset to {DefaultCollection}." );
PluginLog.Error( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." );
Current = DefaultName;
configChanged = true;
}

View file

@ -1,148 +0,0 @@
using System;
using System.Collections.Generic;
using OtterGui.Classes;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Collections;
public struct ConflictCache
{
// A conflict stores all data about a mod conflict.
public readonly struct Conflict : IComparable< Conflict >
{
public readonly object Data;
public readonly int Mod1;
public readonly int Mod2;
public readonly bool Mod1Priority;
public readonly bool Solved;
public Conflict( int modIdx1, int modIdx2, bool priority, bool solved, object data )
{
Mod1 = modIdx1;
Mod2 = modIdx2;
Data = data;
Mod1Priority = priority;
Solved = solved;
}
// Order: Mod1 -> Mod1 overwritten -> Mod2 -> File > MetaManipulation
public int CompareTo( Conflict other )
{
var idxComp = Mod1.CompareTo( other.Mod1 );
if( idxComp != 0 )
{
return idxComp;
}
if( Mod1Priority != other.Mod1Priority )
{
return Mod1Priority ? 1 : -1;
}
idxComp = Mod2.CompareTo( other.Mod2 );
if( idxComp != 0 )
{
return idxComp;
}
return Data switch
{
Utf8GamePath p when other.Data is Utf8GamePath q => p.CompareTo( q ),
Utf8GamePath => -1,
MetaManipulation m when other.Data is MetaManipulation n => m.CompareTo( n ),
MetaManipulation => 1,
_ => 0,
};
}
public override string ToString()
=> ( Mod1Priority, Solved ) switch
{
(true, true) => $"{Penumbra.ModManager[ Mod1 ].Name} > {Penumbra.ModManager[ Mod2 ].Name} ({Data})",
(true, false) => $"{Penumbra.ModManager[ Mod1 ].Name} >= {Penumbra.ModManager[ Mod2 ].Name} ({Data})",
(false, true) => $"{Penumbra.ModManager[ Mod1 ].Name} < {Penumbra.ModManager[ Mod2 ].Name} ({Data})",
(false, false) => $"{Penumbra.ModManager[ Mod1 ].Name} <= {Penumbra.ModManager[ Mod2 ].Name} ({Data})",
};
}
private readonly List< Conflict > _conflicts = new();
private bool _isSorted = true;
public ConflictCache()
{ }
public IReadOnlyList< Conflict > Conflicts
{
get
{
Sort();
return _conflicts;
}
}
// Find all mod conflicts concerning the specified mod (in both directions).
public SubList< Conflict > ModConflicts( int modIdx )
{
Sort();
var start = _conflicts.FindIndex( c => c.Mod1 == modIdx );
if( start < 0 )
{
return SubList< Conflict >.Empty;
}
var end = _conflicts.FindIndex( start, c => c.Mod1 != modIdx );
return new SubList< Conflict >( _conflicts, start, end - start );
}
private void Sort()
{
if( !_isSorted )
{
_conflicts?.Sort();
_isSorted = true;
}
}
// Add both directions for the mod.
// On same priority, it is assumed that mod1 is the earlier one.
// Also update older conflicts to refer to the highest-prioritized conflict.
private void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, object data )
{
var solved = priority1 != priority2;
var priority = priority1 >= priority2;
var prioritizedMod = priority ? modIdx1 : modIdx2;
_conflicts.Add( new Conflict( modIdx1, modIdx2, priority, solved, data ) );
_conflicts.Add( new Conflict( modIdx2, modIdx1, !priority, solved, data ) );
for( var i = 0; i < _conflicts.Count; ++i )
{
var c = _conflicts[ i ];
if( data.Equals( c.Data ) )
{
_conflicts[ i ] = c.Mod1Priority
? new Conflict( prioritizedMod, c.Mod2, true, c.Solved || solved, data )
: new Conflict( c.Mod1, prioritizedMod, false, c.Solved || solved, data );
}
}
_isSorted = false;
}
public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, Utf8GamePath gamePath )
=> AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )gamePath );
public void AddConflict( int modIdx1, int modIdx2, int priority1, int priority2, MetaManipulation manipulation )
=> AddConflict( modIdx1, modIdx2, priority1, priority2, ( object )manipulation );
public void ClearConflicts()
=> _conflicts?.Clear();
public void ClearFileConflicts()
=> _conflicts?.RemoveAll( m => m.Data is Utf8GamePath );
public void ClearMetaConflicts()
=> _conflicts?.RemoveAll( m => m.Data is MetaManipulation );
public void ClearConflictsWithMod( int modIdx )
=> _conflicts?.RemoveAll( m => m.Mod1 == modIdx || m.Mod2 == modIdx );
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Logging;
using OtterGui.Classes;
@ -70,7 +72,24 @@ public partial class ModCollection
// Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile( Utf8GamePath path, FullPath fullPath )
=> _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath );
{
if( CheckFullPath( path, fullPath ) )
{
_cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath );
}
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static bool CheckFullPath( Utf8GamePath path, FullPath fullPath )
{
if( fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength )
{
return true;
}
PluginLog.Error( $"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}" );
return false;
}
// Force a file resolve to be removed.
internal void RemoveFile( Utf8GamePath path )

View file

@ -326,6 +326,11 @@ public partial class ModCollection
// Inside the same mod, conflicts are not recorded.
private void AddFile( Utf8GamePath path, FullPath file, IMod mod )
{
if( !CheckFullPath( path, file ) )
{
return;
}
if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) )
{
return;

View file

@ -135,7 +135,7 @@ public partial class ModCollection
var settings = _settings[ idx ];
if( settings != null )
{
_unusedSettings.Add( mod.ModPath.Name, new ModSettings.SavedSettings( settings, mod ) );
_unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings( settings, mod );
}
_settings.RemoveAt( idx );

View file

@ -46,7 +46,9 @@ public class DdsFile
ParseType.R8G8B8A8 => Header.Height * Header.Width * 4,
ParseType.B8G8R8A8 => Header.Height * Header.Width * 4,
_ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ),
ParseType.A16B16G16R16F => Header.Height * Header.Width * 8,
_ => throw new ArgumentOutOfRangeException( nameof( ParseType ), ParseType, null ),
};
if( Header.MipMapCount < level )
@ -89,25 +91,26 @@ public class DdsFile
return ParseType switch
{
ParseType.Unsupported => Array.Empty< byte >(),
ParseType.DXT1 => ImageParsing.DecodeDxt1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.DXT3 => ImageParsing.DecodeDxt3( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.DXT5 => ImageParsing.DecodeDxt5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.BC4 => ImageParsing.DecodeBc4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.BC5 => ImageParsing.DecodeBc5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.Greyscale => ImageParsing.DecodeUncompressedGreyscale( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B4G4R4A4 => ImageParsing.DecodeUncompressedB4G4R4A4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G5B5 => ImageParsing.DecodeUncompressedR5G5B5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G5R5 => ImageParsing.DecodeUncompressedB5G5R5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G6B5 => ImageParsing.DecodeUncompressedR5G6B5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G6R5 => ImageParsing.DecodeUncompressedB5G6R5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G5R5A1 => ImageParsing.DecodeUncompressedB5G5R5A1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R8G8B8 => ImageParsing.DecodeUncompressedR8G8B8( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B8G8R8 => ImageParsing.DecodeUncompressedB8G8R8( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R8G8B8A8 => _data.Length == Header.Width * Header.Height * 4 ? _data : _data[ ..( Header.Width * Header.Height * 4 ) ],
ParseType.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( MipMap( 0 ), Header.Height, Header.Width ),
_ => throw new ArgumentOutOfRangeException(),
ParseType.DXT1 => ImageParsing.DecodeDxt1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.DXT3 => ImageParsing.DecodeDxt3( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.DXT5 => ImageParsing.DecodeDxt5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.BC4 => ImageParsing.DecodeBc4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.BC5 => ImageParsing.DecodeBc5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.Greyscale => ImageParsing.DecodeUncompressedGreyscale( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R4G4B4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B4G4R4A4 => ImageParsing.DecodeUncompressedB4G4R4A4( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G5B5 => ImageParsing.DecodeUncompressedR5G5B5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G5R5 => ImageParsing.DecodeUncompressedB5G5R5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G6B5 => ImageParsing.DecodeUncompressedR5G6B5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G6R5 => ImageParsing.DecodeUncompressedB5G6R5( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R5G5B5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B5G5R5A1 => ImageParsing.DecodeUncompressedB5G5R5A1( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R8G8B8 => ImageParsing.DecodeUncompressedR8G8B8( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.B8G8R8 => ImageParsing.DecodeUncompressedB8G8R8( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.R8G8B8A8 => _data.Length == Header.Width * Header.Height * 4 ? _data : _data[ ..( Header.Width * Header.Height * 4 ) ],
ParseType.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( MipMap( 0 ), Header.Height, Header.Width ),
ParseType.A16B16G16R16F => ImageParsing.DecodeUncompressedA16B16G16R16F( MipMap( 0 ), Header.Height, Header.Width ),
_ => throw new ArgumentOutOfRangeException(),
};
}
@ -126,6 +129,12 @@ public class DdsFile
var dxt10 = header.PixelFormat.FourCC == PixelFormat.FourCCType.DX10 ? ( DXT10Header? )br.ReadStructure< DXT10Header >() : null;
var type = header.PixelFormat.ToParseType( dxt10 );
if( type == ParseType.Unsupported )
{
PluginLog.Error( "DDS format unsupported." );
return false;
}
file = new DdsFile( type, header, br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) ), dxt10 );
return true;
}
@ -224,30 +233,4 @@ public class DdsFile
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
};
}
}
public class TmpTexFile
{
public TexFile.TexHeader Header;
public byte[] RgbaData = Array.Empty< byte >();
public void Load( BinaryReader br )
{
Header = br.ReadStructure< TexFile.TexHeader >();
var data = br.ReadBytes( ( int )( br.BaseStream.Length - br.BaseStream.Position ) );
RgbaData = Header.Format switch
{
TexFile.TextureFormat.L8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ),
TexFile.TextureFormat.A8 => ImageParsing.DecodeUncompressedGreyscale( data, Header.Height, Header.Width ),
TexFile.TextureFormat.DXT1 => ImageParsing.DecodeDxt1( data, Header.Height, Header.Width ),
TexFile.TextureFormat.DXT3 => ImageParsing.DecodeDxt3( data, Header.Height, Header.Width ),
TexFile.TextureFormat.DXT5 => ImageParsing.DecodeDxt5( data, Header.Height, Header.Width ),
TexFile.TextureFormat.B8G8R8A8 => ImageParsing.DecodeUncompressedB8G8R8A8( data, Header.Height, Header.Width ),
TexFile.TextureFormat.B8G8R8X8 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ),
//TexFile.TextureFormat.A8R8G8B82 => ImageParsing.DecodeUncompressedR8G8B8A8( data, Header.Height, Header.Width ),
TexFile.TextureFormat.B4G4R4A4 => ImageParsing.DecodeUncompressedR4G4B4A4( data, Header.Height, Header.Width ),
TexFile.TextureFormat.B5G5R5A1 => ImageParsing.DecodeUncompressedR5G5B5A1( data, Header.Height, Header.Width ),
_ => throw new ArgumentOutOfRangeException(),
};
}
}

View file

@ -12,9 +12,9 @@ public static partial class ImageParsing
{
var ret = new Rgba32
{
R = ( byte )( c & 0x1F ),
R = ( byte )( c >> 11 ),
G = ( byte )( ( c >> 5 ) & 0x3F ),
B = ( byte )( c >> 11 ),
B = ( byte )( c & 0x1F ),
A = 0xFF,
};
@ -577,4 +577,25 @@ public static partial class ImageParsing
return ret;
}
public static unsafe byte[] DecodeUncompressedA16B16G16R16F( ReadOnlySpan< byte > data, int height, int width )
{
Verify( data, height, width, 1, 8 );
var ret = new byte[data.Length / 2];
fixed( byte* r = ret, d = data )
{
var ptr = r;
var input = ( Half* )d;
var end = (Half*) (d + data.Length);
while( input != end )
{
*ptr++ = ( byte )( byte.MaxValue * (float) *input++ );
*ptr++ = ( byte )( byte.MaxValue * (float) *input++ );
*ptr++ = ( byte )( byte.MaxValue * (float) *input++ );
*ptr++ = ( byte )( byte.MaxValue * (float) *input++ );
}
}
return ret;
}
}

View file

@ -26,6 +26,8 @@ public enum ParseType
B8G8R8,
R8G8B8A8,
B8G8R8A8,
A16B16G16R16F,
}
[StructLayout( LayoutKind.Sequential )]
@ -54,23 +56,23 @@ public struct PixelFormat
public enum FourCCType : uint
{
NoCompression = 0,
DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ),
DXT2 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '2' << 24 ),
DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ),
DXT4 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '4' << 24 ),
DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ),
DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ),
ATI1 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '1' << 24 ),
BC4U = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( 'U' << 24 ),
BC45 = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( '5' << 24 ),
ATI2 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '2' << 24 ),
BC5U = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( 'U' << 24 ),
BC55 = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( '5' << 24 ),
NoCompression = 0,
DXT1 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '1' << 24 ),
DXT2 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '2' << 24 ),
DXT3 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '3' << 24 ),
DXT4 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '4' << 24 ),
DXT5 = 'D' | ( 'X' << 8 ) | ( 'T' << 16 ) | ( '5' << 24 ),
DX10 = 'D' | ( 'X' << 8 ) | ( '1' << 16 ) | ( '0' << 24 ),
ATI1 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '1' << 24 ),
BC4U = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( 'U' << 24 ),
BC45 = 'B' | ( 'C' << 8 ) | ( '4' << 16 ) | ( '5' << 24 ),
ATI2 = 'A' | ( 'T' << 8 ) | ( 'I' << 16 ) | ( '2' << 24 ),
BC5U = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( 'U' << 24 ),
BC55 = 'B' | ( 'C' << 8 ) | ( '5' << 16 ) | ( '5' << 24 ),
D3D_A16B16G16R16 = 113,
}
public void Write( BinaryWriter bw )
{
bw.Write( Size );
@ -87,20 +89,21 @@ public struct PixelFormat
{
return FourCC switch
{
FourCCType.NoCompression => HandleUncompressed(),
FourCCType.DXT1 => ParseType.DXT1,
FourCCType.DXT2 => ParseType.Unsupported,
FourCCType.DXT3 => ParseType.DXT3,
FourCCType.DXT4 => ParseType.Unsupported,
FourCCType.DXT5 => ParseType.DXT5,
FourCCType.DX10 => dxt10?.ToParseType() ?? ParseType.Unsupported,
FourCCType.ATI1 => ParseType.BC4,
FourCCType.BC4U => ParseType.BC4,
FourCCType.BC45 => ParseType.BC4,
FourCCType.ATI2 => ParseType.BC5,
FourCCType.BC5U => ParseType.BC5,
FourCCType.BC55 => ParseType.BC5,
_ => ParseType.Unsupported,
FourCCType.NoCompression => HandleUncompressed(),
FourCCType.DXT1 => ParseType.DXT1,
FourCCType.DXT2 => ParseType.Unsupported,
FourCCType.DXT3 => ParseType.DXT3,
FourCCType.DXT4 => ParseType.Unsupported,
FourCCType.DXT5 => ParseType.DXT5,
FourCCType.DX10 => dxt10?.ToParseType() ?? ParseType.Unsupported,
FourCCType.ATI1 => ParseType.BC4,
FourCCType.BC4U => ParseType.BC4,
FourCCType.BC45 => ParseType.BC4,
FourCCType.ATI2 => ParseType.BC5,
FourCCType.BC5U => ParseType.BC5,
FourCCType.BC55 => ParseType.BC5,
FourCCType.D3D_A16B16G16R16 => ParseType.A16B16G16R16F,
_ => ParseType.Unsupported,
};
}

View file

@ -1,8 +1,10 @@
using System;
using System.IO;
using Lumina.Data.Files;
using OtterGui;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Functions = Penumbra.GameData.Util.Functions;
namespace Penumbra.Import.Dds;
@ -13,7 +15,7 @@ public class TextureImporter
using var mem = new MemoryStream( target );
using var bw = new BinaryWriter( mem );
bw.Write( ( uint )TexFile.Attribute.TextureType2D );
bw.Write( ( uint )TexFile.TextureFormat.B8G8R8X8 );
bw.Write( ( uint )TexFile.TextureFormat.B8G8R8A8 );
bw.Write( ( ushort )width );
bw.Write( ( ushort )height );
bw.Write( ( ushort )1 );
@ -71,15 +73,7 @@ public class TextureImporter
texData = new byte[80 + width * height * 4];
WriteHeader( texData, width, height );
// RGBA to BGRA.
for( var i = 0; i < rgba.Length; i += 4 )
{
texData[ 80 + i + 0 ] = rgba[ i + 2 ];
texData[ 80 + i + 1 ] = rgba[ i + 1 ];
texData[ 80 + i + 2 ] = rgba[ i + 0 ];
texData[ 80 + i + 3 ] = rgba[ i + 3 ];
}
rgba.CopyTo( texData.AsSpan( 80 ) );
return true;
}

View file

@ -111,7 +111,7 @@ public partial class TexToolsImporter
using var t = new StreamReader( e );
using var j = new JsonTextReader( t );
var obj = JObject.Load( j );
var name = obj[ nameof( Mod.Name ) ]?.Value< string >().RemoveInvalidPathSymbols() ?? string.Empty;
var name = obj[ nameof( Mod.Name ) ]?.Value< string >()?.RemoveInvalidPathSymbols() ?? string.Empty;
if( name.Length == 0 )
{
throw new Exception( "Invalid mod archive: mod meta has no name." );

View file

@ -8,17 +8,17 @@ namespace Penumbra.Interop;
public unsafe class CharacterUtility : IDisposable
{
// A static pointer to the CharacterUtility address.
[Signature( "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2", ScanType = ScanType.StaticAddress )]
[Signature( "48 8B 05 ?? ?? ?? ?? 83 B9", ScanType = ScanType.StaticAddress )]
private readonly Structs.CharacterUtility** _characterUtilityAddress = null;
// Only required for migration anymore.
public delegate void LoadResources( Structs.CharacterUtility* address );
[Signature( "E8 ?? ?? ?? 00 48 8D 8E ?? ?? 00 00 E8 ?? ?? ?? 00 33 D2" )]
public readonly LoadResources? LoadCharacterResourcesFunc;
[Signature( "E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? E8 ?? ?? ?? ?? 33 D2 45 33 C0" )]
public readonly LoadResources LoadCharacterResourcesFunc = null!;
public void LoadCharacterResources()
=> LoadCharacterResourcesFunc?.Invoke( Address );
=> LoadCharacterResourcesFunc.Invoke( Address );
public Structs.CharacterUtility* Address
=> *_characterUtilityAddress;

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
@ -161,7 +162,9 @@ public unsafe partial class ResourceLoader
return ret == null ? null : ret->Value;
}
public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph, int idx );
public delegate void ExtMapAction( ResourceCategory category, StdMap< uint, Pointer< StdMap< uint, Pointer< ResourceHandle > > > >* graph,
int idx );
public delegate void ResourceMapAction( uint ext, StdMap< uint, Pointer< ResourceHandle > >* graph );
public delegate void ResourceAction( uint crc32, ResourceHandle* graph );
@ -223,9 +226,14 @@ public unsafe partial class ResourceLoader
// Prevent resource management weirdness.
private byte ResourceHandleDecRefDetour( ResourceHandle* handle )
{
if( handle == null )
{
return 0;
}
if( handle->RefCount != 0 )
{
return _decRefHook!.Original( handle );
return _decRefHook.Original( handle );
}
PluginLog.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." );

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
namespace Penumbra.Interop.Resolver;
public class CutsceneCharacters : IDisposable
{
public const int CutsceneStartIdx = 200;
public const int CutsceneSlots = 40;
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots;
private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray();
public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors
=> Enumerable.Range( CutsceneStartIdx, CutsceneSlots )
.Where( i => Dalamud.Objects[ i ] != null )
.Select( i => KeyValuePair.Create( i, this[ i ] ?? Dalamud.Objects[ i ]! ) );
public CutsceneCharacters()
=> SignatureHelper.Initialise( this );
// Get the related actor to a cutscene actor.
// Does not check for valid input index.
// Returns null if no connected actor is set or the actor does not exist anymore.
public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ]
{
get
{
Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx );
idx = _copiedCharacters[ idx - CutsceneStartIdx ];
return idx < 0 ? null : Dalamud.Objects[ idx ];
}
}
// Return the currently set index of a parent or -1 if none is set or the index is invalid.
public int GetParentIndex( int idx )
{
if( idx is >= CutsceneStartIdx and < CutsceneEndIdx )
{
return _copiedCharacters[ idx - CutsceneStartIdx ];
}
return -1;
}
public void Enable()
=> _copyCharacterHook.Enable();
public void Disable()
=> _copyCharacterHook.Disable();
public void Dispose()
=> _copyCharacterHook.Dispose();
private unsafe delegate ulong CopyCharacterDelegate( GameObject* target, GameObject* source, uint unk );
[Signature( "E8 ?? ?? ?? ?? 0F B6 9F ?? ?? ?? ?? 48 8D 8F", DetourName = nameof( CopyCharacterDetour ) )]
private readonly Hook< CopyCharacterDelegate > _copyCharacterHook = null!;
private unsafe ulong CopyCharacterDetour( GameObject* target, GameObject* source, uint unk )
{
try
{
if( target != null && target->ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx )
{
var parent = source == null || source->ObjectIndex is < 0 or >= CutsceneStartIdx
? -1
: source->ObjectIndex;
_copiedCharacters[ target->ObjectIndex - CutsceneStartIdx ] = ( short )parent;
}
}
catch
{
// ignored
}
return _copyCharacterHook.Original( target, source, unk );
}
}

View file

@ -1,123 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
private ModCollection? _animationLoadCollection;
private ModCollection? _lastAvfxCollection = null;
public delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline );
// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
[Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )]
public Hook< LoadTimelineResourcesDelegate >? LoadTimelineResourcesHook;
private ulong LoadTimelineResourcesDetour( IntPtr timeline )
{
ulong ret;
var old = _animationLoadCollection;
try
{
var getGameObjectIdx = ( ( delegate* unmanaged < IntPtr, int>** )timeline )[ 0 ][ 28 ];
var idx = getGameObjectIdx( timeline );
if( idx >= 0 && idx < Dalamud.Objects.Length )
{
var obj = Dalamud.Objects[ idx ];
_animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null;
}
else
{
_animationLoadCollection = null;
}
}
finally
{
ret = LoadTimelineResourcesHook!.Original( timeline );
}
_animationLoadCollection = old;
return ret;
}
// Probably used when the base idle animation gets loaded.
// Make it aware of the correct collection to load the correct pap files.
[Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05", DetourName = "CharacterBaseLoadAnimationDetour" )]
public Hook< CharacterBaseDestructorDelegate >? CharacterBaseLoadAnimationHook;
private void CharacterBaseLoadAnimationDetour( IntPtr drawObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = _lastCreatedCollection
?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default );
CharacterBaseLoadAnimationHook!.Original( drawObject );
_animationLoadCollection = last;
}
public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 );
[Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )]
public Hook< LoadSomeAvfx >? LoadSomeAvfxHook;
private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
var ret = LoadSomeAvfxHook!.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 );
_animationLoadCollection = last;
return ret;
}
// Unknown what exactly this is but it seems to load a bunch of paps.
public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )]
public Hook< LoadSomePap >? LoadSomePapHook;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
var timelinePtr = a1 + 0x50;
var last = _animationLoadCollection;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length )
{
_animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) );
}
}
LoadSomePapHook!.Original( a1, a2, a3, a4 );
_animationLoadCollection = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )]
public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook;
private void SomeActionLoadDetour( IntPtr gameObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
SomeActionLoadHook!.Original( gameObject );
_animationLoadCollection = last;
}
[Signature( "E8 ?? ?? ?? ?? 44 84 BB", DetourName = nameof( SomeOtherAvfxDetour ) )]
public Hook< CharacterBaseDestructorDelegate >? SomeOtherAvfxHook;
private void SomeOtherAvfxDetour( IntPtr unk )
{
var last = _animationLoadCollection;
var gameObject = ( GameObject* )( unk - 0x8B0 );
_animationLoadCollection = IdentifyCollection( gameObject );
SomeOtherAvfxHook!.Original( unk );
_animationLoadCollection = last;
}
}

View file

@ -0,0 +1,215 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class AnimationState
{
private readonly DrawObjectState _drawObjectState;
private ModCollection? _animationLoadCollection;
private ModCollection? _lastAvfxCollection;
public AnimationState( DrawObjectState drawObjectState )
{
_drawObjectState = drawObjectState;
SignatureHelper.Initialise( this );
}
public bool HandleFiles( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection )
{
switch( type )
{
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Scd:
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Avfx:
_lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default;
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Atex:
if( _lastAvfxCollection != null )
{
collection = _lastAvfxCollection;
return true;
}
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
}
collection = null;
return false;
}
public void Enable()
{
_loadTimelineResourcesHook.Enable();
_characterBaseLoadAnimationHook.Enable();
_loadSomeAvfxHook.Enable();
_loadSomePapHook.Enable();
_someActionLoadHook.Enable();
_someOtherAvfxHook.Enable();
}
public void Disable()
{
_loadTimelineResourcesHook.Disable();
_characterBaseLoadAnimationHook.Disable();
_loadSomeAvfxHook.Disable();
_loadSomePapHook.Disable();
_someActionLoadHook.Disable();
_someOtherAvfxHook.Disable();
}
public void Dispose()
{
_loadTimelineResourcesHook.Dispose();
_characterBaseLoadAnimationHook.Dispose();
_loadSomeAvfxHook.Dispose();
_loadSomePapHook.Dispose();
_someActionLoadHook.Dispose();
_someOtherAvfxHook.Dispose();
}
// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
private delegate ulong LoadTimelineResourcesDelegate( IntPtr timeline );
[Signature( "E8 ?? ?? ?? ?? 83 7F ?? ?? 75 ?? 0F B6 87", DetourName = nameof( LoadTimelineResourcesDetour ) )]
private readonly Hook< LoadTimelineResourcesDelegate > _loadTimelineResourcesHook = null!;
private ulong LoadTimelineResourcesDetour( IntPtr timeline )
{
ulong ret;
var old = _animationLoadCollection;
try
{
var getGameObjectIdx = ( ( delegate* unmanaged< IntPtr, int >** )timeline )[ 0 ][ 28 ];
var idx = getGameObjectIdx( timeline );
if( idx >= 0 && idx < Dalamud.Objects.Length )
{
var obj = Dalamud.Objects[ idx ];
_animationLoadCollection = obj != null ? IdentifyCollection( ( GameObject* )obj.Address ) : null;
}
else
{
_animationLoadCollection = null;
}
}
finally
{
ret = _loadTimelineResourcesHook.Original( timeline );
}
_animationLoadCollection = old;
return ret;
}
// Probably used when the base idle animation gets loaded.
// Make it aware of the correct collection to load the correct pap files.
private delegate void CharacterBaseNoArgumentDelegate( IntPtr drawBase );
[Signature( "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8B CF 44 8B C2 E8 ?? ?? ?? ?? 48 8B 05",
DetourName = nameof( CharacterBaseLoadAnimationDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _characterBaseLoadAnimationHook = null!;
private void CharacterBaseLoadAnimationDetour( IntPtr drawObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = _drawObjectState.LastCreatedCollection
?? ( FindParent( drawObject, out var collection ) != null ? collection : Penumbra.CollectionManager.Default );
_characterBaseLoadAnimationHook.Original( drawObject );
_animationLoadCollection = last;
}
public delegate ulong LoadSomeAvfx( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 );
[Signature( "E8 ?? ?? ?? ?? 45 0F B6 F7", DetourName = nameof( LoadSomeAvfxDetour ) )]
private readonly Hook< LoadSomeAvfx > _loadSomeAvfxHook = null!;
private ulong LoadSomeAvfxDetour( uint a1, IntPtr gameObject, IntPtr gameObject2, float unk1, IntPtr unk2, IntPtr unk3 )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
var ret = _loadSomeAvfxHook.Original( a1, gameObject, gameObject2, unk1, unk2, unk3 );
_animationLoadCollection = last;
return ret;
}
// Unknown what exactly this is but it seems to load a bunch of paps.
private delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51",
DetourName = nameof( LoadSomePapDetour ) )]
private readonly Hook< LoadSomePap > _loadSomePapHook = null!;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
var timelinePtr = a1 + 0x50;
var last = _animationLoadCollection;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length )
{
_animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) );
}
}
_loadSomePapHook.Original( a1, a2, a3, a4 );
_animationLoadCollection = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _someActionLoadHook = null!;
private void SomeActionLoadDetour( IntPtr gameObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
_someActionLoadHook.Original( gameObject );
_animationLoadCollection = last;
}
[Signature( "E8 ?? ?? ?? ?? 44 84 A3", DetourName = nameof( SomeOtherAvfxDetour ) )]
private readonly Hook< CharacterBaseNoArgumentDelegate > _someOtherAvfxHook = null!;
private void SomeOtherAvfxDetour( IntPtr unk )
{
var last = _animationLoadCollection;
var gameObject = ( GameObject* )( unk - 0x8D0 );
_animationLoadCollection = IdentifyCollection( gameObject );
_someOtherAvfxHook.Original( unk );
_animationLoadCollection = last;
}
}
}

View file

@ -1,88 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectDemiVTable;
public Hook< GeneralResolveDelegate >? ResolveDemiDecalPathHook;
public Hook< EidResolveDelegate >? ResolveDemiEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiImcPathHook;
public Hook< MPapResolveDelegate >? ResolveDemiMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiPhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveDemiSkpPathHook;
public Hook< EidResolveDelegate >? ResolveDemiTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveDemiVfxPathHook;
private void SetupDemiHooks()
{
ResolveDemiDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveDecalIdx ], ResolveDemiDecalDetour );
ResolveDemiEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveEidIdx ], ResolveDemiEidDetour );
ResolveDemiImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveImcIdx ], ResolveDemiImcDetour );
ResolveDemiMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMPapIdx ], ResolveDemiMPapDetour );
ResolveDemiMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveMdlIdx ], ResolveDemiMdlDetour );
ResolveDemiMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveMtrlIdx ], ResolveDemiMtrlDetour );
ResolveDemiPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolvePapIdx ], ResolveDemiPapDetour );
ResolveDemiPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolvePhybIdx ], ResolveDemiPhybDetour );
ResolveDemiSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSklbIdx ], ResolveDemiSklbDetour );
ResolveDemiSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveSkpIdx ], ResolveDemiSkpDetour );
ResolveDemiTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectDemiVTable[ ResolveTmbIdx ], ResolveDemiTmbDetour );
ResolveDemiVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectDemiVTable[ ResolveVfxIdx ], ResolveDemiVfxDetour );
}
private void EnableDemiHooks()
{
ResolveDemiDecalPathHook?.Enable();
ResolveDemiEidPathHook?.Enable();
ResolveDemiImcPathHook?.Enable();
ResolveDemiMPapPathHook?.Enable();
ResolveDemiMdlPathHook?.Enable();
ResolveDemiMtrlPathHook?.Enable();
ResolveDemiPapPathHook?.Enable();
ResolveDemiPhybPathHook?.Enable();
ResolveDemiSklbPathHook?.Enable();
ResolveDemiSkpPathHook?.Enable();
ResolveDemiTmbPathHook?.Enable();
ResolveDemiVfxPathHook?.Enable();
}
private void DisableDemiHooks()
{
ResolveDemiDecalPathHook?.Disable();
ResolveDemiEidPathHook?.Disable();
ResolveDemiImcPathHook?.Disable();
ResolveDemiMPapPathHook?.Disable();
ResolveDemiMdlPathHook?.Disable();
ResolveDemiMtrlPathHook?.Disable();
ResolveDemiPapPathHook?.Disable();
ResolveDemiPhybPathHook?.Disable();
ResolveDemiSklbPathHook?.Disable();
ResolveDemiSkpPathHook?.Disable();
ResolveDemiTmbPathHook?.Disable();
ResolveDemiVfxPathHook?.Disable();
}
private void DisposeDemiHooks()
{
ResolveDemiDecalPathHook?.Dispose();
ResolveDemiEidPathHook?.Dispose();
ResolveDemiImcPathHook?.Dispose();
ResolveDemiMPapPathHook?.Dispose();
ResolveDemiMdlPathHook?.Dispose();
ResolveDemiMtrlPathHook?.Dispose();
ResolveDemiPapPathHook?.Dispose();
ResolveDemiPhybPathHook?.Dispose();
ResolveDemiSklbPathHook?.Dispose();
ResolveDemiSkpPathHook?.Dispose();
ResolveDemiTmbPathHook?.Dispose();
ResolveDemiVfxPathHook?.Dispose();
}
}

View file

@ -0,0 +1,236 @@
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class DrawObjectState
{
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
public IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjects
=> _drawObjectToObject;
public int Count
=> _drawObjectToObject.Count;
public bool TryGetValue( IntPtr drawObject, out (ModCollection, int) value, out GameObject* gameObject )
{
gameObject = null;
if( !_drawObjectToObject.TryGetValue( drawObject, out value ) )
{
return false;
}
var gameObjectIdx = value.Item2;
return VerifyEntry( drawObject, gameObjectIdx, out gameObject );
}
// Set and update a parent object if it exists and a last game object is set.
public ModCollection? CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject )
{
if( parentObject == IntPtr.Zero && LastGameObject != null )
{
var collection = IdentifyCollection( LastGameObject );
_drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex );
return collection;
}
return null;
}
public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection )
{
if( type == ResourceType.Tex
&& LastCreatedCollection != null
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) )
{
collection = LastCreatedCollection!;
return true;
}
collection = null;
return false;
}
public ModCollection? LastCreatedCollection
=> _lastCreatedCollection;
public GameObject* LastGameObject { get; private set; }
public DrawObjectState()
{
SignatureHelper.Initialise( this );
}
public void Enable()
{
_characterBaseCreateHook.Enable();
_characterBaseDestructorHook.Enable();
_enableDrawHook.Enable();
_weaponReloadHook.Enable();
InitializeDrawObjects();
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
}
public void Disable()
{
_characterBaseCreateHook.Disable();
_characterBaseDestructorHook.Disable();
_enableDrawHook.Disable();
_weaponReloadHook.Disable();
Penumbra.CollectionManager.CollectionChanged -= CheckCollections;
}
public void Dispose()
{
Disable();
_characterBaseCreateHook.Dispose();
_characterBaseDestructorHook.Dispose();
_enableDrawHook.Dispose();
_weaponReloadHook.Dispose();
}
// Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it.
private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject )
{
gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx );
var draw = ( DrawObject* )drawObject;
if( gameObject != null
&& ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) )
{
return true;
}
gameObject = null;
_drawObjectToObject.Remove( drawObject );
return false;
}
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
// It contains any DrawObjects that correspond to a human actor, even those without specific collections.
private readonly Dictionary< IntPtr, (ModCollection, int) > _drawObjectToObject = new();
private ModCollection? _lastCreatedCollection;
// Keep track of created DrawObjects that are CharacterBase,
// and use the last game object that called EnableDraw to link them.
private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d );
[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = nameof( CharacterBaseCreateDetour ) )]
private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!;
private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d )
{
using var cmp = MetaChanger.ChangeCmp( LastGameObject, out _lastCreatedCollection );
if( LastGameObject != null )
{
var modelPtr = &a;
CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c );
}
var ret = _characterBaseCreateHook.Original( a, b, c, d );
if( LastGameObject != null )
{
_drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex );
CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ret );
}
return ret;
}
// Remove DrawObjects from the list when they are destroyed.
private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase );
[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30",
DetourName = nameof( CharacterBaseDestructorDetour ) )]
private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!;
private void CharacterBaseDestructorDetour( IntPtr drawBase )
{
_drawObjectToObject.Remove( drawBase );
_characterBaseDestructorHook!.Original.Invoke( drawBase );
}
// EnableDraw is what creates DrawObjects for gameObjects,
// so we always keep track of the current GameObject to be able to link it to the DrawObject.
private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d );
[Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 33 45 33 C0", DetourName = nameof( EnableDrawDetour ) )]
private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!;
private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d )
{
var oldObject = LastGameObject;
LastGameObject = ( GameObject* )gameObject;
_enableDrawHook!.Original.Invoke( gameObject, b, c, d );
LastGameObject = oldObject;
}
// Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8,
// so we use that.
private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 );
[Signature( "E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof( WeaponReloadDetour ) )]
private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!;
public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 )
{
var oldGame = LastGameObject;
LastGameObject = *( GameObject** )( a1 + 8 );
_weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 );
LastGameObject = oldGame;
}
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name )
{
if( type is CollectionType.Inactive or CollectionType.Current )
{
return;
}
foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() )
{
if( !VerifyEntry( key, idx, out var obj ) )
{
_drawObjectToObject.Remove( key );
}
var newCollection = IdentifyCollection( obj );
_drawObjectToObject[ key ] = ( newCollection, idx );
}
}
// Find all current DrawObjects used in the GameObject table.
// We do not iterate the Dalamud table because it does not work when not logged in.
private void InitializeDrawObjects()
{
for( var i = 0; i < Dalamud.Objects.Length; ++i )
{
var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i );
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null )
{
_drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex );
}
}
}
}
}

View file

@ -1,118 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
// We can hook the different Resolve-Functions using just the VTable of Human.
// The other DrawObject VTables and the ResolveRoot function are currently unused.
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectHumanVTable;
// [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )]
// public IntPtr* DrawObjectVTable;
//
// public const int ResolveRootIdx = 71;
public const int ResolveSklbIdx = 72;
public const int ResolveMdlIdx = 73;
public const int ResolveSkpIdx = 74;
public const int ResolvePhybIdx = 75;
public const int ResolvePapIdx = 76;
public const int ResolveTmbIdx = 77;
public const int ResolveMPapIdx = 79;
public const int ResolveImcIdx = 81;
public const int ResolveMtrlIdx = 82;
public const int ResolveDecalIdx = 83;
public const int ResolveVfxIdx = 84;
public const int ResolveEidIdx = 85;
public const int OnModelLoadCompleteIdx = 58;
public delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 );
public delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 );
public delegate IntPtr MaterialResolveDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 );
public delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 );
public delegate void OnModelLoadCompleteDelegate( IntPtr drawObject );
public Hook< GeneralResolveDelegate >? ResolveDecalPathHook;
public Hook< EidResolveDelegate >? ResolveEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveImcPathHook;
public Hook< MPapResolveDelegate >? ResolveMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolvePapPathHook;
public Hook< GeneralResolveDelegate >? ResolvePhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveSkpPathHook;
public Hook< EidResolveDelegate >? ResolveTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveVfxPathHook;
private void SetupHumanHooks()
{
ResolveDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveDecalIdx ], ResolveDecalDetour );
ResolveEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveEidIdx ], ResolveEidDetour );
ResolveImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveImcIdx ], ResolveImcDetour );
ResolveMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMPapIdx ], ResolveMPapDetour );
ResolveMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveMdlIdx ], ResolveMdlDetour );
ResolveMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveMtrlIdx ], ResolveMtrlDetour );
ResolvePapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolvePapIdx ], ResolvePapDetour );
ResolvePhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolvePhybIdx ], ResolvePhybDetour );
ResolveSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSklbIdx ], ResolveSklbDetour );
ResolveSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveSkpIdx ], ResolveSkpDetour );
ResolveTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectHumanVTable[ ResolveTmbIdx ], ResolveTmbDetour );
ResolveVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectHumanVTable[ ResolveVfxIdx ], ResolveVfxDetour );
}
private void EnableHumanHooks()
{
ResolveDecalPathHook?.Enable();
ResolveEidPathHook?.Enable();
ResolveImcPathHook?.Enable();
ResolveMPapPathHook?.Enable();
ResolveMdlPathHook?.Enable();
ResolveMtrlPathHook?.Enable();
ResolvePapPathHook?.Enable();
ResolvePhybPathHook?.Enable();
ResolveSklbPathHook?.Enable();
ResolveSkpPathHook?.Enable();
ResolveTmbPathHook?.Enable();
ResolveVfxPathHook?.Enable();
}
private void DisableHumanHooks()
{
ResolveDecalPathHook?.Disable();
ResolveEidPathHook?.Disable();
ResolveImcPathHook?.Disable();
ResolveMPapPathHook?.Disable();
ResolveMdlPathHook?.Disable();
ResolveMtrlPathHook?.Disable();
ResolvePapPathHook?.Disable();
ResolvePhybPathHook?.Disable();
ResolveSklbPathHook?.Disable();
ResolveSkpPathHook?.Disable();
ResolveTmbPathHook?.Disable();
ResolveVfxPathHook?.Disable();
}
private void DisposeHumanHooks()
{
ResolveDecalPathHook?.Dispose();
ResolveEidPathHook?.Dispose();
ResolveImcPathHook?.Dispose();
ResolveMPapPathHook?.Dispose();
ResolveMdlPathHook?.Dispose();
ResolveMtrlPathHook?.Dispose();
ResolvePapPathHook?.Dispose();
ResolvePhybPathHook?.Dispose();
ResolveSklbPathHook?.Dispose();
ResolveSkpPathHook?.Dispose();
ResolveTmbPathHook?.Dispose();
ResolveVfxPathHook?.Dispose();
}
}

View file

@ -1,17 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Penumbra.Api;
using Lumina.Excel.GeneratedSheets;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
@ -22,147 +17,8 @@ namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
// Keep track of created DrawObjects that are CharacterBase,
// and use the last game object that called EnableDraw to link them.
public delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d );
[Signature( "E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour" )]
public Hook< CharacterBaseCreateDelegate >? CharacterBaseCreateHook;
private ModCollection? _lastCreatedCollection;
public event CreatingCharacterBaseDelegate? CreatingCharacterBase;
private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d )
{
using var cmp = MetaChanger.ChangeCmp( this, out _lastCreatedCollection );
if( LastGameObject != null )
{
var modelPtr = &a;
CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!, ( IntPtr )modelPtr, b, c );
}
var ret = CharacterBaseCreateHook!.Original( a, b, c, d );
if( LastGameObject != null )
{
DrawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex );
}
return ret;
}
// Remove DrawObjects from the list when they are destroyed.
public delegate void CharacterBaseDestructorDelegate( IntPtr drawBase );
[Signature( "E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30",
DetourName = "CharacterBaseDestructorDetour" )]
public Hook< CharacterBaseDestructorDelegate >? CharacterBaseDestructorHook;
private void CharacterBaseDestructorDetour( IntPtr drawBase )
{
DrawObjectToObject.Remove( drawBase );
CharacterBaseDestructorHook!.Original.Invoke( drawBase );
}
// EnableDraw is what creates DrawObjects for gameObjects,
// so we always keep track of the current GameObject to be able to link it to the DrawObject.
public delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d );
[Signature( "E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0" )]
public Hook< EnableDrawDelegate >? EnableDrawHook;
private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d )
{
var oldObject = LastGameObject;
LastGameObject = ( GameObject* )gameObject;
EnableDrawHook!.Original.Invoke( gameObject, b, c, d );
LastGameObject = oldObject;
}
// Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8,
// so we use that.
public delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 );
[Signature( "E8 ?? ?? ?? ?? 44 8B 9F" )]
public Hook< WeaponReloadFunc >? WeaponReloadHook;
public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 )
{
var oldGame = LastGameObject;
LastGameObject = *( GameObject** )( a1 + 8 );
WeaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 );
LastGameObject = oldGame;
}
private void EnableDataHooks()
{
CharacterBaseCreateHook?.Enable();
EnableDrawHook?.Enable();
CharacterBaseDestructorHook?.Enable();
WeaponReloadHook?.Enable();
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
LoadTimelineResourcesHook?.Enable();
CharacterBaseLoadAnimationHook?.Enable();
LoadSomeAvfxHook?.Enable();
LoadSomePapHook?.Enable();
SomeActionLoadHook?.Enable();
SomeOtherAvfxHook?.Enable();
}
private void DisableDataHooks()
{
Penumbra.CollectionManager.CollectionChanged -= CheckCollections;
WeaponReloadHook?.Disable();
CharacterBaseCreateHook?.Disable();
EnableDrawHook?.Disable();
CharacterBaseDestructorHook?.Disable();
LoadTimelineResourcesHook?.Disable();
CharacterBaseLoadAnimationHook?.Disable();
LoadSomeAvfxHook?.Disable();
LoadSomePapHook?.Disable();
SomeActionLoadHook?.Disable();
SomeOtherAvfxHook?.Disable();
}
private void DisposeDataHooks()
{
WeaponReloadHook?.Dispose();
CharacterBaseCreateHook?.Dispose();
EnableDrawHook?.Dispose();
CharacterBaseDestructorHook?.Dispose();
LoadTimelineResourcesHook?.Dispose();
CharacterBaseLoadAnimationHook?.Dispose();
LoadSomeAvfxHook?.Dispose();
LoadSomePapHook?.Dispose();
SomeActionLoadHook?.Dispose();
SomeOtherAvfxHook?.Dispose();
}
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
// It contains any DrawObjects that correspond to a human actor, even those without specific collections.
internal readonly Dictionary< IntPtr, (ModCollection, int) > DrawObjectToObject = new();
// This map links files to their corresponding collection, if it is non-default.
internal readonly ConcurrentDictionary< Utf8String, ModCollection > PathCollections = new();
internal GameObject* LastGameObject = null;
// Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it.
private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject )
{
gameObject = ( GameObject* )Dalamud.Objects.GetObjectAddress( gameObjectIdx );
var draw = ( DrawObject* )drawObject;
if( gameObject != null && ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) )
{
return true;
}
gameObject = null;
DrawObjectToObject.Remove( drawObject );
return false;
}
[Signature( "0F B7 0D ?? ?? ?? ?? C7 85", ScanType = ScanType.StaticAddress )]
private static ushort* _inspectTitleId = null!;
// Obtain the name of the current player, if one exists.
private static string? GetPlayerName()
@ -183,17 +39,14 @@ public unsafe partial class PathResolver
}
var ui = ( AtkUnitBase* )addon;
if( ui->UldManager.NodeListCount < 60 )
if( ui->UldManager.NodeListCount <= 60 )
{
return null;
}
var text = ( AtkTextNode* )ui->UldManager.NodeList[ 59 ];
if( text == null || !text->AtkResNode.IsVisible )
{
text = ( AtkTextNode* )ui->UldManager.NodeList[ 60 ];
}
var nodeId = Dalamud.GameData.GetExcelSheet< Title >()?.GetRow( *_inspectTitleId )?.IsPrefix == true ? 59 : 60;
var text = ( AtkTextNode* )ui->UldManager.NodeList[ nodeId ];
return text != null ? text->NodeText.ToString() : null;
}
@ -244,6 +97,13 @@ public unsafe partial class PathResolver
return null;
}
var parent = Cutscenes[ gameObject->ObjectIndex ];
if( parent != null )
{
return parent.Name.ToString();
}
// should not really happen but keep it in as a emergency case.
var player = Dalamud.Objects[ 0 ];
if( player == null )
{
@ -327,12 +187,13 @@ public unsafe partial class PathResolver
// Only OwnerName can be applied to something with a non-empty name, and that is the specific case we want to handle.
var actualName = gameObject->ObjectIndex switch
{
240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window
241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor.
242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on
243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview
>= 200 => GetCutsceneName( gameObject ),
_ => null,
240 => Penumbra.Config.UseCharacterCollectionInMainWindow ? GetPlayerName() : null, // character window
241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor.
242 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // try-on
243 => Penumbra.Config.UseCharacterCollectionInTryOn ? GetPlayerName() : null, // dye preview
244 => Penumbra.Config.UseCharacterCollectionsInCards ? GetPlayerName() : null, // portrait list and editor
>= CutsceneCharacters.CutsceneStartIdx and < CutsceneCharacters.CutsceneEndIdx => GetCutsceneName( gameObject ),
_ => null,
}
?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString();
@ -385,7 +246,9 @@ public unsafe partial class PathResolver
{
collection = null;
// Check for the Yourself collection.
if( actor->ObjectIndex is 0 or ObjectReloader.GPosePlayerIdx || name == Dalamud.ClientState.LocalPlayer?.Name.ToString() )
if( actor->ObjectIndex == 0
|| actor->ObjectIndex == ObjectReloader.GPosePlayerIdx && name.Length > 0
|| name == Dalamud.ClientState.LocalPlayer?.Name.ToString() )
{
collection = Penumbra.CollectionManager.ByType( CollectionType.Yourself );
if( collection != null )
@ -464,77 +327,4 @@ public unsafe partial class PathResolver
return false;
}
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string? name )
{
if( type is CollectionType.Inactive or CollectionType.Current )
{
return;
}
foreach( var (key, (_, idx)) in DrawObjectToObject.ToArray() )
{
if( !VerifyEntry( key, idx, out var obj ) )
{
DrawObjectToObject.Remove( key );
}
var newCollection = IdentifyCollection( obj );
DrawObjectToObject[ key ] = ( newCollection, idx );
}
}
// Use the stored information to find the GameObject and Collection linked to a DrawObject.
private GameObject* FindParent( IntPtr drawObject, out ModCollection collection )
{
if( DrawObjectToObject.TryGetValue( drawObject, out var data ) )
{
var gameObjectIdx = data.Item2;
if( VerifyEntry( drawObject, gameObjectIdx, out var gameObject ) )
{
collection = data.Item1;
return gameObject;
}
}
if( LastGameObject != null && ( LastGameObject->DrawObject == null || LastGameObject->DrawObject == ( DrawObject* )drawObject ) )
{
collection = IdentifyCollection( LastGameObject );
return LastGameObject;
}
collection = IdentifyCollection( null );
return null;
}
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
private void SetCollection( Utf8String path, ModCollection collection )
{
if( PathCollections.ContainsKey( path ) || path.IsOwned )
{
PathCollections[ path ] = collection;
}
else
{
PathCollections[ path.Clone() ] = collection;
}
}
// Find all current DrawObjects used in the GameObject table.
// We do not iterate the Dalamud table because it does not work when not logged in.
private void InitializeDrawObjects()
{
for( var i = 0; i < Dalamud.Objects.Length; ++i )
{
var ptr = ( GameObject* )Dalamud.Objects.GetObjectAddress( i );
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null )
{
DrawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr ), ptr->ObjectIndex );
}
}
}
}

View file

@ -4,7 +4,6 @@ using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
@ -12,132 +11,144 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Resolver;
// Materials do contain their own paths to textures and shader packages.
// Those are loaded synchronously.
// Thus, we need to ensure the correct files are loaded when a material is loaded.
public unsafe partial class PathResolver
{
public delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle );
[Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour" )]
public Hook< LoadMtrlFilesDelegate >? LoadMtrlTexHook;
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
// Materials do contain their own paths to textures and shader packages.
// Those are loaded synchronously.
// Thus, we need to ensure the correct files are loaded when a material is loaded.
public class MaterialState : IDisposable
{
LoadMtrlHelper( mtrlResourceHandle );
var ret = LoadMtrlTexHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
private readonly PathState _paths;
[Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89",
DetourName = "LoadMtrlShpkDetour" )]
public Hook< LoadMtrlFilesDelegate >? LoadMtrlShpkHook;
private ModCollection? _mtrlCollection;
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle )
{
LoadMtrlHelper( mtrlResourceHandle );
var ret = LoadMtrlShpkHook!.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
private ModCollection? _mtrlCollection;
private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
{
if( mtrlResourceHandle == IntPtr.Zero )
public MaterialState( PathState paths )
{
return;
SignatureHelper.Initialise( this );
_paths = paths;
}
var mtrl = ( MtrlResource* )mtrlResourceHandle;
var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true );
_mtrlCollection = PathCollections.TryGetValue( mtrlPath, out var c ) ? c : null;
}
// Check specifically for shpk and tex files whether we are currently in a material load.
private bool HandleMaterialSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection )
{
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
// Check specifically for shpk and tex files whether we are currently in a material load.
public bool HandleSubFiles( ResourceType type, [NotNullWhen( true )] out ModCollection? collection )
{
collection = _mtrlCollection;
return true;
}
if( _mtrlCollection != null && type is ResourceType.Tex or ResourceType.Shpk )
{
collection = _mtrlCollection;
return true;
}
collection = null;
return false;
}
// We need to set the correct collection for the actual material path that is loaded
// before actually loading the file.
private bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
{
ret = 0;
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl )
{
collection = null;
return false;
}
var lastUnderscore = split.LastIndexOf( ( byte )'_' );
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
if( Penumbra.TempMods.CollectionByName( name, out var collection )
|| Penumbra.CollectionManager.ByName( name, out collection ) )
// Materials need to be set per collection so they can load their textures independently from each other.
public static void HandleCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, object?) data )
{
if( nonDefault && type == ResourceType.Mtrl )
{
var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" );
data = ( fullPath, collection );
}
else
{
data = ( resolved, collection );
}
}
public void Enable()
{
_loadMtrlShpkHook.Enable();
_loadMtrlTexHook.Enable();
Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler;
}
public void Disable()
{
_loadMtrlShpkHook.Disable();
_loadMtrlTexHook.Disable();
Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler;
}
public void Dispose()
{
Disable();
_loadMtrlShpkHook?.Dispose();
_loadMtrlTexHook?.Dispose();
}
// We need to set the correct collection for the actual material path that is loaded
// before actually loading the file.
public bool MtrlLoadHandler( Utf8String split, Utf8String path, ResourceManager* resourceManager,
SeFileDescriptor* fileDescriptor, int priority, bool isSync, out byte ret )
{
ret = 0;
if( fileDescriptor->ResourceHandle->FileType != ResourceType.Mtrl )
{
return false;
}
var lastUnderscore = split.LastIndexOf( ( byte )'_' );
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
if( Penumbra.TempMods.CollectionByName( name, out var collection )
|| Penumbra.CollectionManager.ByName( name, out collection ) )
{
#if DEBUG
PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path );
PluginLog.Verbose( "Using MtrlLoadHandler with collection {$Split:l} for path {$Path:l}.", name, path );
#endif
SetCollection( path, collection );
}
else
{
_paths.SetCollection( path, collection );
}
else
{
#if DEBUG
PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path );
PluginLog.Verbose( "Using MtrlLoadHandler with no collection for path {$Path:l}.", path );
#endif
}
// Force isSync = true for this call. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true );
_paths.Consume( path, out _ );
return true;
}
// Force isSync = true for this call. I don't really understand why,
// or where the difference even comes from.
// Was called with True on my client and with false on other peoples clients,
// which caused problems.
ret = Penumbra.ResourceLoader.DefaultLoadResource( path, resourceManager, fileDescriptor, priority, true );
PathCollections.TryRemove( path, out _ );
return true;
}
private delegate byte LoadMtrlFilesDelegate( IntPtr mtrlResourceHandle );
// Materials need to be set per collection so they can load their textures independently from each other.
private static void HandleMtrlCollection( ModCollection collection, string path, bool nonDefault, ResourceType type, FullPath? resolved,
out (FullPath?, object?) data )
{
if( nonDefault && type == ResourceType.Mtrl )
[Signature( "4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = nameof( LoadMtrlTexDetour ) )]
private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlTexHook = null!;
private byte LoadMtrlTexDetour( IntPtr mtrlResourceHandle )
{
var fullPath = new FullPath( $"|{collection.Name}_{collection.ChangeCounter}|{path}" );
data = ( fullPath, collection );
LoadMtrlHelper( mtrlResourceHandle );
var ret = _loadMtrlTexHook.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
else
[Signature( "48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B7 89",
DetourName = nameof( LoadMtrlShpkDetour ) )]
private readonly Hook< LoadMtrlFilesDelegate > _loadMtrlShpkHook = null!;
private byte LoadMtrlShpkDetour( IntPtr mtrlResourceHandle )
{
data = ( resolved, collection );
LoadMtrlHelper( mtrlResourceHandle );
var ret = _loadMtrlShpkHook.Original( mtrlResourceHandle );
_mtrlCollection = null;
return ret;
}
}
private void EnableMtrlHooks()
{
LoadMtrlShpkHook?.Enable();
LoadMtrlTexHook?.Enable();
Penumbra.ResourceLoader.ResourceLoadCustomization += MtrlLoadHandler;
}
private void LoadMtrlHelper( IntPtr mtrlResourceHandle )
{
if( mtrlResourceHandle == IntPtr.Zero )
{
return;
}
private void DisableMtrlHooks()
{
LoadMtrlShpkHook?.Disable();
LoadMtrlTexHook?.Disable();
Penumbra.ResourceLoader.ResourceLoadCustomization -= MtrlLoadHandler;
}
private void DisposeMtrlHooks()
{
LoadMtrlShpkHook?.Dispose();
LoadMtrlTexHook?.Dispose();
var mtrl = ( MtrlResource* )mtrlResourceHandle;
var mtrlPath = Utf8String.FromSpanUnsafe( mtrl->Handle.FileNameSpan(), true, null, true );
_mtrlCollection = _paths.TryGetValue( mtrlPath, out var c ) ? c : null;
}
}
}

View file

@ -1,9 +1,9 @@
using System;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Collections;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Interop.Resolver;
@ -27,142 +27,162 @@ namespace Penumbra.Interop.Resolver;
// RSP tail entries seem to be obtained by "E8 ?? ?? ?? ?? 0F 28 F0 48 8B 05"
// RSP bust size entries seem to be obtained by "E8 ?? ?? ?? ?? F2 0F 10 44 24 ?? 8B 44 24 ?? F2 0F 11 45 ?? 89 45 ?? 83 FF"
// they all are called by many functions, but the most relevant seem to be Human.SetupFromCharacterData, which is only called by CharacterBase.Create,
// and RspSetupCharacter, which is hooked here.
// ChangeCustomize and RspSetupCharacter, which is hooked here.
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
public unsafe partial class PathResolver
{
public delegate void UpdateModelDelegate( IntPtr drawObject );
[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = "UpdateModelsDetour" )]
public Hook< UpdateModelDelegate >? UpdateModelsHook;
private void UpdateModelsDetour( IntPtr drawObject )
public unsafe class MetaState : IDisposable
{
// Shortcut because this is called all the time.
// Same thing is checked at the beginning of the original function.
if( *( int* )( drawObject + 0x90c ) == 0 )
private readonly PathResolver _parent;
public MetaState( PathResolver parent, IntPtr* humanVTable )
{
return;
SignatureHelper.Initialise( this );
_parent = parent;
_onModelLoadCompleteHook = Hook< OnModelLoadCompleteDelegate >.FromAddress( humanVTable[ 58 ], OnModelLoadCompleteDetour );
}
var collection = GetCollection( drawObject );
if( collection != null )
public void Enable()
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
UpdateModelsHook!.Original.Invoke( drawObject );
}
else
{
UpdateModelsHook!.Original.Invoke( drawObject );
}
}
[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C",
DetourName = "GetEqpIndirectDetour" )]
public Hook< OnModelLoadCompleteDelegate >? GetEqpIndirectHook;
private void GetEqpIndirectDetour( IntPtr drawObject )
{
// Shortcut because this is also called all the time.
// Same thing is checked at the beginning of the original function.
if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 )
{
return;
_getEqpIndirectHook.Enable();
_updateModelsHook.Enable();
_onModelLoadCompleteHook.Enable();
_setupVisorHook.Enable();
_rspSetupCharacterHook.Enable();
_changeCustomize.Enable();
}
using var eqp = MetaChanger.ChangeEqp( this, drawObject );
GetEqpIndirectHook!.Original( drawObject );
}
public Hook< OnModelLoadCompleteDelegate >? OnModelLoadCompleteHook;
private void OnModelLoadCompleteDetour( IntPtr drawObject )
{
var collection = GetCollection( drawObject );
if( collection != null )
public void Disable()
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
OnModelLoadCompleteHook!.Original.Invoke( drawObject );
}
else
{
OnModelLoadCompleteHook!.Original.Invoke( drawObject );
}
}
// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
// but it only applies a changed gmp file after a redraw for some reason.
public delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState );
[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = "SetupVisorDetour" )]
public Hook< SetupVisorDelegate >? SetupVisorHook;
private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState )
{
using var gmp = MetaChanger.ChangeGmp( this, drawObject );
return SetupVisorHook!.Original( drawObject, modelId, visorState );
}
// RSP
public delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = "RspSetupCharacterDetour" )]
public Hook< RspSetupCharacterDelegate >? RspSetupCharacterHook;
private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 )
{
using var rsp = MetaChanger.ChangeCmp( this, drawObject );
RspSetupCharacterHook!.Original( drawObject, unk2, unk3, unk4, unk5 );
}
private void SetupMetaHooks()
{
OnModelLoadCompleteHook =
Hook< OnModelLoadCompleteDelegate >.FromAddress( DrawObjectHumanVTable[ OnModelLoadCompleteIdx ], OnModelLoadCompleteDetour );
}
private void EnableMetaHooks()
{
GetEqpIndirectHook?.Enable();
UpdateModelsHook?.Enable();
OnModelLoadCompleteHook?.Enable();
SetupVisorHook?.Enable();
RspSetupCharacterHook?.Enable();
}
private void DisableMetaHooks()
{
GetEqpIndirectHook?.Disable();
UpdateModelsHook?.Disable();
OnModelLoadCompleteHook?.Disable();
SetupVisorHook?.Disable();
RspSetupCharacterHook?.Disable();
}
private void DisposeMetaHooks()
{
GetEqpIndirectHook?.Dispose();
UpdateModelsHook?.Dispose();
OnModelLoadCompleteHook?.Dispose();
SetupVisorHook?.Dispose();
RspSetupCharacterHook?.Dispose();
}
private ModCollection? GetCollection( IntPtr drawObject )
{
var parent = FindParent( drawObject, out var collection );
if( parent == null || collection == Penumbra.CollectionManager.Default )
{
return null;
_getEqpIndirectHook.Disable();
_updateModelsHook.Disable();
_onModelLoadCompleteHook.Disable();
_setupVisorHook.Disable();
_rspSetupCharacterHook.Disable();
_changeCustomize.Disable();
}
return collection.HasCache ? collection : null;
}
public void Dispose()
{
_getEqpIndirectHook.Dispose();
_updateModelsHook.Dispose();
_onModelLoadCompleteHook.Dispose();
_setupVisorHook.Dispose();
_rspSetupCharacterHook.Dispose();
_changeCustomize.Dispose();
}
private delegate void OnModelLoadCompleteDelegate( IntPtr drawObject );
private readonly Hook< OnModelLoadCompleteDelegate > _onModelLoadCompleteHook;
private void OnModelLoadCompleteDetour( IntPtr drawObject )
{
var collection = GetCollection( drawObject );
if( collection != null )
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
_onModelLoadCompleteHook.Original.Invoke( drawObject );
}
else
{
_onModelLoadCompleteHook.Original.Invoke( drawObject );
}
}
private delegate void UpdateModelDelegate( IntPtr drawObject );
[Signature( "48 8B ?? 56 48 83 ?? ?? ?? B9", DetourName = nameof( UpdateModelsDetour ) )]
private readonly Hook< UpdateModelDelegate > _updateModelsHook = null!;
private void UpdateModelsDetour( IntPtr drawObject )
{
// Shortcut because this is called all the time.
// Same thing is checked at the beginning of the original function.
if( *( int* )( drawObject + 0x90c ) == 0 )
{
return;
}
var collection = GetCollection( drawObject );
if( collection != null )
{
using var eqp = MetaChanger.ChangeEqp( collection );
using var eqdp = MetaChanger.ChangeEqdp( collection );
_updateModelsHook.Original.Invoke( drawObject );
}
else
{
_updateModelsHook.Original.Invoke( drawObject );
}
}
[Signature( "40 ?? 48 83 ?? ?? ?? 81 ?? ?? ?? ?? ?? 48 8B ?? 74 ?? ?? 83 ?? ?? ?? ?? ?? ?? 74 ?? 4C",
DetourName = nameof( GetEqpIndirectDetour ) )]
private readonly Hook< OnModelLoadCompleteDelegate > _getEqpIndirectHook = null!;
private void GetEqpIndirectDetour( IntPtr drawObject )
{
// Shortcut because this is also called all the time.
// Same thing is checked at the beginning of the original function.
if( ( *( byte* )( drawObject + 0xa30 ) & 1 ) == 0 || *( ulong* )( drawObject + 0xa28 ) == 0 )
{
return;
}
using var eqp = MetaChanger.ChangeEqp( _parent, drawObject );
_getEqpIndirectHook.Original( drawObject );
}
// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
// but it only applies a changed gmp file after a redraw for some reason.
private delegate byte SetupVisorDelegate( IntPtr drawObject, ushort modelId, byte visorState );
[Signature( "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", DetourName = nameof( SetupVisorDetour ) )]
private readonly Hook< SetupVisorDelegate > _setupVisorHook = null!;
private byte SetupVisorDetour( IntPtr drawObject, ushort modelId, byte visorState )
{
using var gmp = MetaChanger.ChangeGmp( _parent, drawObject );
return _setupVisorHook.Original( drawObject, modelId, visorState );
}
// RSP
private delegate void RspSetupCharacterDelegate( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 88 54 24 ?? 57 41 56", DetourName = nameof( RspSetupCharacterDetour ) )]
private readonly Hook< RspSetupCharacterDelegate > _rspSetupCharacterHook = null!;
private void RspSetupCharacterDetour( IntPtr drawObject, IntPtr unk2, float unk3, IntPtr unk4, byte unk5 )
{
if( _inChangeCustomize )
{
using var rsp = MetaChanger.ChangeCmp( _parent, drawObject );
_rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 );
}
else
{
_rspSetupCharacterHook.Original( drawObject, unk2, unk3, unk4, unk5 );
}
}
// ChangeCustomize calls RspSetupCharacter, so skip the additional cmp change.
private bool _inChangeCustomize = false;
private delegate bool ChangeCustomizeDelegate( IntPtr human, IntPtr data, byte skipEquipment );
[Signature( "E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86", DetourName = nameof( ChangeCustomizeDetour ) )]
private readonly Hook< ChangeCustomizeDelegate > _changeCustomize = null!;
private bool ChangeCustomizeDetour( IntPtr human, IntPtr data, byte skipEquipment )
{
_inChangeCustomize = true;
using var rsp = MetaChanger.ChangeCmp( _parent, human );
return _changeCustomize.Original( human, data, skipEquipment );
}
}
// Small helper to handle setting metadata and reverting it at the end of the function.
// Since eqp and eqdp may be called multiple times in a row, we need to count them,
@ -194,11 +214,12 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeEqp( PathResolver resolver, IntPtr drawObject )
{
var collection = resolver.GetCollection( drawObject );
var collection = GetCollection( drawObject );
if( collection != null )
{
return ChangeEqp( collection );
}
return new MetaChanger( MetaManipulation.Type.Unknown );
}
@ -207,12 +228,13 @@ public unsafe partial class PathResolver
{
if( modelType < 10 )
{
var collection = resolver.GetCollection( drawObject );
var collection = GetCollection( drawObject );
if( collection != null )
{
return ChangeEqdp( collection );
}
}
return new MetaChanger( MetaManipulation.Type.Unknown );
}
@ -224,31 +246,33 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeGmp( PathResolver resolver, IntPtr drawObject )
{
var collection = resolver.GetCollection( drawObject );
var collection = GetCollection( drawObject );
if( collection != null )
{
collection.SetGmpFiles();
return new MetaChanger( MetaManipulation.Type.Gmp );
}
return new MetaChanger( MetaManipulation.Type.Unknown );
}
public static MetaChanger ChangeEst( PathResolver resolver, IntPtr drawObject )
{
var collection = resolver.GetCollection( drawObject );
var collection = GetCollection( drawObject );
if( collection != null )
{
collection.SetEstFiles();
return new MetaChanger( MetaManipulation.Type.Est );
}
return new MetaChanger( MetaManipulation.Type.Unknown );
}
public static MetaChanger ChangeCmp( PathResolver resolver, out ModCollection? collection )
public static MetaChanger ChangeCmp( GameObject* gameObject, out ModCollection? collection )
{
if( resolver.LastGameObject != null )
if( gameObject != null )
{
collection = IdentifyCollection( resolver.LastGameObject );
collection = IdentifyCollection( gameObject );
if( collection != Penumbra.CollectionManager.Default && collection.HasCache )
{
collection.SetCmpFiles();
@ -265,12 +289,13 @@ public unsafe partial class PathResolver
public static MetaChanger ChangeCmp( PathResolver resolver, IntPtr drawObject )
{
var collection = resolver.GetCollection( drawObject );
var collection = GetCollection( drawObject );
if( collection != null )
{
collection.SetCmpFiles();
return new MetaChanger( MetaManipulation.Type.Rsp );
}
return new MetaChanger( MetaManipulation.Type.Unknown );
}

View file

@ -1,88 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectMonsterVTable;
public Hook<GeneralResolveDelegate>? ResolveMonsterDecalPathHook;
public Hook<EidResolveDelegate>? ResolveMonsterEidPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterImcPathHook;
public Hook<MPapResolveDelegate>? ResolveMonsterMPapPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterMdlPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterMtrlPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterPapPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterPhybPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterSklbPathHook;
public Hook<GeneralResolveDelegate>? ResolveMonsterSkpPathHook;
public Hook<EidResolveDelegate>? ResolveMonsterTmbPathHook;
public Hook<MaterialResolveDetour>? ResolveMonsterVfxPathHook;
private void SetupMonsterHooks()
{
ResolveMonsterDecalPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveDecalIdx], ResolveMonsterDecalDetour );
ResolveMonsterEidPathHook = Hook<EidResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveEidIdx], ResolveMonsterEidDetour );
ResolveMonsterImcPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveImcIdx], ResolveMonsterImcDetour );
ResolveMonsterMPapPathHook = Hook<MPapResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveMPapIdx], ResolveMonsterMPapDetour );
ResolveMonsterMdlPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveMdlIdx], ResolveMonsterMdlDetour );
ResolveMonsterMtrlPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolveMtrlIdx], ResolveMonsterMtrlDetour );
ResolveMonsterPapPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolvePapIdx], ResolveMonsterPapDetour );
ResolveMonsterPhybPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolvePhybIdx], ResolveMonsterPhybDetour );
ResolveMonsterSklbPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveSklbIdx], ResolveMonsterSklbDetour );
ResolveMonsterSkpPathHook = Hook<GeneralResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveSkpIdx], ResolveMonsterSkpDetour );
ResolveMonsterTmbPathHook = Hook<EidResolveDelegate>.FromAddress( DrawObjectMonsterVTable[ResolveTmbIdx], ResolveMonsterTmbDetour );
ResolveMonsterVfxPathHook = Hook<MaterialResolveDetour>.FromAddress( DrawObjectMonsterVTable[ResolveVfxIdx], ResolveMonsterVfxDetour );
}
private void EnableMonsterHooks()
{
ResolveMonsterDecalPathHook?.Enable();
ResolveMonsterEidPathHook?.Enable();
ResolveMonsterImcPathHook?.Enable();
ResolveMonsterMPapPathHook?.Enable();
ResolveMonsterMdlPathHook?.Enable();
ResolveMonsterMtrlPathHook?.Enable();
ResolveMonsterPapPathHook?.Enable();
ResolveMonsterPhybPathHook?.Enable();
ResolveMonsterSklbPathHook?.Enable();
ResolveMonsterSkpPathHook?.Enable();
ResolveMonsterTmbPathHook?.Enable();
ResolveMonsterVfxPathHook?.Enable();
}
private void DisableMonsterHooks()
{
ResolveMonsterDecalPathHook?.Disable();
ResolveMonsterEidPathHook?.Disable();
ResolveMonsterImcPathHook?.Disable();
ResolveMonsterMPapPathHook?.Disable();
ResolveMonsterMdlPathHook?.Disable();
ResolveMonsterMtrlPathHook?.Disable();
ResolveMonsterPapPathHook?.Disable();
ResolveMonsterPhybPathHook?.Disable();
ResolveMonsterSklbPathHook?.Disable();
ResolveMonsterSkpPathHook?.Disable();
ResolveMonsterTmbPathHook?.Disable();
ResolveMonsterVfxPathHook?.Disable();
}
private void DisposeMonsterHooks()
{
ResolveMonsterDecalPathHook?.Dispose();
ResolveMonsterEidPathHook?.Dispose();
ResolveMonsterImcPathHook?.Dispose();
ResolveMonsterMPapPathHook?.Dispose();
ResolveMonsterMdlPathHook?.Dispose();
ResolveMonsterMtrlPathHook?.Dispose();
ResolveMonsterPapPathHook?.Dispose();
ResolveMonsterPhybPathHook?.Dispose();
ResolveMonsterSklbPathHook?.Dispose();
ResolveMonsterSkpPathHook?.Dispose();
ResolveMonsterTmbPathHook?.Dispose();
ResolveMonsterVfxPathHook?.Dispose();
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Dalamud.Utility.Signatures;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
public class PathState : IDisposable
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress )]
public readonly IntPtr* HumanVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B",
ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _weaponVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _demiHumanVTable = null!;
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )]
private readonly IntPtr* _monsterVTable = null!;
private readonly ResolverHooks _human;
private readonly ResolverHooks _weapon;
private readonly ResolverHooks _demiHuman;
private readonly ResolverHooks _monster;
// This map links files to their corresponding collection, if it is non-default.
private readonly ConcurrentDictionary< Utf8String, ModCollection > _pathCollections = new();
public PathState( PathResolver parent )
{
SignatureHelper.Initialise( this );
_human = new ResolverHooks( parent, HumanVTable, ResolverHooks.Type.Human );
_weapon = new ResolverHooks( parent, _weaponVTable, ResolverHooks.Type.Weapon );
_demiHuman = new ResolverHooks( parent, _demiHumanVTable, ResolverHooks.Type.Other );
_monster = new ResolverHooks( parent, _monsterVTable, ResolverHooks.Type.Other );
}
public void Enable()
{
_human.Enable();
_weapon.Enable();
_demiHuman.Enable();
_monster.Enable();
}
public void Disable()
{
_human.Disable();
_weapon.Disable();
_demiHuman.Disable();
_monster.Disable();
}
public void Dispose()
{
_human.Dispose();
_weapon.Dispose();
_demiHuman.Dispose();
_monster.Dispose();
}
public int Count
=> _pathCollections.Count;
public IEnumerable< KeyValuePair< Utf8String, ModCollection > > Paths
=> _pathCollections;
public bool TryGetValue( Utf8String path, [NotNullWhen( true )] out ModCollection? collection )
=> _pathCollections.TryGetValue( path, out collection );
public bool Consume( Utf8String path, [NotNullWhen( true )] out ModCollection? collection )
=> _pathCollections.TryRemove( path, out collection );
// Just add or remove the resolved path.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
public IntPtr ResolvePath( ModCollection collection, IntPtr path )
{
if( path == IntPtr.Zero )
{
return path;
}
var gamePath = new Utf8String( ( byte* )path );
SetCollection( gamePath, collection );
return path;
}
// Special handling for paths so that we do not store non-owned temporary strings in the dictionary.
public void SetCollection( Utf8String path, ModCollection collection )
{
if( _pathCollections.ContainsKey( path ) || path.IsOwned )
{
_pathCollections[ path ] = collection;
}
else
{
_pathCollections[ path.Clone() ] = collection;
}
}
}
}

View file

@ -1,223 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
namespace Penumbra.Interop.Resolver;
// The actual resolve detours are basically all the same.
public unsafe partial class PathResolver
{
// Humans
private IntPtr ResolveDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
{
using var eqdp = MetaChanger.ChangeEqdp( this, drawObject, modelType );
return ResolvePathDetour( drawObject, ResolveMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
}
private IntPtr ResolveMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolvePapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
}
private IntPtr ResolvePhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolvePhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolveSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( this, drawObject );
return ResolvePathDetour( drawObject, ResolveSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Weapons
private IntPtr ResolveWeaponDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveWeaponImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveWeaponMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveWeaponPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveWeaponTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveWeaponVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPathDetour( drawObject, ResolveWeaponVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Monsters
private IntPtr ResolveMonsterDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveMonsterEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveMonsterImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePathDetour( drawObject, ResolveMonsterMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMonsterMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMonsterPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveMonsterSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMonsterTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveMonsterTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveMonsterVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveMonsterVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Demihumans
private IntPtr ResolveDemiDecalDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiDecalPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiEidDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveDemiEidPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveDemiImcDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiImcPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiMPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiMPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiMdlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePathDetour( drawObject, ResolveDemiMdlPathHook!.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveDemiMtrlDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiMtrlPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiPapDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiPapPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveDemiPhybDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiPhybPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiSklbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiSklbPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiSkpDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePathDetour( drawObject, ResolveDemiSkpPathHook!.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveDemiTmbDetour( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePathDetour( drawObject, ResolveDemiTmbPathHook!.Original( drawObject, path, unk3 ) );
private IntPtr ResolveDemiVfxDetour( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePathDetour( drawObject, ResolveDemiVfxPathHook!.Original( drawObject, path, unk3, unk4, unk5 ) );
// Implementation
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePathDetour( IntPtr drawObject, IntPtr path )
=> ResolvePathDetour( FindParent( drawObject, out var collection ) == null
? Penumbra.CollectionManager.Default
: collection, path );
// Weapons have the characters DrawObject as a parent,
// but that may not be set yet when creating a new object, so we have to do the same detour
// as for Human DrawObjects that are just being created.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolveWeaponPathDetour( IntPtr drawObject, IntPtr path )
{
var parent = FindParent( drawObject, out var collection );
if( parent != null )
{
return ResolvePathDetour( collection, path );
}
var parentObject = ( ( DrawObject* )drawObject )->Object.ParentObject;
if( parentObject == null && LastGameObject != null )
{
var c2 = IdentifyCollection( LastGameObject );
DrawObjectToObject[ drawObject ] = ( c2, LastGameObject->ObjectIndex );
return ResolvePathDetour( c2, path );
}
parent = FindParent( ( IntPtr )parentObject, out collection );
return ResolvePathDetour( parent == null
? Penumbra.CollectionManager.Default
: collection, path );
}
// Just add or remove the resolved path.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePathDetour( ModCollection collection, IntPtr path )
{
if( path == IntPtr.Zero )
{
return path;
}
var gamePath = new Utf8String( ( byte* )path );
SetCollection( gamePath, collection );
return path;
}
}

View file

@ -0,0 +1,258 @@
using System;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
namespace Penumbra.Interop.Resolver;
public partial class PathResolver
{
public unsafe class ResolverHooks : IDisposable
{
public enum Type
{
Human,
Weapon,
Other,
}
private delegate IntPtr GeneralResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 );
private delegate IntPtr MPapResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 );
private delegate IntPtr MaterialResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 );
private delegate IntPtr EidResolveDelegate( IntPtr drawObject, IntPtr path, IntPtr unk3 );
private readonly Hook< GeneralResolveDelegate > _resolveDecalPathHook;
private readonly Hook< EidResolveDelegate > _resolveEidPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveImcPathHook;
private readonly Hook< MPapResolveDelegate > _resolveMPapPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveMdlPathHook;
private readonly Hook< MaterialResolveDelegate > _resolveMtrlPathHook;
private readonly Hook< MaterialResolveDelegate > _resolvePapPathHook;
private readonly Hook< GeneralResolveDelegate > _resolvePhybPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveSklbPathHook;
private readonly Hook< GeneralResolveDelegate > _resolveSkpPathHook;
private readonly Hook< EidResolveDelegate > _resolveTmbPathHook;
private readonly Hook< MaterialResolveDelegate > _resolveVfxPathHook;
private readonly PathResolver _parent;
public ResolverHooks( PathResolver parent, IntPtr* vTable, Type type )
{
_parent = parent;
_resolveDecalPathHook = Create< GeneralResolveDelegate >( vTable[ 83 ], type, ResolveDecalWeapon, ResolveDecal );
_resolveEidPathHook = Create< EidResolveDelegate >( vTable[ 85 ], type, ResolveEidWeapon, ResolveEid );
_resolveImcPathHook = Create< GeneralResolveDelegate >( vTable[ 81 ], type, ResolveImcWeapon, ResolveImc );
_resolveMPapPathHook = Create< MPapResolveDelegate >( vTable[ 79 ], type, ResolveMPapWeapon, ResolveMPap );
_resolveMdlPathHook = Create< GeneralResolveDelegate >( vTable[ 73 ], type, ResolveMdlWeapon, ResolveMdl, ResolveMdlHuman );
_resolveMtrlPathHook = Create< MaterialResolveDelegate >( vTable[ 82 ], type, ResolveMtrlWeapon, ResolveMtrl );
_resolvePapPathHook = Create< MaterialResolveDelegate >( vTable[ 76 ], type, ResolvePapWeapon, ResolvePap, ResolvePapHuman );
_resolvePhybPathHook = Create< GeneralResolveDelegate >( vTable[ 75 ], type, ResolvePhybWeapon, ResolvePhyb, ResolvePhybHuman );
_resolveSklbPathHook = Create< GeneralResolveDelegate >( vTable[ 72 ], type, ResolveSklbWeapon, ResolveSklb, ResolveSklbHuman );
_resolveSkpPathHook = Create< GeneralResolveDelegate >( vTable[ 74 ], type, ResolveSkpWeapon, ResolveSkp, ResolveSkpHuman );
_resolveTmbPathHook = Create< EidResolveDelegate >( vTable[ 77 ], type, ResolveTmbWeapon, ResolveTmb );
_resolveVfxPathHook = Create< MaterialResolveDelegate >( vTable[ 84 ], type, ResolveVfxWeapon, ResolveVfx );
}
public void Enable()
{
_resolveDecalPathHook.Enable();
_resolveEidPathHook.Enable();
_resolveImcPathHook.Enable();
_resolveMPapPathHook.Enable();
_resolveMdlPathHook.Enable();
_resolveMtrlPathHook.Enable();
_resolvePapPathHook.Enable();
_resolvePhybPathHook.Enable();
_resolveSklbPathHook.Enable();
_resolveSkpPathHook.Enable();
_resolveTmbPathHook.Enable();
_resolveVfxPathHook.Enable();
}
public void Disable()
{
_resolveDecalPathHook.Disable();
_resolveEidPathHook.Disable();
_resolveImcPathHook.Disable();
_resolveMPapPathHook.Disable();
_resolveMdlPathHook.Disable();
_resolveMtrlPathHook.Disable();
_resolvePapPathHook.Disable();
_resolvePhybPathHook.Disable();
_resolveSklbPathHook.Disable();
_resolveSkpPathHook.Disable();
_resolveTmbPathHook.Disable();
_resolveVfxPathHook.Disable();
}
public void Dispose()
{
_resolveDecalPathHook.Dispose();
_resolveEidPathHook.Dispose();
_resolveImcPathHook.Dispose();
_resolveMPapPathHook.Dispose();
_resolveMdlPathHook.Dispose();
_resolveMtrlPathHook.Dispose();
_resolvePapPathHook.Dispose();
_resolvePhybPathHook.Dispose();
_resolveSklbPathHook.Dispose();
_resolveSkpPathHook.Dispose();
_resolveTmbPathHook.Dispose();
_resolveVfxPathHook.Dispose();
}
private IntPtr ResolveDecal( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEid( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImc( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolvePath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMtrl( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePap( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePhyb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSklb( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSkp( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveTmb( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolvePath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfx( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolvePath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
{
using var eqdp = MetaChanger.ChangeEqdp( _parent, drawObject, modelType );
return ResolvePath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
}
private IntPtr ResolvePapHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
}
private IntPtr ResolvePhybHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSklbHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveSkpHuman( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
{
using var est = MetaChanger.ChangeEst( _parent, drawObject );
return ResolvePath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
}
private IntPtr ResolveDecalWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveDecalPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveEidWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPath( drawObject, _resolveEidPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveImcWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveImcPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveMPapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5 )
=> ResolveWeaponPath( drawObject, _resolveMPapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolveMdlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType )
=> ResolveWeaponPath( drawObject, _resolveMdlPathHook.Original( drawObject, path, unk3, modelType ) );
private IntPtr ResolveMtrlWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolveMtrlPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePapWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolvePapPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
private IntPtr ResolvePhybWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolvePhybPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSklbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveSklbPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveSkpWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4 )
=> ResolveWeaponPath( drawObject, _resolveSkpPathHook.Original( drawObject, path, unk3, unk4 ) );
private IntPtr ResolveTmbWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3 )
=> ResolveWeaponPath( drawObject, _resolveTmbPathHook.Original( drawObject, path, unk3 ) );
private IntPtr ResolveVfxWeapon( IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5 )
=> ResolveWeaponPath( drawObject, _resolveVfxPathHook.Original( drawObject, path, unk3, unk4, unk5 ) );
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other, T human ) where T : Delegate
{
var del = type switch
{
Type.Human => human,
Type.Weapon => weapon,
_ => other,
};
return Hook< T >.FromAddress( address, del );
}
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private static Hook< T > Create< T >( IntPtr address, Type type, T weapon, T other ) where T : Delegate
=> Create( address, type, weapon, other, other );
// Implementation
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolvePath( IntPtr drawObject, IntPtr path )
=> _parent._paths.ResolvePath( FindParent( drawObject, out var collection ) == null
? Penumbra.CollectionManager.Default
: collection, path );
// Weapons have the characters DrawObject as a parent,
// but that may not be set yet when creating a new object, so we have to do the same detour
// as for Human DrawObjects that are just being created.
[MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )]
private IntPtr ResolveWeaponPath( IntPtr drawObject, IntPtr path )
{
var parent = FindParent( drawObject, out var collection );
if( parent != null )
{
return _parent._paths.ResolvePath( collection, path );
}
var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject;
var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject );
if( parentCollection != null )
{
return _parent._paths.ResolvePath( parentCollection, path );
}
parent = FindParent( parentObject, out collection );
return _parent._paths.ResolvePath( parent == null
? Penumbra.CollectionManager.Default
: collection, path );
}
}
}

View file

@ -1,89 +0,0 @@
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Penumbra.Interop.Resolver;
public unsafe partial class PathResolver
{
[Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 B8 ?? ?? ?? ?? 66 89 83 ?? ?? ?? ?? 48 8B C3 48 89 8B ?? ?? ?? ?? 48 89 8B",
ScanType = ScanType.StaticAddress )]
public IntPtr* DrawObjectWeaponVTable;
public Hook< GeneralResolveDelegate >? ResolveWeaponDecalPathHook;
public Hook< EidResolveDelegate >? ResolveWeaponEidPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponImcPathHook;
public Hook< MPapResolveDelegate >? ResolveWeaponMPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponMdlPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponMtrlPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponPapPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponPhybPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponSklbPathHook;
public Hook< GeneralResolveDelegate >? ResolveWeaponSkpPathHook;
public Hook< EidResolveDelegate >? ResolveWeaponTmbPathHook;
public Hook< MaterialResolveDetour >? ResolveWeaponVfxPathHook;
private void SetupWeaponHooks()
{
ResolveWeaponDecalPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveDecalIdx ], ResolveWeaponDecalDetour );
ResolveWeaponEidPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveEidIdx ], ResolveWeaponEidDetour );
ResolveWeaponImcPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveImcIdx ], ResolveWeaponImcDetour );
ResolveWeaponMPapPathHook = Hook< MPapResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMPapIdx ], ResolveWeaponMPapDetour );
ResolveWeaponMdlPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveMdlIdx ], ResolveWeaponMdlDetour );
ResolveWeaponMtrlPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveMtrlIdx ], ResolveWeaponMtrlDetour );
ResolveWeaponPapPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolvePapIdx ], ResolveWeaponPapDetour );
ResolveWeaponPhybPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolvePhybIdx ], ResolveWeaponPhybDetour );
ResolveWeaponSklbPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSklbIdx ], ResolveWeaponSklbDetour );
ResolveWeaponSkpPathHook = Hook< GeneralResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveSkpIdx ], ResolveWeaponSkpDetour );
ResolveWeaponTmbPathHook = Hook< EidResolveDelegate >.FromAddress( DrawObjectWeaponVTable[ ResolveTmbIdx ], ResolveWeaponTmbDetour );
ResolveWeaponVfxPathHook = Hook< MaterialResolveDetour >.FromAddress( DrawObjectWeaponVTable[ ResolveVfxIdx ], ResolveWeaponVfxDetour );
}
private void EnableWeaponHooks()
{
ResolveWeaponDecalPathHook?.Enable();
ResolveWeaponEidPathHook?.Enable();
ResolveWeaponImcPathHook?.Enable();
ResolveWeaponMPapPathHook?.Enable();
ResolveWeaponMdlPathHook?.Enable();
ResolveWeaponMtrlPathHook?.Enable();
ResolveWeaponPapPathHook?.Enable();
ResolveWeaponPhybPathHook?.Enable();
ResolveWeaponSklbPathHook?.Enable();
ResolveWeaponSkpPathHook?.Enable();
ResolveWeaponTmbPathHook?.Enable();
ResolveWeaponVfxPathHook?.Enable();
}
private void DisableWeaponHooks()
{
ResolveWeaponDecalPathHook?.Disable();
ResolveWeaponEidPathHook?.Disable();
ResolveWeaponImcPathHook?.Disable();
ResolveWeaponMPapPathHook?.Disable();
ResolveWeaponMdlPathHook?.Disable();
ResolveWeaponMtrlPathHook?.Disable();
ResolveWeaponPapPathHook?.Disable();
ResolveWeaponPhybPathHook?.Disable();
ResolveWeaponSklbPathHook?.Disable();
ResolveWeaponSkpPathHook?.Disable();
ResolveWeaponTmbPathHook?.Disable();
ResolveWeaponVfxPathHook?.Disable();
}
private void DisposeWeaponHooks()
{
ResolveWeaponDecalPathHook?.Dispose();
ResolveWeaponEidPathHook?.Dispose();
ResolveWeaponImcPathHook?.Dispose();
ResolveWeaponMPapPathHook?.Dispose();
ResolveWeaponMdlPathHook?.Dispose();
ResolveWeaponMtrlPathHook?.Dispose();
ResolveWeaponPapPathHook?.Dispose();
ResolveWeaponPhybPathHook?.Dispose();
ResolveWeaponSklbPathHook?.Dispose();
ResolveWeaponSkpPathHook?.Dispose();
ResolveWeaponTmbPathHook?.Dispose();
ResolveWeaponVfxPathHook?.Dispose();
}
}

View file

@ -1,7 +1,9 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
@ -17,18 +19,24 @@ namespace Penumbra.Interop.Resolver;
// to resolve paths for character collections.
public partial class PathResolver : IDisposable
{
private readonly ResourceLoader _loader;
public bool Enabled { get; private set; }
public PathResolver( ResourceLoader loader )
private readonly ResourceLoader _loader;
private static readonly CutsceneCharacters Cutscenes = new();
private static readonly DrawObjectState DrawObjects = new();
private readonly AnimationState _animations;
private readonly PathState _paths;
private readonly MetaState _meta;
private readonly MaterialState _materials;
public unsafe PathResolver( ResourceLoader loader )
{
_loader = loader;
SignatureHelper.Initialise( this );
SetupHumanHooks();
SetupWeaponHooks();
SetupMonsterHooks();
SetupDemiHooks();
SetupMetaHooks();
_loader = loader;
_animations = new AnimationState( DrawObjects );
_paths = new PathState( this );
_meta = new MetaState( this, _paths.HumanVTable );
_materials = new MaterialState( _paths );
}
// The modified resolver that handles game path resolving.
@ -40,10 +48,10 @@ public partial class PathResolver : IDisposable
// If not use the default collection.
// We can remove paths after they have actually been loaded.
// A potential next request will add the path anew.
var nonDefault = HandleMaterialSubFiles( type, out var collection )
|| PathCollections.TryRemove( gamePath.Path, out collection )
|| HandleAnimationFile( type, gamePath, out collection )
|| HandleDecalFile( type, gamePath, out collection );
var nonDefault = _materials.HandleSubFiles( type, out var collection )
|| _paths.Consume( gamePath.Path, out collection )
|| _animations.HandleFiles( type, gamePath, out collection )
|| DrawObjects.HandleDecalFile( type, gamePath, out collection );
if( !nonDefault || collection == null )
{
collection = Penumbra.CollectionManager.Default;
@ -56,67 +64,10 @@ public partial class PathResolver : IDisposable
// so that the functions loading tex and shpk can find that path and use its collection.
// We also need to handle defaulted materials against a non-default collection.
var path = resolved == null ? gamePath.Path.ToString() : resolved.Value.FullName;
HandleMtrlCollection( collection, path, nonDefault, type, resolved, out data );
MaterialState.HandleCollection( collection, path, nonDefault, type, resolved, out data );
return true;
}
private bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, [NotNullWhen( true )] out ModCollection? collection )
{
if( type == ResourceType.Tex
&& _lastCreatedCollection != null
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( 'd', 'e', 'c', 'a', 'l', '_', 'f', 'a', 'c', 'e' ) )
{
collection = _lastCreatedCollection;
return true;
}
collection = null;
return false;
}
private bool HandleAnimationFile( ResourceType type, Utf8GamePath _, [NotNullWhen( true )] out ModCollection? collection )
{
switch( type )
{
case ResourceType.Tmb:
case ResourceType.Pap:
case ResourceType.Scd:
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Avfx:
_lastAvfxCollection = _animationLoadCollection ?? Penumbra.CollectionManager.Default;
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
case ResourceType.Atex:
if( _lastAvfxCollection != null )
{
collection = _lastAvfxCollection;
return true;
}
if( _animationLoadCollection != null )
{
collection = _animationLoadCollection;
return true;
}
break;
}
collection = null;
return false;
}
public void Enable()
{
if( Enabled )
@ -125,15 +76,12 @@ public partial class PathResolver : IDisposable
}
Enabled = true;
InitializeDrawObjects();
EnableHumanHooks();
EnableWeaponHooks();
EnableMonsterHooks();
EnableDemiHooks();
EnableMtrlHooks();
EnableDataHooks();
EnableMetaHooks();
Cutscenes.Enable();
DrawObjects.Enable();
_animations.Enable();
_paths.Enable();
_meta.Enable();
_materials.Enable();
_loader.ResolvePathCustomization += CharacterResolver;
PluginLog.Debug( "Character Path Resolver enabled." );
@ -147,16 +95,12 @@ public partial class PathResolver : IDisposable
}
Enabled = false;
DisableHumanHooks();
DisableWeaponHooks();
DisableMonsterHooks();
DisableDemiHooks();
DisableMtrlHooks();
DisableDataHooks();
DisableMetaHooks();
DrawObjectToObject.Clear();
PathCollections.Clear();
_animations.Disable();
DrawObjects.Disable();
Cutscenes.Disable();
_paths.Disable();
_meta.Disable();
_materials.Disable();
_loader.ResolvePathCustomization -= CharacterResolver;
PluginLog.Debug( "Character Path Resolver disabled." );
@ -165,18 +109,60 @@ public partial class PathResolver : IDisposable
public void Dispose()
{
Disable();
DisposeHumanHooks();
DisposeWeaponHooks();
DisposeMonsterHooks();
DisposeDemiHooks();
DisposeMtrlHooks();
DisposeDataHooks();
DisposeMetaHooks();
_paths.Dispose();
_animations.Dispose();
DrawObjects.Dispose();
Cutscenes.Dispose();
_meta.Dispose();
_materials.Dispose();
}
public unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject )
public static unsafe (IntPtr, ModCollection) IdentifyDrawObject( IntPtr drawObject )
{
var parent = FindParent( drawObject, out var collection );
return ( ( IntPtr )parent, collection );
}
public int CutsceneActor( int idx )
=> Cutscenes.GetParentIndex( idx );
// Use the stored information to find the GameObject and Collection linked to a DrawObject.
public static unsafe GameObject* FindParent( IntPtr drawObject, out ModCollection collection )
{
if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) )
{
collection = data.Item1;
return gameObject;
}
if( DrawObjects.LastGameObject != null
&& ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) )
{
collection = IdentifyCollection( DrawObjects.LastGameObject );
return DrawObjects.LastGameObject;
}
collection = IdentifyCollection( null );
return null;
}
private static unsafe ModCollection? GetCollection( IntPtr drawObject )
{
var parent = FindParent( drawObject, out var collection );
if( parent == null || collection == Penumbra.CollectionManager.Default )
{
return null;
}
return collection.HasCache ? collection : null;
}
internal IEnumerable< KeyValuePair< Utf8String, ModCollection > > PathCollections
=> _paths.Paths;
internal IEnumerable< KeyValuePair< IntPtr, (ModCollection, int) > > DrawObjectMap
=> DrawObjects.DrawObjects;
internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors
=> Cutscenes.Actors;
}

View file

@ -47,7 +47,6 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
throw new IndexOutOfRangeException();
}
var x = new ReadOnlySpan< ushort >( ( ushort* )Data, Length / 2 );
return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) );
}
set

View file

@ -67,7 +67,6 @@ public unsafe class ImcFile : MetaBaseFile
public readonly Utf8GamePath Path;
public readonly int NumParts;
public bool ChangesSinceLoad = true;
public ReadOnlySpan< ImcEntry > Span
=> new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry ));

View file

@ -74,6 +74,8 @@ public partial class Mod
public bool FileChanges { get; private set; }
private List< FileRegistry > _availableFiles = null!;
private List< FileRegistry > _mtrlFiles = null!;
private List< FileRegistry > _mdlFiles = null!;
private readonly HashSet< Utf8GamePath > _usedPaths = new();
// All paths that are used in
@ -82,6 +84,12 @@ public partial class Mod
public IReadOnlySet< FullPath > MissingFiles
=> _missingFiles;
public IReadOnlyList< FileRegistry > MtrlFiles
=> _mtrlFiles;
public IReadOnlyList< FileRegistry > MdlFiles
=> _mdlFiles;
// Remove all path redirections where the pointed-to file does not exist.
public void RemoveMissingPaths()
{
@ -121,6 +129,8 @@ public partial class Mod
.OfType< FileRegistry >() )
.ToList();
_usedPaths.Clear();
_mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList();
_mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList();
FileChanges = false;
foreach( var subMod in _mod.AllSubMods )
{

View file

@ -17,8 +17,9 @@ 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
private readonly List< ModelMaterialInfo > _modelFiles = new();
public IReadOnlyList< ModelMaterialInfo > ModelFiles
=> _modelFiles;
// Non-ASCII encoding can not be used.
@ -50,7 +51,9 @@ public partial class Mod
public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown )
{
if( !ValidString( toSuffix ) )
{
return;
}
foreach( var info in _modelFiles )
{
@ -62,17 +65,17 @@ public partial class Mod
&& ( 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 );
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()
public void ScanModels()
{
_modelFiles.Clear();
foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) )
foreach( var file in _mdlFiles.Where( f => f.File.Extension == ".mdl" ) )
{
try
{
@ -82,7 +85,7 @@ public partial class Mod
.Select( p => p.Item2 ).ToArray();
if( materials.Length > 0 )
{
_modelFiles.Add( new MaterialInfo( file.File, mdlFile, materials ) );
_modelFiles.Add( new ModelMaterialInfo( file.File, mdlFile, materials ) );
}
}
catch( Exception e )
@ -93,22 +96,22 @@ public partial class Mod
}
// A class that collects information about skin materials in a model file and handle changes on them.
public class MaterialInfo
public class ModelMaterialInfo
{
public readonly FullPath Path;
private readonly MdlFile _file;
private readonly string[] _currentMaterials;
private readonly IReadOnlyList<int> _materialIndices;
public readonly FullPath Path;
public readonly MdlFile File;
private readonly string[] _currentMaterials;
private readonly IReadOnlyList< int > _materialIndices;
public bool Changed { get; private set; }
public IReadOnlyList<string> CurrentMaterials
public IReadOnlyList< string > CurrentMaterials
=> _currentMaterials;
private IEnumerable<string> DefaultMaterials
=> _materialIndices.Select( i => _file.Materials[i] );
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 (string Current, string Default) this[ int idx ]
=> ( _currentMaterials[ idx ], File.Materials[ _materialIndices[ idx ] ] );
public int Count
=> _materialIndices.Count;
@ -116,8 +119,8 @@ public partial class Mod
// 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;
var mat = File.Materials[ _materialIndices[ materialIdx ] ];
_currentMaterials[ materialIdx ] = value;
if( mat != value )
{
Changed = true;
@ -138,12 +141,12 @@ public partial class Mod
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
_file.Materials[idx] = _currentMaterials[i];
File.Materials[ idx ] = _currentMaterials[ i ];
}
try
{
File.WriteAllBytes( Path.FullName, _file.Write() );
System.IO.File.WriteAllBytes( Path.FullName, File.Write() );
Changed = false;
}
catch( Exception e )
@ -157,23 +160,25 @@ public partial class Mod
public void Restore()
{
if( !Changed )
{
return;
}
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
_currentMaterials[i] = _file.Materials[idx];
_currentMaterials[ i ] = File.Materials[ idx ];
}
Changed = false;
}
public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList<int> indices )
public ModelMaterialInfo( FullPath path, MdlFile file, IReadOnlyList< int > indices )
{
Path = path;
_file = file;
_materialIndices = indices;
Path = path;
File = file;
_materialIndices = indices;
_currentMaterials = DefaultMaterials.ToArray();
}
}
}
}

View file

@ -70,6 +70,7 @@ public sealed partial class Mod
var group = mod._groups[ groupIdx ];
ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 );
mod._groups.RemoveAt( groupIdx );
UpdateSubModPositions( mod, groupIdx );
group.DeleteFile( mod.ModPath, groupIdx );
ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 );
}
@ -78,18 +79,22 @@ public sealed partial class Mod
{
if( mod._groups.Move( groupIdxFrom, groupIdxTo ) )
{
foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( Math.Min( groupIdxFrom, groupIdxTo ) ) )
{
foreach( var (o, optionIdx) in group.OfType<SubMod>().WithIndex() )
{
o.SetPosition( groupIdx, optionIdx );
}
}
UpdateSubModPositions( mod, Math.Min( groupIdxFrom, groupIdxTo ) );
ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo );
}
}
private static void UpdateSubModPositions( Mod mod, int fromGroup )
{
foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( fromGroup ) )
{
foreach( var (o, optionIdx) in group.OfType<SubMod>().WithIndex() )
{
o.SetPosition( groupIdx, optionIdx );
}
}
}
public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription )
{
var group = mod._groups[ groupIdx ];

View file

@ -13,6 +13,7 @@ public sealed partial class Mod
public event Action? ModDiscoveryStarted;
public event Action? ModDiscoveryFinished;
public event Action< string, bool > ModDirectoryChanged;
// Change the mod base directory and discover available mods.
public void DiscoverMods( string newDir )
@ -35,6 +36,10 @@ public sealed partial class Mod
{
Valid = false;
BasePath = new DirectoryInfo( "." );
if( Penumbra.Config.ModDirectory != BasePath.FullName )
{
ModDirectoryChanged.Invoke( string.Empty, false );
}
}
else
{
@ -56,13 +61,19 @@ public sealed partial class Mod
Valid = Directory.Exists( newDir.FullName );
if( Penumbra.Config.ModDirectory != BasePath.FullName )
{
PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.", Penumbra.Config.ModDirectory, BasePath.FullName );
Penumbra.Config.ModDirectory = BasePath.FullName;
Penumbra.Config.Save();
ModDirectoryChanged.Invoke( BasePath.FullName, Valid );
}
}
}
private static void OnModDirectoryChange( string newPath, bool _ )
{
PluginLog.Information( "Set new mod base directory from {OldDirectory:l} to {NewDirectory:l}.",
Penumbra.Config.ModDirectory, newPath );
Penumbra.Config.ModDirectory = newPath;
Penumbra.Config.Save();
}
// Discover new mods.
public void DiscoverMods()
{

View file

@ -34,6 +34,7 @@ public sealed partial class Mod
public Manager( string modDirectory )
{
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory( modDirectory, true );
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;

View file

@ -97,7 +97,7 @@ public partial class Mod
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
.Where( t => t.Item1 );
var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving.
var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving.
{
Name = option.Name,
};
@ -112,7 +112,7 @@ public partial class Mod
// Create an empty sub mod for single groups with None options.
internal static ISubMod CreateEmptySubMod( string name )
=> new SubMod(null! ) // Mod is irrelevant here, only used for saving.
=> new SubMod( null! ) // Mod is irrelevant here, only used for saving.
{
Name = name,
};
@ -144,6 +144,16 @@ public partial class Mod
// and the path must obviously be valid itself.
public static string ReplaceBadXivSymbols( string s, string replacement = "_" )
{
if( s == "." )
{
return replacement;
}
if( s == ".." )
{
return replacement + replacement;
}
StringBuilder sb = new(s.Length);
foreach( var c in s )
{

View file

@ -70,7 +70,7 @@ public class Penumbra : IDalamudPlugin
public Penumbra( DalamudPluginInterface pluginInterface )
{
Dalamud.Initialize( pluginInterface );
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
GameData.GameData.GetIdentifier( Dalamud.GameData );
DevPenumbraExists = CheckDevPluginPenumbra();
IsNotInstalledPenumbra = CheckIsNotInstalled();
@ -79,9 +79,9 @@ public class Penumbra : IDalamudPlugin
Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() );
Config = Configuration.Load();
TempMods = new TempModManager();
MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader( this );
TempMods = new TempModManager();
MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader( this );
ResourceLoader.EnableHooks();
ResourceLogger = new ResourceLogger( ResourceLoader );
ResidentResources = new ResidentResourceManager();
@ -450,10 +450,10 @@ public class Penumbra : IDalamudPlugin
c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) );
sb.AppendLine( "**Collections**" );
sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 );
sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) );
sb.AppendFormat( "> **`Default Collection: `** {0}\n", CollectionManager.Default.AnonymizedName);
sb.AppendFormat( "> **`Current Collection: `** {0}\n", CollectionManager.Current.AnonymizedName);
sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count - 1 );
sb.AppendFormat( "> **`Active Collections: `** {0}\n", CollectionManager.Count( c => c.HasCache ) );
sb.AppendFormat( "> **`Base Collection: `** {0}\n", CollectionManager.Default.AnonymizedName );
sb.AppendFormat( "> **`Selected Collection: `** {0}\n", CollectionManager.Current.AnonymizedName );
foreach( var type in CollectionTypeExtensions.Special )
{
var collection = CollectionManager.ByType( type );

View file

@ -1,18 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-windows</TargetFramework>
<TargetFramework>net6.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
<PlatformTarget>x64</PlatformTarget>
<AssemblyTitle>Penumbra</AssemblyTitle>
<Company>absolute gangstas</Company>
<Product>Penumbra</Product>
<Copyright>Copyright © 2020</Copyright>
<Copyright>Copyright © 2022</Copyright>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<OutputPath>bin\$(Configuration)\</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<PropertyGroup>
@ -25,36 +27,43 @@
</Content>
</ItemGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
</ItemGroup>

View file

@ -7,7 +7,7 @@
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"Tags": [ "modding" ],
"DalamudApiLevel": 6,
"DalamudApiLevel": 7,
"LoadPriority": 69420,
"LoadRequiredState": 2,
"LoadSync": true,

View file

@ -0,0 +1,607 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Logging;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Files;
using Penumbra.Mods;
using Functions = Penumbra.GameData.Util.Functions;
namespace Penumbra.UI.Classes;
public partial class ModEditWindow
{
private readonly FileEditor< MtrlFile > _materialTab;
private readonly FileEditor< MdlFile > _modelTab;
private class FileEditor< T > where T : class, IWritable
{
private readonly string _tabName;
private readonly string _fileType;
private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles;
private readonly Func< T, bool, bool > _drawEdit;
private Mod.Editor.FileRegistry? _currentPath;
private T? _currentFile;
private bool _changed;
private string _defaultPath = string.Empty;
private bool _inInput = false;
private T? _defaultFile;
private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!;
public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles,
Func< T, bool, bool > drawEdit )
{
_tabName = tabName;
_fileType = fileType;
_getFiles = getFiles;
_drawEdit = drawEdit;
}
public void Draw()
{
_list = _getFiles();
if( _list.Count == 0 )
{
return;
}
using var tab = ImRaii.TabItem( _tabName );
if( !tab )
{
return;
}
ImGui.NewLine();
DrawFileSelectCombo();
SaveButton();
ImGui.SameLine();
ResetButton();
ImGui.SameLine();
DefaultInput();
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
DrawFilePanel();
}
private void DefaultInput()
{
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength );
_inInput = ImGui.IsItemActive();
if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 )
{
try
{
var file = Dalamud.GameData.GetFile( _defaultPath );
if( file != null )
{
_defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T;
}
}
catch
{
_defaultFile = null;
}
}
}
public void Reset()
{
_currentPath = null;
_currentFile = null;
_changed = false;
}
private void DrawFileSelectCombo()
{
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." );
if( !combo )
{
return;
}
foreach( var file in _list )
{
if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) )
{
UpdateCurrentFile( file );
}
}
}
private void UpdateCurrentFile( Mod.Editor.FileRegistry path )
{
if( ReferenceEquals( _currentPath, path ) )
{
return;
}
_changed = false;
_currentPath = path;
try
{
var bytes = File.ReadAllBytes( _currentPath.File.FullName );
_currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T;
}
catch( Exception e )
{
PluginLog.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" );
_currentFile = null;
}
}
private void SaveButton()
{
if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero,
$"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) )
{
File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() );
_changed = false;
}
}
private void ResetButton()
{
if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero,
$"Reset all changes made to the {_fileType} file.", !_changed ) )
{
var tmp = _currentPath;
_currentPath = null;
UpdateCurrentFile( tmp! );
}
}
private void DrawFilePanel()
{
using var child = ImRaii.Child( "##filePanel", -Vector2.One, true );
if( !child )
{
return;
}
if( _currentPath != null )
{
if( _currentFile == null )
{
ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." );
}
else
{
using var id = ImRaii.PushId( 0 );
_changed |= _drawEdit( _currentFile, false );
}
}
if( !_inInput && _defaultPath.Length > 0 )
{
if( _currentPath != null )
{
ImGui.NewLine();
ImGui.NewLine();
ImGui.TextUnformatted( $"Preview of {_defaultPath}:" );
ImGui.Separator();
}
if( _defaultFile == null )
{
ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file." );
}
else
{
using var id = ImRaii.PushId( 1 );
_drawEdit( _defaultFile, true );
}
}
}
}
private static bool DrawModelPanel( MdlFile file, bool disabled )
{
var ret = false;
for( var i = 0; i < file.Materials.Length; ++i )
{
using var id = ImRaii.PushId( i );
var tmp = file.Materials[ i ];
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
&& tmp.Length > 0
&& tmp != file.Materials[ i ] )
{
file.Materials[ i ] = tmp;
ret = true;
}
}
return disabled && ret;
}
private static bool DrawMaterialPanel( MtrlFile file, bool disabled )
{
var ret = DrawMaterialTextureChange( file, disabled );
ImGui.NewLine();
ret |= DrawMaterialColorSetChange( file, disabled );
return disabled && ret;
}
private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled )
{
using var id = ImRaii.PushId( "Textures" );
var ret = false;
for( var i = 0; i < file.Textures.Length; ++i )
{
using var _ = ImRaii.PushId( i );
var tmp = file.Textures[ i ].Path;
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
&& tmp.Length > 0
&& tmp != file.Textures[ i ].Path )
{
ret = true;
file.Textures[ i ].Path = tmp;
}
}
return ret;
}
private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
{
if( file.ColorSets.Length == 0 )
{
return false;
}
using var table = ImRaii.Table( "##ColorSets", 10,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
if( !table )
{
return false;
}
ImGui.TableNextColumn();
ImGui.TableHeader( string.Empty );
ImGui.TableNextColumn();
ImGui.TableHeader( "Row" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Diffuse" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Specular" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Emissive" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Gloss" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Tile" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Repeat" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Skew" );
ImGui.TableNextColumn();
ImGui.TableHeader( "Dye" );
var ret = false;
for( var j = 0; j < file.ColorSets.Length; ++j )
{
using var _ = ImRaii.PushId( j );
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{
ret |= DrawColorSetRow( file, j, i, disabled );
ImGui.TableNextRow();
}
}
return ret;
}
private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Export this row to your clipboard.", false, true ) )
{
try
{
var data = new byte[MtrlFile.ColorSet.Row.Size + 2];
fixed( byte* ptr = data )
{
Functions.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size );
Functions.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 );
}
var text = Convert.ToBase64String( data );
ImGui.SetClipboardText( text );
}
catch
{
// ignored
}
}
}
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
{
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
"Import an exported row from your clipboard onto this row.", disabled, true ) )
{
try
{
var text = ImGui.GetClipboardText();
var data = Convert.FromBase64String( text );
if( data.Length != MtrlFile.ColorSet.Row.Size + 2
|| file.ColorSets.Length <= colorSetIdx )
{
return false;
}
fixed( byte* ptr = data )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr;
if( file.ColorDyeSets.Length <= colorSetIdx )
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size );
}
}
return true;
}
catch
{
// ignored
}
}
return false;
}
private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
{
using var id = ImRaii.PushId( rowIdx );
var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
var hasDye = file.ColorDyeSets.Length > colorSetIdx;
var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row();
var floatSize = 70 * ImGuiHelpers.GlobalScale;
var intSize = 45 * ImGuiHelpers.GlobalScale;
ImGui.TableNextColumn();
ColorSetCopyClipboardButton( row, dye );
ImGui.SameLine();
var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled );
ImGui.TableNextColumn();
ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" );
ImGui.TableNextColumn();
using var dis = ImRaii.Disabled(disabled);
ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c );
if( hasDye )
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled );
}
ImGui.TableNextColumn();
ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c );
ImGui.SameLine();
var tmpFloat = row.SpecularStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.SpecularStrength )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
ret = true;
}
ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled );
if( hasDye )
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled );
}
ImGui.TableNextColumn();
ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c );
if( hasDye )
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled );
}
ImGui.TableNextColumn();
tmpFloat = row.GlossStrength;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.GlossStrength )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
ret = true;
}
ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled );
if( hasDye )
{
ImGui.SameLine();
ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled );
}
ImGui.TableNextColumn();
int tmpInt = row.TileSet;
ImGui.SetNextItemWidth( intSize );
if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
ret = true;
}
ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.TableNextColumn();
tmpFloat = row.MaterialRepeat.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.X )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
ret = true;
}
ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.SameLine();
tmpFloat = row.MaterialRepeat.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.Y )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
ret = true;
}
ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.TableNextColumn();
tmpFloat = row.MaterialSkew.X;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.X )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
ret = true;
}
ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.SameLine();
tmpFloat = row.MaterialSkew.Y;
ImGui.SetNextItemWidth( floatSize );
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.Y )
{
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
ret = true;
}
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
ImGui.TableNextColumn();
if( hasDye )
{
tmpInt = dye.Template;
ImGui.SetNextItemWidth( intSize );
if( ImGui.InputInt( "##DyeTemplate", ref tmpInt, 0, 0 )
&& tmpInt != dye.Template
&& tmpInt is >= 0 and <= ushort.MaxValue )
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = ( ushort )tmpInt;
ret = true;
}
ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled );
}
return ret;
}
private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter )
{
var ret = false;
var tmp = input;
if( ImGui.ColorEdit3( label, ref tmp,
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip )
&& tmp != input )
{
setter( tmp );
ret = true;
}
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
return ret;
}
private void DrawMaterialReassignmentTab()
{
if( _editor!.ModelFiles.Count == 0 )
{
return;
}
using var tab = ImRaii.TabItem( "Material Reassignment" );
if( !tab )
{
return;
}
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 );
}
}
}
}
}

View file

@ -319,7 +319,10 @@ public partial class ModEditWindow
if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, !changes ) )
{
var failedFiles = _editor!.ApplyFiles();
PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.Name}." );
if( failedFiles > 0 )
{
PluginLog.Information( $"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}." );
}
}

View file

@ -79,7 +79,6 @@ public partial class ModEditWindow
if( table )
{
drawNew( _editor!, _iconSize );
ImGui.Separator();
foreach( var (item, index) in items.ToArray().WithIndex() )
{
using var id = ImRaii.PushId( index );
@ -114,12 +113,12 @@ public partial class ModEditWindow
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) )
if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) )
{
_new = _new with { SetId = setId };
}
ImGuiUtil.HoverTooltip( "Model Set ID" );
ImGuiUtil.HoverTooltip( "Model Set ID");
ImGui.TableNextColumn();
if( EqpEquipSlotCombo( "##eqpSlot", _new.Slot, out var slot ) )
@ -127,9 +126,10 @@ public partial class ModEditWindow
_new = _new with { Slot = slot };
}
ImGuiUtil.HoverTooltip( "Equip Slot" );
ImGuiUtil.HoverTooltip( "Equip Slot");
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
new Vector2( 3 * ImGuiHelpers.GlobalScale, ImGui.GetStyle().ItemSpacing.Y ) );
@ -209,7 +209,7 @@ public partial class ModEditWindow
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) )
if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) )
{
_new = _new with { SetId = setId };
}
@ -241,6 +241,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Equip Slot" );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
var (bit1, bit2) = defaultEntry.ToBits( _new.Slot );
Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ );
@ -337,7 +338,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Object Type" );
ImGui.TableNextColumn();
if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, ushort.MaxValue ) )
if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue ) )
{
_new = _new with { PrimaryId = setId };
}
@ -360,7 +361,7 @@ public partial class ModEditWindow
}
else
{
if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, ushort.MaxValue ) )
if( IdInput( "##imcId2", 100 * ImGuiHelpers.GlobalScale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue ) )
{
_new = _new with { SecondaryId = setId2 };
}
@ -369,7 +370,7 @@ public partial class ModEditWindow
}
ImGui.TableNextColumn();
if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, byte.MaxValue ) )
if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue ) )
{
_new = _new with { Variant = variant };
}
@ -377,6 +378,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Variant ID" );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _,
1, byte.MaxValue, 0f );
@ -518,7 +520,7 @@ public partial class ModEditWindow
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) )
if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1 ) )
{
_new = _new with { SetId = setId };
}
@ -550,6 +552,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "EST Type" );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f );
}
@ -577,7 +580,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "EST Type" );
// Values
var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId );
var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId );
ImGui.TableNextColumn();
if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
out var entry, 0, ushort.MaxValue, 0.05f ) )
@ -616,7 +619,7 @@ public partial class ModEditWindow
// Identifier
ImGui.TableNextColumn();
if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, ExpandedEqpGmpBase.Count - 1 ) )
if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1 ) )
{
_new = _new with { SetId = setId };
}
@ -624,6 +627,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Model Set ID" );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ );
ImGui.TableNextColumn();
@ -743,6 +747,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip( "Scaling Type" );
// Values
using var disabled = ImRaii.Disabled();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth( FloatWidth );
ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f );
@ -810,13 +815,13 @@ public partial class ModEditWindow
// A number input for ids with a optional max id of given width.
// Returns true if newId changed against currentId.
private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int maxId )
private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId )
{
int tmp = currentId;
ImGui.SetNextItemWidth( width );
if( ImGui.InputInt( label, ref tmp, 0 ) )
{
tmp = Math.Clamp( tmp, 1, maxId );
tmp = Math.Clamp( tmp, minId, maxId );
}
newId = ( ushort )tmp;
@ -831,7 +836,7 @@ public partial class ModEditWindow
defaultValue ? ColorId.DecreasedMetaValue.Value() : ColorId.IncreasedMetaValue.Value(), defaultValue != currentValue );
newValue = currentValue;
ImGui.Checkbox( label, ref newValue );
ImGuiUtil.HoverTooltip( tooltip );
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
return newValue != currentValue;
}
@ -850,7 +855,7 @@ public partial class ModEditWindow
newValue = Math.Clamp( newValue, minValue, maxValue );
}
ImGuiUtil.HoverTooltip( tooltip );
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
return newValue != currentValue;
}

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Threading.Tasks;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
@ -165,16 +164,6 @@ public partial class ModEditWindow
{
try
{
if( fromDisk )
{
var tmp = new TmpTexFile();
using var stream = File.OpenRead( path );
using var br = new BinaryReader( stream );
tmp.Load(br);
return (tmp.RgbaData, tmp.Header.Width, tmp.Header.Height);
}
var tex = fromDisk ? Dalamud.GameData.GameData.GetFileFromDisk< TexFile >( path ) : Dalamud.GameData.GetFile< TexFile >( path );
if( tex == null )
{
@ -253,7 +242,14 @@ public partial class ModEditWindow
if( data != null )
{
wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 );
try
{
wrap = Dalamud.PluginInterface.UiBuilder.LoadImageRaw( data, width, height, 4 );
}
catch( Exception e )
{
PluginLog.Error( $"Could not load raw image:\n{e}" );
}
}
UpdateCenter();
@ -367,7 +363,10 @@ public partial class ModEditWindow
if( wrap != null )
{
ImGui.TextUnformatted( $"Image Dimensions: {wrap.Width} x {wrap.Height}" );
size = size with { Y = wrap.Height * size.X / wrap.Width };
size = size.X < wrap.Width
? size with { Y = wrap.Height * size.X / wrap.Width }
: new Vector2( wrap.Width, wrap.Height );
ImGui.Image( wrap.ImGuiHandle, size );
}
else if( path.Length > 0 )
@ -436,8 +435,10 @@ public partial class ModEditWindow
return;
}
var leftRightWidth = new Vector2( ( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 );
var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 );
var leftRightWidth =
new Vector2(
( ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetStyle().FramePadding.X * 4 ) / 3, -1 );
var imageSize = new Vector2( leftRightWidth.X - ImGui.GetStyle().FramePadding.X * 2 );
using( var child = ImRaii.Child( "ImageLeft", leftRightWidth, true ) )
{
if( child )

View file

@ -10,6 +10,7 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Mods;
using Penumbra.Util;
using static Penumbra.Mods.Mod;
@ -40,11 +41,16 @@ public partial class ModEditWindow : Window, IDisposable
MaximumSize = 4000 * Vector2.One,
};
_selectedFiles.Clear();
_modelTab.Reset();
_materialTab.Reset();
}
public void ChangeOption( ISubMod? subMod )
=> _editor?.SetSubMod( subMod );
public void UpdateModels()
=> _editor?.ScanModels();
public override bool DrawConditions()
=> _editor != null;
@ -129,7 +135,9 @@ public partial class ModEditWindow : Window, IDisposable
DrawSwapTab();
DrawMissingFilesTab();
DrawDuplicatesTab();
DrawMaterialChangeTab();
DrawMaterialReassignmentTab();
_modelTab.Draw();
_materialTab.Draw();
DrawTextureTab();
}
@ -220,118 +228,41 @@ public partial class ModEditWindow : Window, IDisposable
}
}
private void DrawMaterialChangeTab()
private void DrawMissingFilesTab()
{
using var tab = ImRaii.TabItem( "Model Materials" );
if( !tab )
if( _editor!.MissingFiles.Count == 0 )
{
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()
{
using var tab = ImRaii.TabItem( "Missing Files" );
if( !tab )
{
return;
}
if( _editor!.MissingFiles.Count == 0 )
ImGui.NewLine();
if( ImGui.Button( "Remove Missing Files from Mod" ) )
{
ImGui.NewLine();
ImGui.TextUnformatted( "No missing files detected." );
_editor.RemoveMissingPaths();
}
else
using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true );
if( !child )
{
if( ImGui.Button( "Remove Missing Files from Mod" ) )
{
_editor.RemoveMissingPaths();
}
return;
}
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 )
{
return;
}
using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One );
if( !table )
{
return;
}
foreach( var path in _editor.MissingFiles )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( path.FullName );
}
foreach( var path in _editor.MissingFiles )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( path.FullName );
}
}
@ -572,7 +503,12 @@ public partial class ModEditWindow : Window, IDisposable
public ModEditWindow()
: base( WindowBaseLabel )
{ }
{
_materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawMaterialPanel );
_modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawModelPanel );
}
public void Dispose()
{

View file

@ -200,9 +200,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void AddImportModButton( Vector2 size )
{
var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size,
"Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true );
"Import one or multiple mods from Tex Tools Mod Pack Files.", !Penumbra.ModManager.Valid, true );
ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport );
if (!button)
if( !button )
{
return;
}
@ -212,15 +212,16 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
: Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null;
_hasSetFolder = true;
_fileManager.OpenFileDialog( "Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) =>
{
if( s )
_fileManager.OpenFileDialog( "Import Mod Pack",
"Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) =>
{
_import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ),
AddNewMod );
ImGui.OpenPopup( "Import Status" );
}
}, 0, modPath );
if( s )
{
_import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ),
AddNewMod );
ImGui.OpenPopup( "Import Status" );
}
}, 0, modPath );
}
// Draw the progress information for import.
@ -312,13 +313,20 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
{
ImGui.OpenPopup( "ExtendedHelp" );
}
ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp );
}
// Helpers.
private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false )
{
var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l => l.Value );
var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l =>
{
// Any mod handled here should not stay new.
Penumbra.ModManager.NewMods.Remove( l.Value );
return l.Value;
} );
if( inherit )
{
Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled );

View file

@ -11,6 +11,7 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.ByteString;
using Penumbra.Interop.Loader;
using Penumbra.Interop.Resolver;
using Penumbra.Interop.Structs;
using CharacterUtility = Penumbra.Interop.CharacterUtility;
@ -86,9 +87,9 @@ public partial class ConfigWindow
var manager = Penumbra.ModManager;
PrintValue( "Penumbra Version", $"{Penumbra.Version} {DebugVersionString}" );
PrintValue( "Git Commit Hash", Penumbra.CommitHash );
PrintValue( "Current Collection", Penumbra.CollectionManager.Current.Name );
PrintValue( SelectedCollection, Penumbra.CollectionManager.Current.Name );
PrintValue( " has Cache", Penumbra.CollectionManager.Current.HasCache.ToString() );
PrintValue( "Default Collection", Penumbra.CollectionManager.Default.Name );
PrintValue( DefaultCollection, Penumbra.CollectionManager.Default.Name );
PrintValue( " has Cache", Penumbra.CollectionManager.Default.HasCache.ToString() );
PrintValue( "Mod Manager BasePath", manager.BasePath.Name );
PrintValue( "Mod Manager BasePath-Full", manager.BasePath.FullName );
@ -155,45 +156,63 @@ public partial class ConfigWindow
return;
}
using var drawTree = ImRaii.TreeNode( "Draw Object to Object" );
if( drawTree )
using( var drawTree = ImRaii.TreeNode( "Draw Object to Object" ) )
{
using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit );
if( table )
if( drawTree )
{
foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectToObject )
using var table = ImRaii.Table( "###DrawObjectResolverTable", 5, ImGuiTableFlags.SizingFixedFit );
if( table )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( ptr.ToString( "X" ) );
ImGui.TableNextColumn();
ImGui.TextUnformatted( idx.ToString() );
ImGui.TableNextColumn();
var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx );
var (address, name) =
obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" );
ImGui.TextUnformatted( address );
ImGui.TableNextColumn();
ImGui.TextUnformatted( name );
ImGui.TableNextColumn();
ImGui.TextUnformatted( c.Name );
foreach( var (ptr, (c, idx)) in _window._penumbra.PathResolver.DrawObjectMap )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( ptr.ToString( "X" ) );
ImGui.TableNextColumn();
ImGui.TextUnformatted( idx.ToString() );
ImGui.TableNextColumn();
var obj = ( GameObject* )Dalamud.Objects.GetObjectAddress( idx );
var (address, name) =
obj != null ? ( $"0x{( ulong )obj:X}", new Utf8String( obj->Name ).ToString() ) : ( "NULL", "NULL" );
ImGui.TextUnformatted( address );
ImGui.TableNextColumn();
ImGui.TextUnformatted( name );
ImGui.TableNextColumn();
ImGui.TextUnformatted( c.Name );
}
}
}
}
drawTree.Dispose();
using var pathTree = ImRaii.TreeNode( "Path Collections" );
if( pathTree )
using( var pathTree = ImRaii.TreeNode( "Path Collections" ) )
{
using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit );
if( pathTree )
{
using var table = ImRaii.Table( "###PathCollectionResolverTable", 2, ImGuiTableFlags.SizingFixedFit );
if( table )
{
foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections )
{
ImGui.TableNextColumn();
ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.Name );
}
}
}
}
using var cutsceneTree = ImRaii.TreeNode( "Cutscene Actors" );
if( cutsceneTree )
{
using var table = ImRaii.Table( "###PCutsceneResolverTable", 2, ImGuiTableFlags.SizingFixedFit );
if( table )
{
foreach( var (path, collection) in _window._penumbra.PathResolver.PathCollections )
foreach( var (idx, actor) in _window._penumbra.PathResolver.CutsceneActors )
{
ImGui.TableNextColumn();
ImGuiNative.igTextUnformatted( path.Path, path.Path + path.Length );
ImGui.TextUnformatted( $"Cutscene Actor {idx}" );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.Name );
ImGui.TextUnformatted( actor.Name.ToString() );
}
}
}

View file

@ -93,6 +93,26 @@ public partial class ConfigWindow
MoveDirectory.Draw( _mod, buttonSize );
ImGui.Dummy( _window._defaultSpace );
DrawUpdateBibo( buttonSize );
ImGui.Dummy( _window._defaultSpace );
}
private void DrawUpdateBibo( Vector2 buttonSize)
{
if( ImGui.Button( "Update Bibo Material", buttonSize ) )
{
var editor = new Mod.Editor( _mod, null );
editor.ReplaceAllMaterials( "bibo", "b" );
editor.ReplaceAllMaterials( "bibopube", "c" );
editor.SaveAllModels();
_window.ModEditPopup.UpdateModels();
}
ImGuiUtil.HoverTooltip( "For every model in this mod, change all material names that end in a _b or _c suffix to a _bibo or _bibopube suffix respectively.\n"
+ "Does nothing if the mod does not contain any such models or no model contains such materials.\n"
+ "Use this for outdated mods made for old Bibo bodies.\n"
+ "Go to Advanced Editing for more fine-tuned control over material assignment." );
}
private void BackupButtons( Vector2 buttonSize )

View file

@ -126,8 +126,8 @@ public partial class ConfigWindow
_currentPriority = null;
}
ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with higher priority take precedence before Mods with lower priority.\n"
+ "That means, if Mod A should overwrite changes from Mod B, Mod A should have higher priority than Mod B." );
ImGuiUtil.LabeledHelpMarker( "Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n"
+ "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B." );
}
// Draw a button to remove the current settings and inherit them instead

View file

@ -88,7 +88,7 @@ public partial class ConfigWindow
return;
}
ImGui.TextWrapped( _mod.Description );
ImGuiUtil.TextWrapped( _mod.Description );
}
// A simple clipped list of changed items.

View file

@ -79,12 +79,12 @@ public partial class ConfigWindow
private static void DrawDefaultCollectionButton( Vector2 width )
{
var name = $"Default Collection ({Penumbra.CollectionManager.Default.Name})";
var name = $"{DefaultCollection} ({Penumbra.CollectionManager.Default.Name})";
var isCurrent = Penumbra.CollectionManager.Default == Penumbra.CollectionManager.Current;
var isEmpty = Penumbra.CollectionManager.Default == ModCollection.Empty;
var tt = isCurrent ? "The current collection is already the configured default collection."
: isEmpty ? "The default collection is configured to be empty."
: "Set the current collection to the configured default collection.";
var tt = isCurrent ? $"The current collection is already the configured {DefaultCollection}."
: isEmpty ? $"The {DefaultCollection} is configured to be empty."
: $"Set the {SelectedCollection} to the configured {DefaultCollection}.";
if( ImGuiUtil.DrawDisabledButton( name, width, tt, isCurrent || isEmpty ) )
{
Penumbra.CollectionManager.SetCollection( Penumbra.CollectionManager.Default, CollectionType.Current );

View file

@ -59,27 +59,27 @@ public partial class ConfigWindow
Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = !v;
} );
ImGui.Dummy( _window._defaultSpace );
Checkbox( "Use Special Collections in Character Window",
"Use the character collection for your character's name in your main character window, if it is set.",
Checkbox( $"Use {AssignedCollections} in Character Window",
"Use the character collection for your characters name or the Your Character collection in your main character window, if it is set.",
Penumbra.Config.UseCharacterCollectionInMainWindow, v => Penumbra.Config.UseCharacterCollectionInMainWindow = v );
Checkbox( "Use Special Collections in Adventurer Cards",
Checkbox( $"Use {AssignedCollections} in Adventurer Cards",
"Use the appropriate character collection for the adventurer card you are currently looking at, based on the adventurer's name.",
Penumbra.Config.UseCharacterCollectionsInCards, v => Penumbra.Config.UseCharacterCollectionsInCards = v );
Checkbox( "Use Special Collections in Try-On Window",
Checkbox( $"Use {AssignedCollections} in Try-On Window",
"Use the character collection for your character's name in your try-on, dye preview or glamour plate window, if it is set.",
Penumbra.Config.UseCharacterCollectionInTryOn, v => Penumbra.Config.UseCharacterCollectionInTryOn = v );
Checkbox( "Use Special Collections in Inspect Windows",
Checkbox( $"Use {AssignedCollections} in Inspect Windows",
"Use the appropriate character collection for the character you are currently inspecting, based on their name.",
Penumbra.Config.UseCharacterCollectionInInspect, v => Penumbra.Config.UseCharacterCollectionInInspect = v );
Checkbox( "Use Special Collections based on Ownership",
Checkbox( $"Use {AssignedCollections} based on Ownership",
"Use the owner's name to determine the appropriate character collection for mounts, companions and combat pets.",
Penumbra.Config.UseOwnerNameForCharacterCollection, v => Penumbra.Config.UseOwnerNameForCharacterCollection = v );
Checkbox( "Prefer Named Collections over Ownership",
"If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owner's collection.\n"
"If you have a character collection set to a specific name for a companion or combat pet, prefer this collection over the owners collection.\n"
+ "That is, if you have a 'Topaz Carbuncle' collection, it will use this one instead of the one for its owner.",
Penumbra.Config.PreferNamedCollectionsOverOwners, v => Penumbra.Config.PreferNamedCollectionsOverOwners = v );
Checkbox( "Use Default Collection for Housing Retainers",
"Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the default collection.\n"
Checkbox( $"Use {DefaultCollection} for Housing Retainers",
$"Housing Retainers use the name of their owner instead of their own, you can decide to let them use their owners character collection or the {DefaultCollection}.\n"
+ "It is not possible to make them have their own collection, since they have no connection to their actual name.",
Penumbra.Config.UseDefaultCollectionForRetainers, v => Penumbra.Config.UseDefaultCollectionForRetainers = v );
ImGui.Dummy( _window._defaultSpace );

View file

@ -11,6 +11,7 @@ public partial class ConfigWindow
public const string SelectedCollection = "Selected Collection";
public const string DefaultCollection = "Base Collection";
public const string ActiveCollections = "Active Collections";
public const string AssignedCollections = "Assigned Collections";
public const string GroupAssignment = "Group Assignment";
public const string CharacterGroups = "Character Groups";
public const string ConditionalGroup = "Group";

View file

@ -8,7 +8,7 @@
"TestingAssemblyVersion": "1.0.0.0",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 6,
"DalamudApiLevel": 7,
"IsHide": "False",
"IsTestingExclusive": "False",
"DownloadCount": 0,

View file

@ -8,7 +8,7 @@
"TestingAssemblyVersion": "0.5.5.0",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 6,
"DalamudApiLevel": 7,
"IsHide": "False",
"IsTestingExclusive": "False",
"DownloadCount": 0,