Change most things to new byte strings, introduce new ResourceLoader and Logger fully.

This commit is contained in:
Ottermandias 2022-03-06 16:45:16 +01:00
parent 5d77cd5514
commit f5fccb0235
55 changed files with 2681 additions and 2730 deletions

View file

@ -1,17 +1,21 @@
using System; using System;
using System.IO; using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
namespace Penumbra.GameData.ByteString; namespace Penumbra.GameData.ByteString;
[JsonConverter( typeof( FullPathConverter ) )]
public readonly struct FullPath : IComparable, IEquatable< FullPath > public readonly struct FullPath : IComparable, IEquatable< FullPath >
{ {
public readonly string FullName; public readonly string FullName;
public readonly Utf8String InternalName; public readonly Utf8String InternalName;
public readonly ulong Crc64; public readonly ulong Crc64;
public static readonly FullPath Empty = new(string.Empty);
public FullPath( DirectoryInfo baseDir, NewRelPath relPath ) public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath )
: this( Path.Combine( baseDir.FullName, relPath.ToString() ) ) : this( Path.Combine( baseDir.FullName, relPath.ToString() ) )
{ } { }
@ -19,10 +23,11 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
: this( file.FullName ) : this( file.FullName )
{ } { }
public FullPath( string s ) public FullPath( string s )
{ {
FullName = s; FullName = s;
InternalName = Utf8String.FromString( FullName, out var name, true ) ? name : Utf8String.Empty; InternalName = Utf8String.FromString( FullName, out var name, true ) ? name.Replace( ( byte )'\\', ( byte )'/' ) : Utf8String.Empty;
Crc64 = Functions.ComputeCrc64( InternalName.Span ); Crc64 = Functions.ComputeCrc64( InternalName.Span );
} }
@ -35,9 +40,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
public string Name public string Name
=> Path.GetFileName( FullName ); => Path.GetFileName( FullName );
public bool ToGamePath( DirectoryInfo dir, out NewGamePath path ) public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path )
{ {
path = NewGamePath.Empty; path = Utf8GamePath.Empty;
if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) ) if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) )
{ {
return false; return false;
@ -45,13 +50,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
var substring = InternalName.Substring( dir.FullName.Length + 1 ); var substring = InternalName.Substring( dir.FullName.Length + 1 );
path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) ); path = new Utf8GamePath( substring );
return true; return true;
} }
public bool ToRelPath( DirectoryInfo dir, out NewRelPath path ) public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path )
{ {
path = NewRelPath.Empty; path = Utf8RelPath.Empty;
if( !FullName.StartsWith( dir.FullName ) ) if( !FullName.StartsWith( dir.FullName ) )
{ {
return false; return false;
@ -59,7 +64,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
var substring = InternalName.Substring( dir.FullName.Length + 1 ); var substring = InternalName.Substring( dir.FullName.Length + 1 );
path = new NewRelPath( substring ); path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) );
return true; return true;
} }
@ -88,9 +93,35 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
return InternalName.Equals( other.InternalName ); return InternalName.Equals( other.InternalName );
} }
public bool IsRooted
=> new Utf8GamePath( InternalName ).IsRooted();
public override int GetHashCode() public override int GetHashCode()
=> InternalName.Crc32; => InternalName.Crc32;
public override string ToString() public override string ToString()
=> FullName; => FullName;
public class FullPathConverter : JsonConverter
{
public override bool CanConvert( Type objectType )
=> objectType == typeof( FullPath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{
var token = JToken.Load( reader ).ToString();
return new FullPath( token );
}
public override bool CanWrite
=> true;
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{
if( value is FullPath p )
{
serializer.Serialize( writer, p.ToString() );
}
}
}
} }

View file

@ -3,20 +3,21 @@ using System.IO;
using Dalamud.Utility; using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.GameData.Util;
namespace Penumbra.GameData.ByteString; namespace Penumbra.GameData.ByteString;
// NewGamePath wrap some additional validity checking around Utf8String, // NewGamePath wrap some additional validity checking around Utf8String,
// provide some filesystem helpers, and conversion to Json. // provide some filesystem helpers, and conversion to Json.
[JsonConverter( typeof( NewGamePathConverter ) )] [JsonConverter( typeof( Utf8GamePathConverter ) )]
public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable
{ {
public const int MaxGamePathLength = 256; public const int MaxGamePathLength = 256;
public readonly Utf8String Path; public readonly Utf8String Path;
public static readonly NewGamePath Empty = new(Utf8String.Empty); public static readonly Utf8GamePath Empty = new(Utf8String.Empty);
internal NewGamePath( Utf8String s ) internal Utf8GamePath( Utf8String s )
=> Path = s; => Path = s;
public int Length public int Length
@ -25,16 +26,16 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
public bool IsEmpty public bool IsEmpty
=> Path.IsEmpty; => Path.IsEmpty;
public NewGamePath ToLower() public Utf8GamePath ToLower()
=> new(Path.AsciiToLower()); => new(Path.AsciiToLower());
public static unsafe bool FromPointer( byte* ptr, out NewGamePath path, bool lower = false ) public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false )
{ {
var utf = new Utf8String( ptr ); var utf = new Utf8String( ptr );
return ReturnChecked( utf, out path, lower ); return ReturnChecked( utf, out path, lower );
} }
public static bool FromSpan( ReadOnlySpan< byte > data, out NewGamePath path, bool lower = false ) public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false )
{ {
var utf = Utf8String.FromSpanUnsafe( data, false, null, null ); var utf = Utf8String.FromSpanUnsafe( data, false, null, null );
return ReturnChecked( utf, out path, lower ); return ReturnChecked( utf, out path, lower );
@ -43,7 +44,7 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
// Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one. // Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one.
// Does not check for initial slashes either, since they are assumed to be by choice. // Does not check for initial slashes either, since they are assumed to be by choice.
// Checks for maxlength, ASCII and lowercase. // Checks for maxlength, ASCII and lowercase.
private static bool ReturnChecked( Utf8String utf, out NewGamePath path, bool lower = false ) private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false )
{ {
path = Empty; path = Empty;
if( !utf.IsAscii || utf.Length > MaxGamePathLength ) if( !utf.IsAscii || utf.Length > MaxGamePathLength )
@ -51,14 +52,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
return false; return false;
} }
path = new NewGamePath( lower ? utf.AsciiToLower() : utf ); path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf );
return true; return true;
} }
public NewGamePath Clone() public Utf8GamePath Clone()
=> new(Path.Clone()); => new(Path.Clone());
public static bool FromString( string? s, out NewGamePath path, bool toLower = false ) public static explicit operator Utf8GamePath( string s )
=> FromString( s, out var p, true ) ? p : Empty;
public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false )
{ {
path = Empty; path = Empty;
if( s.IsNullOrEmpty() ) if( s.IsNullOrEmpty() )
@ -83,11 +87,11 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
return false; return false;
} }
path = new NewGamePath( ascii ); path = new Utf8GamePath( ascii );
return true; return true;
} }
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false ) public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false )
{ {
path = Empty; path = Empty;
if( !file.FullName.StartsWith( baseDir.FullName ) ) if( !file.FullName.StartsWith( baseDir.FullName ) )
@ -111,13 +115,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
return idx == -1 ? Utf8String.Empty : Path.Substring( idx ); return idx == -1 ? Utf8String.Empty : Path.Substring( idx );
} }
public bool Equals( NewGamePath other ) public bool Equals( Utf8GamePath other )
=> Path.Equals( other.Path ); => Path.Equals( other.Path );
public override int GetHashCode() public override int GetHashCode()
=> Path.GetHashCode(); => Path.GetHashCode();
public int CompareTo( NewGamePath other ) public int CompareTo( Utf8GamePath other )
=> Path.CompareTo( other.Path ); => Path.CompareTo( other.Path );
public override string ToString() public override string ToString()
@ -132,17 +136,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
&& ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' ) && ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' )
&& Path[ 1 ] == ':'; && Path[ 1 ] == ':';
private class NewGamePathConverter : JsonConverter public class Utf8GamePathConverter : JsonConverter
{ {
public override bool CanConvert( Type objectType ) public override bool CanConvert( Type objectType )
=> objectType == typeof( NewGamePath ); => objectType == typeof( Utf8GamePath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{ {
var token = JToken.Load( reader ).ToString(); var token = JToken.Load( reader ).ToString();
return FromString( token, out var p, true ) return FromString( token, out var p, true )
? p ? p
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewGamePath )}." ); : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." );
} }
public override bool CanWrite public override bool CanWrite
@ -150,10 +154,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{ {
if( value is NewGamePath p ) if( value is Utf8GamePath p )
{ {
serializer.Serialize( writer, p.ToString() ); serializer.Serialize( writer, p.ToString() );
} }
} }
} }
public GamePath ToGamePath()
=> GamePath.GenerateUnchecked( ToString() );
} }

View file

@ -1,23 +1,28 @@
using System; using System;
using System.IO; using System.IO;
using Dalamud.Utility; using Dalamud.Utility;
using Microsoft.VisualBasic.CompilerServices;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Penumbra.GameData.ByteString; namespace Penumbra.GameData.ByteString;
[JsonConverter( typeof( NewRelPathConverter ) )] [JsonConverter( typeof( Utf8RelPathConverter ) )]
public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable
{ {
public const int MaxRelPathLength = 250; public const int MaxRelPathLength = 250;
public readonly Utf8String Path; public readonly Utf8String Path;
public static readonly NewRelPath Empty = new(Utf8String.Empty); public static readonly Utf8RelPath Empty = new(Utf8String.Empty);
internal NewRelPath( Utf8String path ) internal Utf8RelPath( Utf8String path )
=> Path = path; => Path = path;
public static bool FromString( string? s, out NewRelPath path )
public static explicit operator Utf8RelPath( string s )
=> FromString( s, out var p ) ? p : Empty;
public static bool FromString( string? s, out Utf8RelPath path )
{ {
path = Empty; path = Empty;
if( s.IsNullOrEmpty() ) if( s.IsNullOrEmpty() )
@ -42,11 +47,11 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
return false; return false;
} }
path = new NewRelPath( ascii ); path = new Utf8RelPath( ascii );
return true; return true;
} }
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewRelPath path ) public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path )
{ {
path = Empty; path = Empty;
if( !file.FullName.StartsWith( baseDir.FullName ) ) if( !file.FullName.StartsWith( baseDir.FullName ) )
@ -58,7 +63,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
return FromString( substring, out path ); return FromString( substring, out path );
} }
public static bool FromFile( FullPath file, DirectoryInfo baseDir, out NewRelPath path ) public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path )
{ {
path = Empty; path = Empty;
if( !file.FullName.StartsWith( baseDir.FullName ) ) if( !file.FullName.StartsWith( baseDir.FullName ) )
@ -70,10 +75,10 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
return FromString( substring, out path ); return FromString( substring, out path );
} }
public NewRelPath( NewGamePath gamePath ) public Utf8RelPath( Utf8GamePath gamePath )
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' ); => Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
public unsafe NewGamePath ToGamePath( int skipFolders = 0 ) public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 )
{ {
var idx = 0; var idx = 0;
while( skipFolders > 0 ) while( skipFolders > 0 )
@ -82,7 +87,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
--skipFolders; --skipFolders;
if( idx <= 0 ) if( idx <= 0 )
{ {
return NewGamePath.Empty; return Utf8GamePath.Empty;
} }
} }
@ -91,13 +96,13 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' ); ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' );
ByteStringFunctions.AsciiToLowerInPlace( ptr, length ); ByteStringFunctions.AsciiToLowerInPlace( ptr, length );
var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true ); var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true );
return new NewGamePath( utf ); return new Utf8GamePath( utf );
} }
public int CompareTo( NewRelPath rhs ) public int CompareTo( Utf8RelPath rhs )
=> Path.CompareTo( rhs.Path ); => Path.CompareTo( rhs.Path );
public bool Equals( NewRelPath other ) public bool Equals( Utf8RelPath other )
=> Path.Equals( other.Path ); => Path.Equals( other.Path );
public override string ToString() public override string ToString()
@ -106,17 +111,17 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
public void Dispose() public void Dispose()
=> Path.Dispose(); => Path.Dispose();
private class NewRelPathConverter : JsonConverter public class Utf8RelPathConverter : JsonConverter
{ {
public override bool CanConvert( Type objectType ) public override bool CanConvert( Type objectType )
=> objectType == typeof( NewRelPath ); => objectType == typeof( Utf8RelPath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{ {
var token = JToken.Load( reader ).ToString(); var token = JToken.Load( reader ).ToString();
return FromString( token, out var p ) return FromString( token, out var p )
? p ? p
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewRelPath )}." ); : throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." );
} }
public override bool CanWrite public override bool CanWrite
@ -124,7 +129,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{ {
if( value is NewRelPath p ) if( value is Utf8RelPath p )
{ {
serializer.Serialize( writer, p.ToString() ); serializer.Serialize( writer, p.ToString() );
} }

View file

@ -75,6 +75,19 @@ public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, ICompa
return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length ); return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length );
} }
public bool StartsWith( Utf8String other )
{
var otherLength = other.Length;
return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength );
}
public bool EndsWith( Utf8String other )
{
var otherLength = other.Length;
var offset = Length - otherLength;
return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength );
}
public bool StartsWith( params char[] chars ) public bool StartsWith( params char[] chars )
{ {
if( chars.Length > Length ) if( chars.Length > Length )

View file

@ -36,7 +36,7 @@ public class ModsController : WebApiController
public object GetFiles() public object GetFiles()
{ {
return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
o => ( string )o.Key, o => o.Key.ToString(),
o => o.Value.FullName o => o.Value.FullName
) )
?? new Dictionary< string, string >(); ?? new Dictionary< string, string >();

View file

@ -5,6 +5,7 @@ using System.Reflection;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Lumina.Data; using Lumina.Data;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Mods; using Penumbra.Mods;
@ -78,16 +79,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private static string ResolvePath( string path, ModManager manager, ModCollection collection ) private static string ResolvePath( string path, ModManager manager, ModCollection collection )
{ {
if( !Penumbra.Config.IsEnabled ) if( !Penumbra.Config.EnableMods )
{ {
return path; return path;
} }
var gamePath = new GamePath( path ); var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty;
var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath ); ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
ret ??= path; return ret?.ToString() ?? path;
return ret;
} }
public string ResolvePath( string path ) public string ResolvePath( string path )

View file

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Logging; using Dalamud.Logging;
@ -14,13 +12,19 @@ public class Configuration : IPluginConfiguration
public int Version { get; set; } = CurrentVersion; public int Version { get; set; } = CurrentVersion;
public bool IsEnabled { get; set; } = true; public bool EnableMods { get; set; } = true;
#if DEBUG #if DEBUG
public bool DebugMode { get; set; } = true; public bool DebugMode { get; set; } = true;
#else #else
public bool DebugMode { get; set; } = false; public bool DebugMode { get; set; } = false;
#endif #endif
public bool EnableFullResourceLogging { get; set; } = false;
public bool EnableResourceLogging { get; set; } = false;
public string ResourceLoggingFilter { get; set; } = string.Empty;
public bool ScaleModSelector { get; set; } = false; public bool ScaleModSelector { get; set; } = false;
public bool ShowAdvanced { get; set; } public bool ShowAdvanced { get; set; }
public bool DisableFileSystemNotifications { get; set; } public bool DisableFileSystemNotifications { get; set; }

View file

@ -8,14 +8,15 @@ using Dalamud.Game.Gui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.Plugin; using Dalamud.Plugin;
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
namespace Penumbra namespace Penumbra;
public class Dalamud
{ {
public class Dalamud public static void Initialize( DalamudPluginInterface pluginInterface )
{ => pluginInterface.Create< Dalamud >();
public static void Initialize(DalamudPluginInterface pluginInterface)
=> pluginInterface.Create<Dalamud>();
// @formatter:off // @formatter:off
[PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
@ -29,6 +30,5 @@ namespace Penumbra
[PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!;
// @formatter:on // @formatter:on
}
} }

View file

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Penumbra.Structs; using Penumbra.Mod;
namespace Penumbra.Importer.Models namespace Penumbra.Importer.Models
{ {

View file

@ -6,10 +6,10 @@ using System.Text;
using Dalamud.Logging; using Dalamud.Logging;
using ICSharpCode.SharpZipLib.Zip; using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Importer.Models; using Penumbra.Importer.Models;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
using FileMode = System.IO.FileMode; using FileMode = System.IO.FileMode;
@ -336,14 +336,18 @@ internal class TexToolsImport
{ {
OptionName = opt.Name!, OptionName = opt.Name!,
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
}; };
var optDir = NewOptionDirectory( groupFolder, opt.Name! ); var optDir = NewOptionDirectory( groupFolder, opt.Name! );
if( optDir.Exists ) if( optDir.Exists )
{ {
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{ {
option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) ); if( Utf8RelPath.FromFile( file, baseFolder, out var rel )
&& Utf8GamePath.FromFile( file, optDir, out var game, true ) )
{
option.AddFile( rel, game );
}
} }
} }

View file

@ -21,7 +21,7 @@ public unsafe partial class ResourceLoader
{ {
public ResourceHandle* OriginalResource; public ResourceHandle* OriginalResource;
public ResourceHandle* ManipulatedResource; public ResourceHandle* ManipulatedResource;
public NewGamePath OriginalPath; public Utf8GamePath OriginalPath;
public FullPath ManipulatedPath; public FullPath ManipulatedPath;
public ResourceCategory Category; public ResourceCategory Category;
public object? ResolverInfo; public object? ResolverInfo;
@ -44,7 +44,7 @@ public unsafe partial class ResourceLoader
ResourceLoaded -= AddModifiedDebugInfo; ResourceLoaded -= AddModifiedDebugInfo;
} }
private void AddModifiedDebugInfo( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, object? resolverInfo ) private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo )
{ {
if( manipulatedPath == null ) if( manipulatedPath == null )
{ {
@ -188,11 +188,11 @@ public unsafe partial class ResourceLoader
} }
} }
// Logging functions for EnableLogging. // Logging functions for EnableFullLogging.
private static void LogPath( NewGamePath path, bool synchronous ) private static void LogPath( Utf8GamePath path, bool synchronous )
=> PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" ); => PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
private static void LogResource( ResourceHandle* handle, NewGamePath path, FullPath? manipulatedPath, object? _ ) private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ )
{ {
var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString(); var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString();
PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" ); PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" );
@ -200,6 +200,6 @@ public unsafe partial class ResourceLoader
private static void LogLoadedFile( Utf8String path, bool success, bool custom ) private static void LogLoadedFile( Utf8String path, bool success, bool custom )
=> PluginLog.Information( success => PluginLog.Information( success
? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}" ? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}"
: $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." ); : $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." );
} }

View file

@ -46,7 +46,7 @@ public unsafe partial class ResourceLoader
[Conditional( "DEBUG" )] [Conditional( "DEBUG" )]
private static void CompareHash( int local, int game, NewGamePath path ) private static void CompareHash( int local, int game, Utf8GamePath path )
{ {
if( local != game ) if( local != game )
{ {
@ -54,12 +54,12 @@ public unsafe partial class ResourceLoader
} }
} }
private event Action< NewGamePath, FullPath?, object? >? PathResolved; private event Action< Utf8GamePath, FullPath?, object? >? PathResolved;
private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType, private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
int* resourceHash, byte* path, void* unk, bool isUnk ) int* resourceHash, byte* path, void* unk, bool isUnk )
{ {
if( !NewGamePath.FromPointer( path, out var gamePath ) ) if( !Utf8GamePath.FromPointer( path, out var gamePath ) )
{ {
PluginLog.Error( "Could not create GamePath from resource path." ); PluginLog.Error( "Could not create GamePath from resource path." );
return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk ); return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
@ -114,7 +114,7 @@ public unsafe partial class ResourceLoader
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync ); return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
} }
var valid = NewGamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false ); var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
byte ret; byte ret;
// The internal buffer size does not allow for more than 260 characters. // The internal buffer size does not allow for more than 260 characters.
// We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack. // We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack.
@ -151,11 +151,10 @@ public unsafe partial class ResourceLoader
} }
// Use the default method of path replacement. // Use the default method of path replacement.
public static (FullPath?, object?) DefaultReplacer( NewGamePath path ) public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path )
{ {
var gamePath = new GamePath( path.ToString() ); var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath ); return( resolved, null );
return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null );
} }
private void DisposeHooks() private void DisposeHooks()

View file

@ -4,8 +4,6 @@ using Dalamud.Hooking;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle; using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Util;
namespace Penumbra.Interop; namespace Penumbra.Interop;
@ -69,7 +67,7 @@ public unsafe partial class ResourceLoader
: LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr ); : LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr );
private void AddCrc( NewGamePath _, FullPath? path, object? _2 ) private void AddCrc( Utf8GamePath _, FullPath? path, object? _2 )
{ {
if( path is { Extension: ".mdl" or ".tex" } p ) if( path is { Extension: ".mdl" or ".tex" } p )
{ {

View file

@ -17,7 +17,7 @@ public unsafe partial class ResourceLoader : IDisposable
// Events can be used to make smarter logging. // Events can be used to make smarter logging.
public bool IsLoggingEnabled { get; private set; } public bool IsLoggingEnabled { get; private set; }
public void EnableLogging() public void EnableFullLogging()
{ {
if( IsLoggingEnabled ) if( IsLoggingEnabled )
{ {
@ -31,7 +31,7 @@ public unsafe partial class ResourceLoader : IDisposable
EnableHooks(); EnableHooks();
} }
public void DisableLogging() public void DisableFullLogging()
{ {
if( !IsLoggingEnabled ) if( !IsLoggingEnabled )
{ {
@ -99,13 +99,13 @@ public unsafe partial class ResourceLoader : IDisposable
} }
// Event fired whenever a resource is requested. // Event fired whenever a resource is requested.
public delegate void ResourceRequestedDelegate( NewGamePath path, bool synchronous ); public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous );
public event ResourceRequestedDelegate? ResourceRequested; public event ResourceRequestedDelegate? ResourceRequested;
// Event fired whenever a resource is returned. // Event fired whenever a resource is returned.
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource. // If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
// resolveData is additional data returned by the current ResolvePath function and is user-defined. // resolveData is additional data returned by the current ResolvePath function and is user-defined.
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
object? resolveData ); object? resolveData );
public event ResourceLoadedDelegate? ResourceLoaded; public event ResourceLoadedDelegate? ResourceLoaded;
@ -118,10 +118,11 @@ public unsafe partial class ResourceLoader : IDisposable
public event FileLoadedDelegate? FileLoaded; public event FileLoadedDelegate? FileLoaded;
// Customization point to control how path resolving is handled. // Customization point to control how path resolving is handled.
public Func< NewGamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer; public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer;
public void Dispose() public void Dispose()
{ {
DisableFullLogging();
DisposeHooks(); DisposeHooks();
DisposeTexMdlTreatment(); DisposeTexMdlTreatment();
} }

View file

@ -0,0 +1,98 @@
using System;
using System.Text.RegularExpressions;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
namespace Penumbra.Interop;
// A logger class that contains the relevant data to log requested files via regex.
// Filters are case-insensitive.
public class ResourceLogger : IDisposable
{
// Enable or disable the logging of resources subject to the current filter.
public void SetState( bool value )
{
if( value == Penumbra.Config.EnableResourceLogging )
{
return;
}
Penumbra.Config.EnableResourceLogging = value;
Penumbra.Config.Save();
if( value )
{
_resourceLoader.ResourceRequested += OnResourceRequested;
}
else
{
_resourceLoader.ResourceRequested -= OnResourceRequested;
}
}
// Set the current filter to a new string, doing all other necessary work.
public void SetFilter( string newFilter )
{
if( newFilter == Filter )
{
return;
}
Penumbra.Config.ResourceLoggingFilter = newFilter;
Penumbra.Config.Save();
SetupRegex();
}
// Returns whether the current filter is a valid regular expression.
public bool ValidRegex
=> _filterRegex != null;
private readonly ResourceLoader _resourceLoader;
private Regex? _filterRegex;
private static string Filter
=> Penumbra.Config.ResourceLoggingFilter;
private void SetupRegex()
{
try
{
_filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant );
}
catch
{
_filterRegex = null;
}
}
public ResourceLogger( ResourceLoader loader )
{
_resourceLoader = loader;
SetupRegex();
if( Penumbra.Config.EnableResourceLogging )
{
_resourceLoader.ResourceRequested += OnResourceRequested;
}
}
private void OnResourceRequested( Utf8GamePath data, bool synchronous )
{
var path = Match( data.Path );
if( path != null )
{
PluginLog.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" );
}
}
// Returns the converted string if the filter matches, and null otherwise.
// The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it.
private string? Match( Utf8String data )
{
var s = data.ToString();
return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.InvariantCultureIgnoreCase ) )
? s
: null;
}
public void Dispose()
=> _resourceLoader.ResourceRequested -= OnResourceRequested;
}

View file

@ -1,5 +1,4 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Penumbra.Structs;
namespace Penumbra.Interop.Structs; namespace Penumbra.Interop.Structs;

View file

@ -8,7 +8,6 @@ using Penumbra.GameData.ByteString;
using Penumbra.Importer; using Penumbra.Importer;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Meta; namespace Penumbra.Meta;
@ -167,14 +166,14 @@ public class MetaCollection
continue; continue;
} }
var path = new RelPath( file, basePath ); Utf8RelPath.FromFile( file, basePath, out var path );
var foundAny = false; var foundAny = false;
foreach( var group in modMeta.Groups ) foreach( var (name, group) in modMeta.Groups )
{ {
foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) foreach( var option in group.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) )
{ {
foundAny = true; foundAny = true;
AddMeta( group.Key, option.OptionName, metaData ); AddMeta( name, option.OptionName, metaData );
} }
} }

View file

@ -6,7 +6,6 @@ using Dalamud.Logging;
using Lumina.Data.Files; using Lumina.Data.Files;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Interop;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Util; using Penumbra.Util;
@ -24,7 +23,7 @@ public class MetaManager : IDisposable
public FileInformation( object data ) public FileInformation( object data )
=> Data = data; => Data = data;
public void Write( DirectoryInfo dir, GamePath originalPath ) public void Write( DirectoryInfo dir, Utf8GamePath originalPath )
{ {
ByteData = Data switch ByteData = Data switch
{ {
@ -44,16 +43,16 @@ public class MetaManager : IDisposable
public const string TmpDirectory = "penumbrametatmp"; public const string TmpDirectory = "penumbrametatmp";
private readonly DirectoryInfo _dir; private readonly DirectoryInfo _dir;
private readonly Dictionary< GamePath, FullPath > _resolvedFiles; private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles;
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); private readonly Dictionary< Utf8GamePath, FileInformation > _currentFiles = new();
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
public IEnumerable< (GamePath, FullPath) > Files public IEnumerable< (Utf8GamePath, FullPath) > Files
=> _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null )
.Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) );
@ -121,7 +120,7 @@ public class MetaManager : IDisposable
private void ClearDirectory() private void ClearDirectory()
=> ClearDirectory( _dir ); => ClearDirectory( _dir );
public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) public MetaManager( string name, Dictionary< Utf8GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir )
{ {
_resolvedFiles = resolvedFiles; _resolvedFiles = resolvedFiles;
_dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) ); _dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) );
@ -135,13 +134,13 @@ public class MetaManager : IDisposable
Directory.CreateDirectory( _dir.FullName ); Directory.CreateDirectory( _dir.FullName );
} }
foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) foreach( var (key, value) in _currentFiles.Where( kvp => kvp.Value.Changed ) )
{ {
kvp.Value.Write( _dir, kvp.Key ); value.Write( _dir, key );
_resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; _resolvedFiles[ key ] = value.CurrentFile!.Value;
if( kvp.Value.Data is EqpFile ) if( value.Data is EqpFile )
{ {
EqpData = kvp.Value.ByteData; EqpData = value.ByteData;
} }
} }
} }
@ -154,7 +153,7 @@ public class MetaManager : IDisposable
} }
_currentManipulations.Add( m, mod ); _currentManipulations.Add( m, mod );
var gamePath = m.CorrespondingFilename(); var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO
try try
{ {
if( !_currentFiles.TryGetValue( gamePath, out var file ) ) if( !_currentFiles.TryGetValue( gamePath, out var file ) )

View file

@ -3,84 +3,81 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Plugin;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra namespace Penumbra;
public static class MigrateConfiguration
{ {
public static class MigrateConfiguration public static void Version0To1( Configuration config )
{ {
public static void Version0To1( Configuration config ) if( config.Version != 0 )
{ {
if( config.Version != 0 ) return;
{
return;
}
config.ModDirectory = config.CurrentCollection;
config.CurrentCollection = "Default";
config.DefaultCollection = "Default";
config.Version = 1;
ResettleCollectionJson( config );
} }
private static void ResettleCollectionJson( Configuration config ) config.ModDirectory = config.CurrentCollection;
config.CurrentCollection = "Default";
config.DefaultCollection = "Default";
config.Version = 1;
ResettleCollectionJson( config );
}
private static void ResettleCollectionJson( Configuration config )
{
var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) );
if( !collectionJson.Exists )
{ {
var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) ); return;
if( !collectionJson.Exists ) }
{
return;
}
var defaultCollection = new ModCollection(); var defaultCollection = new ModCollection();
var defaultCollectionFile = defaultCollection.FileName(); var defaultCollectionFile = defaultCollection.FileName();
if( defaultCollectionFile.Exists ) if( defaultCollectionFile.Exists )
{ {
return; return;
} }
try try
{ {
var text = File.ReadAllText( collectionJson.FullName ); var text = File.ReadAllText( collectionJson.FullName );
var data = JArray.Parse( text ); var data = JArray.Parse( text );
var maxPriority = 0; var maxPriority = 0;
foreach( var setting in data.Cast< JObject >() ) foreach( var setting in data.Cast< JObject >() )
{
var modName = ( string )setting[ "FolderName" ]!;
var enabled = ( bool )setting[ "Enabled" ]!;
var priority = ( int )setting[ "Priority" ]!;
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
var save = new ModSettings()
{ {
var modName = ( string )setting[ "FolderName" ]!; Enabled = enabled,
var enabled = ( bool )setting[ "Enabled" ]!; Priority = priority,
var priority = ( int )setting[ "Priority" ]!; Settings = settings!,
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() };
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); defaultCollection.Settings.Add( modName, save );
maxPriority = Math.Max( maxPriority, priority );
var save = new ModSettings()
{
Enabled = enabled,
Priority = priority,
Settings = settings!,
};
defaultCollection.Settings.Add( modName, save );
maxPriority = Math.Max( maxPriority, priority );
}
if( !config.InvertModListOrder )
{
foreach( var setting in defaultCollection.Settings.Values )
{
setting.Priority = maxPriority - setting.Priority;
}
}
defaultCollection.Save();
} }
catch( Exception e )
if( !config.InvertModListOrder )
{ {
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); foreach( var setting in defaultCollection.Settings.Values )
throw; {
setting.Priority = maxPriority - setting.Priority;
}
} }
defaultCollection.Save();
}
catch( Exception e )
{
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
throw;
} }
} }
} }

View file

@ -0,0 +1,102 @@
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mod;
public enum SelectType
{
Single,
Multi,
}
public struct Option
{
public string OptionName;
public string OptionDesc;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles;
public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath )
{
if( OptionFiles.TryGetValue( filePath, out var set ) )
{
return set.Add( gamePath );
}
OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath };
return true;
}
}
public struct OptionGroup
{
public string GroupName;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType;
public List< Option > Options;
private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
// Selection contains the path, merge all GamePaths for this config.
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
return true;
}
// If the group contains the file in another selection, return true to skip it for default files.
for( var i = 0; i < Options.Count; ++i )
{
if( i == selection )
{
continue;
}
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
return true;
}
}
return false;
}
private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
var doNotAdd = false;
for( var i = 0; i < Options.Count; ++i )
{
if( ( selection & ( 1 << i ) ) != 0 )
{
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
doNotAdd = true;
}
}
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
doNotAdd = true;
}
}
return doNotAdd;
}
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
{
return SelectionType switch
{
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
};
}
}

View file

@ -1,35 +1,33 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Penumbra.GameData.Util; using Penumbra.GameData.ByteString;
using Penumbra.Util;
namespace Penumbra.Mod namespace Penumbra.Mod;
// A complete Mod containing settings (i.e. dependent on a collection)
// and the resulting cache.
public class Mod
{ {
// A complete Mod containing settings (i.e. dependent on a collection) public ModSettings Settings { get; }
// and the resulting cache. public ModData Data { get; }
public class Mod public ModCache Cache { get; }
public Mod( ModSettings settings, ModData data )
{ {
public ModSettings Settings { get; } Settings = settings;
public ModData Data { get; } Data = data;
public ModCache Cache { get; } Cache = new ModCache();
public Mod( ModSettings settings, ModData data )
{
Settings = settings;
Data = data;
Cache = new ModCache();
}
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< GamePath > GetFiles( FileInfo file )
{
var relPath = new RelPath( file, Data.BasePath );
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
}
public override string ToString()
=> Data.Meta.Name;
} }
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< Utf8GamePath > GetFiles( FileInfo file )
{
var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty;
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
}
public override string ToString()
=> Data.Meta.Name;
} }

View file

@ -1,58 +1,57 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.GameData.Util; using Penumbra.GameData.ByteString;
using Penumbra.Meta; using Penumbra.Meta;
namespace Penumbra.Mod namespace Penumbra.Mod;
// The ModCache contains volatile information dependent on all current settings in a collection.
public class ModCache
{ {
// The ModCache contains volatile information dependent on all current settings in a collection. public Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
public class ModCache
public void AddConflict( Mod precedingMod, Utf8GamePath gamePath )
{ {
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new(); if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
public void AddConflict( Mod precedingMod, GamePath gamePath )
{ {
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) ) conflicts.Files.Add( gamePath );
{
conflicts.Files.Add( gamePath );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
}
} }
else
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
{ {
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) ) Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() );
{
conflicts.Manipulations.Add( manipulation );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
}
}
public void ClearConflicts()
=> Conflicts.Clear();
public void ClearFileConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Files.Clear();
return kvp.Value;
} );
}
public void ClearMetaConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Manipulations.Clear();
return kvp.Value;
} );
} }
} }
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
{
conflicts.Manipulations.Add( manipulation );
}
else
{
Conflicts[ precedingMod ] = ( new List< Utf8GamePath >(), new List< MetaManipulation > { manipulation } );
}
}
public void ClearConflicts()
=> Conflicts.Clear();
public void ClearFileConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Files.Clear();
return kvp.Value;
} );
}
public void ClearMetaConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Manipulations.Clear();
return kvp.Value;
} );
}
} }

View file

@ -2,526 +2,530 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Importer; using Penumbra.Importer;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mod namespace Penumbra.Mod;
public class ModCleanup
{ {
public class ModCleanup private const string Duplicates = "Duplicates";
private const string Required = "Required";
private readonly DirectoryInfo _baseDir;
private readonly ModMeta _mod;
private SHA256? _hasher;
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
private SHA256 Sha()
{ {
private const string Duplicates = "Duplicates"; _hasher ??= SHA256.Create();
private const string Required = "Required"; return _hasher;
}
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
{
_baseDir = baseDir;
_mod = mod;
BuildDict();
}
private readonly DirectoryInfo _baseDir; private void BuildDict()
private readonly ModMeta _mod; {
private SHA256? _hasher; foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
private SHA256 Sha()
{ {
_hasher ??= SHA256.Create(); var fileLength = file.Length;
return _hasher; if( _filesBySize.TryGetValue( fileLength, out var files ) )
}
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
{
_baseDir = baseDir;
_mod = mod;
BuildDict();
}
private void BuildDict()
{
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{ {
var fileLength = file.Length; files.Add( file );
if( _filesBySize.TryGetValue( fileLength, out var files ) ) }
{ else
files.Add( file ); {
} _filesBySize[ fileLength ] = new List< FileInfo > { file };
else
{
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
}
} }
} }
}
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option ) private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
{ {
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName ); return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
return newDir; }
}
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder ) private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
{ {
Penumbra.ModManager.AddMod( newDir ); Penumbra.ModManager.AddMod( newDir );
var newMod = Penumbra.ModManager.Mods[ newDir.Name ]; var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
newMod.Move( newSortOrder ); newMod.Move( newSortOrder );
newMod.ComputeChangedItems(); newMod.ComputeChangedItems();
ModFileSystem.InvokeChange(); ModFileSystem.InvokeChange();
return newMod; return newMod;
} }
private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option ) private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option )
{
var newMeta = new ModMeta
{ {
var newMeta = new ModMeta Author = mod.Meta.Author,
Name = name,
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.",
};
var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
newMeta.SaveToFile( metaFile );
return newMeta;
}
private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option )
{
try
{
var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName );
var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName;
var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName );
foreach( var (fileName, paths) in option.OptionFiles )
{ {
Author = mod.Meta.Author, var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
Name = name, unseenPaths.Remove( oldPath );
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", if( File.Exists( oldPath ) )
};
var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
newMeta.SaveToFile( metaFile );
return newMeta;
}
private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option )
{
try
{
var newDir = CreateNewModDir( mod, group.GroupName!, option.OptionName );
var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName;
var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName!, option.OptionName );
foreach( var (fileName, paths) in option.OptionFiles )
{ {
var oldPath = Path.Combine( mod.BasePath.FullName, fileName ); foreach( var path in paths )
unseenPaths.Remove( oldPath );
if( File.Exists( oldPath ) )
{ {
foreach( var path in paths ) var newPath = Path.Combine( newDir.FullName, path.ToString() );
{ Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
var newPath = Path.Combine( newDir.FullName, path ); File.Copy( oldPath, newPath, true );
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
File.Copy( oldPath, newPath, true );
}
}
}
var newSortOrder = group.SelectionType == SelectType.Single
? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}"
: $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}";
CreateNewMod( newDir, newSortOrder );
}
catch( Exception e )
{
PluginLog.Error( $"Could not split Mod:\n{e}" );
}
}
public static void SplitMod( ModData mod )
{
if( !mod.Meta.Groups.Any() )
{
return;
}
var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet();
foreach( var group in mod.Meta.Groups.Values )
{
foreach( var option in group.Options )
{
CreateModSplit( unseenPaths, mod, group, option );
}
}
if( !unseenPaths.Any() )
{
return;
}
var defaultGroup = new OptionGroup()
{
GroupName = "Default",
SelectionType = SelectType.Multi,
};
var defaultOption = new Option()
{
OptionName = "Files",
OptionFiles = unseenPaths.ToDictionary( p => new RelPath( new FileInfo( p ), mod.BasePath ),
p => new HashSet< GamePath >() { new( new FileInfo( p ), mod.BasePath ) } ),
};
CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption );
}
private static Option FindOrCreateDuplicates( ModMeta meta )
{
static Option RequiredOption()
=> new()
{
OptionName = Required,
OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
};
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
{
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
if( idx >= 0 )
{
return duplicates.Options[ idx ];
}
duplicates.Options.Add( RequiredOption() );
return duplicates.Options.Last();
}
meta.Groups.Add( Duplicates, new OptionGroup
{
GroupName = Duplicates,
SelectionType = SelectType.Single,
Options = new List< Option > { RequiredOption() },
} );
return meta.Groups[ Duplicates ].Options.First();
}
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
{
var dedup = new ModCleanup( baseDir, mod );
foreach( var pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
{
if( pair.Value.Count == 2 )
{
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
{
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
}
}
else
{
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray();
for( var i = 0; i < pair.Value.Count; ++i )
{
if( deleted[ i ] )
{
continue;
}
for( var j = i + 1; j < pair.Value.Count; ++j )
{
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
{
continue;
}
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
deleted[ j ] = true;
}
} }
} }
} }
CleanUpDuplicates( mod ); var newSortOrder = group.SelectionType == SelectType.Single
ClearEmptySubDirectories( dedup._baseDir ); ? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}"
: $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}";
CreateNewMod( newDir, newSortOrder );
}
catch( Exception e )
{
PluginLog.Error( $"Could not split Mod:\n{e}" );
}
}
public static void SplitMod( ModData mod )
{
if( mod.Meta.Groups.Count == 0 )
{
return;
} }
private void ReplaceFile( FileInfo f1, FileInfo f2 ) var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet();
foreach( var group in mod.Meta.Groups.Values )
{ {
RelPath relName1 = new( f1, _baseDir ); foreach( var option in group.Options )
RelPath relName2 = new( f2, _baseDir );
var inOption1 = false;
var inOption2 = false;
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
{ {
if( option.OptionFiles.ContainsKey( relName1 ) ) CreateModSplit( unseenPaths, mod, group, option );
{
inOption1 = true;
}
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
{
continue;
}
inOption2 = true;
foreach( var value in values )
{
option.AddFile( relName1, value );
}
option.OptionFiles.Remove( relName2 );
}
if( !inOption1 || !inOption2 )
{
var duplicates = FindOrCreateDuplicates( _mod );
if( !inOption1 )
{
duplicates.AddFile( relName1, relName2.ToGamePath() );
}
if( !inOption2 )
{
duplicates.AddFile( relName1, relName1.ToGamePath() );
}
}
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
f2.Delete();
}
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
public static bool CompareHashes( byte[] f1, byte[] f2 )
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
public byte[] ComputeHash( FileInfo f )
{
var stream = File.OpenRead( f.FullName );
var ret = Sha().ComputeHash( stream );
stream.Dispose();
return ret;
}
// Does not delete the base directory itself even if it is completely empty at the end.
public static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{
foreach( var subDir in baseDir.GetDirectories() )
{
ClearEmptySubDirectories( subDir );
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
{
subDir.Delete();
}
} }
} }
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false ) if( unseenPaths.Count == 0 )
{ {
var groupEnumerator = exceptDuplicates return;
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
: meta.Groups.Values;
return groupEnumerator.SelectMany( group => group.Options )
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
} }
private static void CleanUpDuplicates( ModMeta meta ) var defaultGroup = new OptionGroup()
{ {
if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) GroupName = "Default",
SelectionType = SelectType.Multi,
};
var defaultOption = new Option()
{
OptionName = "Files",
OptionFiles = unseenPaths.ToDictionary(
p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty,
p => new HashSet< Utf8GamePath >()
{ Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ),
};
CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption );
}
private static Option FindOrCreateDuplicates( ModMeta meta )
{
static Option RequiredOption()
=> new()
{ {
return; OptionName = Required,
OptionDesc = "",
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
};
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
{
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
if( idx >= 0 )
{
return duplicates.Options[ idx ];
} }
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); duplicates.Options.Add( RequiredOption() );
if( requiredIdx >= 0 ) return duplicates.Options.Last();
}
meta.Groups.Add( Duplicates, new OptionGroup
{
GroupName = Duplicates,
SelectionType = SelectType.Single,
Options = new List< Option > { RequiredOption() },
} );
return meta.Groups[ Duplicates ].Options.First();
}
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
{
var dedup = new ModCleanup( baseDir, mod );
foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
{
if( value.Count == 2 )
{ {
var required = info.Options[ requiredIdx ]; if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
foreach( var kvp in required.OptionFiles.ToArray() )
{ {
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) ) dedup.ReplaceFile( value[ 0 ], value[ 1 ] );
}
}
else
{
var deleted = Enumerable.Repeat( false, value.Count ).ToArray();
var hashes = value.Select( dedup.ComputeHash ).ToArray();
for( var i = 0; i < value.Count; ++i )
{
if( deleted[ i ] )
{ {
continue; continue;
} }
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( kvp.Key.ToGamePath() ) == 0 ) for( var j = i + 1; j < value.Count; ++j )
{ {
required.OptionFiles.Remove( kvp.Key ); if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
{
continue;
}
dedup.ReplaceFile( value[ i ], value[ j ] );
deleted[ j ] = true;
} }
} }
if( required.OptionFiles.Count == 0 )
{
info.Options.RemoveAt( requiredIdx );
}
}
if( info.Options.Count == 0 )
{
meta.Groups.Remove( Duplicates );
} }
} }
public enum GroupType CleanUpDuplicates( mod );
ClearEmptySubDirectories( dedup._baseDir );
}
private void ReplaceFile( FileInfo f1, FileInfo f2 )
{
if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 )
|| !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) )
{ {
Both = 0, return;
Single = 1, }
Multi = 2,
var inOption1 = false;
var inOption2 = false;
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
{
if( option.OptionFiles.ContainsKey( relName1 ) )
{
inOption1 = true;
}
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
{
continue;
}
inOption2 = true;
foreach( var value in values )
{
option.AddFile( relName1, value );
}
option.OptionFiles.Remove( relName2 );
}
if( !inOption1 || !inOption2 )
{
var duplicates = FindOrCreateDuplicates( _mod );
if( !inOption1 )
{
duplicates.AddFile( relName1, relName2.ToGamePath() );
}
if( !inOption2 )
{
duplicates.AddFile( relName1, relName1.ToGamePath() );
}
}
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
f2.Delete();
}
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
public static bool CompareHashes( byte[] f1, byte[] f2 )
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
public byte[] ComputeHash( FileInfo f )
{
var stream = File.OpenRead( f.FullName );
var ret = Sha().ComputeHash( stream );
stream.Dispose();
return ret;
}
// Does not delete the base directory itself even if it is completely empty at the end.
public static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{
foreach( var subDir in baseDir.GetDirectories() )
{
ClearEmptySubDirectories( subDir );
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
{
subDir.Delete();
}
}
}
private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false )
{
var groupEnumerator = exceptDuplicates
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
: meta.Groups.Values;
return groupEnumerator.SelectMany( group => group.Options )
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
}
private static void CleanUpDuplicates( ModMeta meta )
{
if( !meta.Groups.TryGetValue( Duplicates, out var info ) )
{
return;
}
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required );
if( requiredIdx >= 0 )
{
var required = info.Options[ requiredIdx ];
foreach( var (key, value) in required.OptionFiles.ToArray() )
{
if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) )
{
continue;
}
if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 )
{
required.OptionFiles.Remove( key );
}
}
if( required.OptionFiles.Count == 0 )
{
info.Options.RemoveAt( requiredIdx );
}
}
if( info.Options.Count == 0 )
{
meta.Groups.Remove( Duplicates );
}
}
public enum GroupType
{
Both = 0,
Single = 1,
Multi = 2,
};
private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both,
bool skipDuplicates = true )
{
if( meta.Groups.Count == 0 )
{
return;
}
var enumerator = type switch
{
GroupType.Both => meta.Groups.Values,
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
}; };
foreach( var group in enumerator )
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
bool skipDuplicates = true )
{ {
if( meta.Groups.Count == 0 ) var optionEnum = skipDuplicates
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
: group.Options;
foreach( var option in optionEnum )
{ {
return; if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
}
var enumerator = type switch
{
GroupType.Both => meta.Groups.Values,
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
};
foreach( var group in enumerator )
{
var optionEnum = skipDuplicates
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
: group.Options;
foreach( var option in optionEnum )
{ {
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) option.OptionFiles.Remove( relPath );
{
option.OptionFiles.Remove( relPath );
}
} }
} }
} }
}
public static bool MoveFile( ModMeta meta, string basePath, RelPath oldRelPath, RelPath newRelPath ) public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath )
{
if( oldRelPath.Equals( newRelPath ) )
{ {
if( oldRelPath == newRelPath )
{
return true;
}
try
{
var newFullPath = Path.Combine( basePath, newRelPath );
new FileInfo( newFullPath ).Directory!.Create();
File.Move( Path.Combine( basePath, oldRelPath ), newFullPath );
}
catch( Exception e )
{
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
return false;
}
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
{
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
{
option.OptionFiles.Add( newRelPath, gamePaths );
option.OptionFiles.Remove( oldRelPath );
}
}
return true; return true;
} }
try
private static void RemoveUselessGroups( ModMeta meta )
{ {
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); new FileInfo( newFullPath ).Directory!.Create();
File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath );
}
catch( Exception e )
{
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
return false;
} }
// Goes through all Single-Select options and checks if file links are in each of them. foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
{ {
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
{ {
var firstOption = true; option.OptionFiles.Add( newRelPath, gamePaths );
HashSet< (RelPath, GamePath) > groupList = new(); option.OptionFiles.Remove( oldRelPath );
foreach( var option in group.Options ) }
}
return true;
}
private static void RemoveUselessGroups( ModMeta meta )
{
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
}
// Goes through all Single-Select options and checks if file links are in each of them.
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
{
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
{
var firstOption = true;
HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new();
foreach( var option in group.Options )
{
HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new();
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
{ {
HashSet< (RelPath, GamePath) > optionList = new(); optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
{
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
}
if( firstOption )
{
groupList = optionList;
}
else
{
groupList.IntersectWith( optionList );
}
firstOption = false;
} }
var newPath = new Dictionary< RelPath, GamePath >(); if( firstOption )
foreach( var (path, gamePath) in groupList )
{ {
var relPath = new RelPath( gamePath ); groupList = optionList;
if( newPath.TryGetValue( path, out var usedGamePath ) )
{
var required = FindOrCreateDuplicates( meta );
var usedRelPath = new RelPath( usedGamePath );
required.AddFile( usedRelPath, gamePath );
required.AddFile( usedRelPath, usedGamePath );
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
{
newPath[ path ] = gamePath;
if( FileIsInAnyGroup( meta, relPath ) )
{
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
}
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
} }
else
{
groupList.IntersectWith( optionList );
}
firstOption = false;
} }
RemoveUselessGroups( meta ); var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
ClearEmptySubDirectories( baseDir ); foreach( var (path, gamePath) in groupList )
{
var relPath = new Utf8RelPath( gamePath );
if( newPath.TryGetValue( path, out var usedGamePath ) )
{
var required = FindOrCreateDuplicates( meta );
var usedRelPath = new Utf8RelPath( usedGamePath );
required.AddFile( usedRelPath, gamePath );
required.AddFile( usedRelPath, usedGamePath );
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
{
newPath[ path ] = gamePath;
if( FileIsInAnyGroup( meta, relPath ) )
{
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
}
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
}
} }
public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) RemoveUselessGroups( meta );
ClearEmptySubDirectories( baseDir );
}
public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta )
{
meta.Groups.Clear();
ClearEmptySubDirectories( baseDir );
foreach( var groupDir in baseDir.EnumerateDirectories() )
{ {
meta.Groups.Clear(); var group = new OptionGroup
ClearEmptySubDirectories( baseDir );
foreach( var groupDir in baseDir.EnumerateDirectories() )
{ {
var group = new OptionGroup GroupName = groupDir.Name,
SelectionType = SelectType.Single,
Options = new List< Option >(),
};
foreach( var optionDir in groupDir.EnumerateDirectories() )
{
var option = new Option
{ {
GroupName = groupDir.Name, OptionDesc = string.Empty,
SelectionType = SelectType.Single, OptionName = optionDir.Name,
Options = new List< Option >(), OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
}; };
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
foreach( var optionDir in groupDir.EnumerateDirectories() )
{ {
var option = new Option if( Utf8RelPath.FromFile( file, baseDir, out var rel )
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
{ {
OptionDesc = string.Empty, option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
OptionName = optionDir.Name,
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
};
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
var relPath = new RelPath( file, baseDir );
var gamePath = new GamePath( file, optionDir );
option.OptionFiles[ relPath ] = new HashSet< GamePath > { gamePath };
}
if( option.OptionFiles.Any() )
{
group.Options.Add( option );
} }
} }
if( group.Options.Any() ) if( option.OptionFiles.Any() )
{ {
meta.Groups.Add( groupDir.Name, group ); group.Options.Add( option );
} }
} }
foreach(var collection in Penumbra.ModManager.Collections.Collections.Values) if( group.Options.Any() )
collection.UpdateSetting(baseDir, meta, true); {
meta.Groups.Add( groupDir.Name, group );
}
}
foreach( var collection in Penumbra.ModManager.Collections.Collections.Values )
{
collection.UpdateSetting( baseDir, meta, true );
} }
} }
} }

View file

@ -3,134 +3,133 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Mod namespace Penumbra.Mod;
public struct SortOrder : IComparable< SortOrder >
{ {
public struct SortOrder : IComparable< SortOrder > public ModFolder ParentFolder { get; set; }
private string _sortOrderName;
public string SortOrderName
{ {
public ModFolder ParentFolder { get; set; } get => _sortOrderName;
set => _sortOrderName = value.Replace( '/', '\\' );
private string _sortOrderName;
public string SortOrderName
{
get => _sortOrderName;
set => _sortOrderName = value.Replace( '/', '\\' );
}
public string SortOrderPath
=> ParentFolder.FullName;
public string FullName
{
get
{
var path = SortOrderPath;
return path.Any() ? $"{path}/{SortOrderName}" : SortOrderName;
}
}
public SortOrder( ModFolder parentFolder, string name )
{
ParentFolder = parentFolder;
_sortOrderName = name.Replace( '/', '\\' );
}
public string FullPath
=> SortOrderPath.Any() ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
public int CompareTo( SortOrder other )
=> string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase );
} }
// ModData contains all permanent information about a mod, public string SortOrderPath
// and is independent of collections or settings. => ParentFolder.FullName;
// It only changes when the user actively changes the mod or their filesystem.
public class ModData public string FullName
{ {
public DirectoryInfo BasePath; get
public ModMeta Meta;
public ModResources Resources;
public SortOrder SortOrder;
public SortedList< string, object? > ChangedItems { get; } = new();
public string LowerChangedItemsString { get; private set; } = string.Empty;
public FileInfo MetaFile { get; set; }
private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources )
{ {
BasePath = basePath; var path = SortOrderPath;
Meta = meta; return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
SortOrder = new SortOrder( parentFolder, Meta.Name );
SortOrder.ParentFolder.AddMod( this );
ComputeChangedItems();
} }
public void ComputeChangedItems()
{
var identifier = GameData.GameData.GetIdentifier();
ChangedItems.Clear();
foreach( var file in Resources.ModFiles.Select( f => new RelPath( f, BasePath ) ) )
{
foreach( var path in ModFunctions.GetAllFiles( file, Meta ) )
{
identifier.Identify( ChangedItems, path );
}
}
foreach( var path in Meta.FileSwaps.Keys )
{
identifier.Identify( ChangedItems, path );
}
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
}
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
=> new( Path.Combine( basePath.FullName, "meta.json" ) );
public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
return null;
}
var metaFile = MetaFileInfo( basePath );
if( !metaFile.Exists )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
return null;
}
var meta = ModMeta.LoadFromFile( metaFile );
if( meta == null )
{
return null;
}
var data = new ModResources();
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
{
data.SetManipulations( meta, basePath );
}
return new ModData( parentFolder, basePath, meta, data );
}
public void SaveMeta()
=> Meta.SaveToFile( MetaFile );
public override string ToString()
=> SortOrder.FullPath;
} }
public SortOrder( ModFolder parentFolder, string name )
{
ParentFolder = parentFolder;
_sortOrderName = name.Replace( '/', '\\' );
}
public string FullPath
=> SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
public int CompareTo( SortOrder other )
=> string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase );
}
// ModData contains all permanent information about a mod,
// and is independent of collections or settings.
// It only changes when the user actively changes the mod or their filesystem.
public class ModData
{
public DirectoryInfo BasePath;
public ModMeta Meta;
public ModResources Resources;
public SortOrder SortOrder;
public SortedList< string, object? > ChangedItems { get; } = new();
public string LowerChangedItemsString { get; private set; } = string.Empty;
public FileInfo MetaFile { get; set; }
private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources )
{
BasePath = basePath;
Meta = meta;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
SortOrder = new SortOrder( parentFolder, Meta.Name );
SortOrder.ParentFolder.AddMod( this );
ComputeChangedItems();
}
public void ComputeChangedItems()
{
var identifier = GameData.GameData.GetIdentifier();
ChangedItems.Clear();
foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) )
{
foreach( var path in ModFunctions.GetAllFiles( file, Meta ) )
{
identifier.Identify( ChangedItems, path.ToGamePath() );
}
}
foreach( var path in Meta.FileSwaps.Keys )
{
identifier.Identify( ChangedItems, path.ToGamePath() );
}
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
}
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
=> new(Path.Combine( basePath.FullName, "meta.json" ));
public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
return null;
}
var metaFile = MetaFileInfo( basePath );
if( !metaFile.Exists )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
return null;
}
var meta = ModMeta.LoadFromFile( metaFile );
if( meta == null )
{
return null;
}
var data = new ModResources();
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
{
data.SetManipulations( meta, basePath );
}
return new ModData( parentFolder, basePath, meta, data );
}
public void SaveMeta()
=> Meta.SaveToFile( MetaFile );
public override string ToString()
=> SortOrder.FullPath;
} }

View file

@ -1,103 +1,100 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.GameData.Util; using Penumbra.GameData.ByteString;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod namespace Penumbra.Mod;
// Functions that do not really depend on only one component of a mod.
public static class ModFunctions
{ {
// Functions that do not really depend on only one component of a mod. public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
public static class ModFunctions
{ {
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) var hashes = modPaths.Select( p => p.Name ).ToHashSet();
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{ {
var hashes = modPaths.Select( p => p.Name ).ToHashSet(); anyChanges |= settings.Remove( toRemove );
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{
anyChanges |= settings.Remove( toRemove );
}
return anyChanges;
} }
public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta ) return anyChanges;
}
public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta )
{
var doNotAdd = false;
var files = new HashSet< Utf8GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{ {
var doNotAdd = false; doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
var files = new HashSet< GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
}
if( !doNotAdd )
{
files.Add( new GamePath( relPath ) );
}
return files;
} }
public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta ) if( !doNotAdd )
{ {
var ret = new HashSet< GamePath >(); files.Add( relPath.ToGamePath() );
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
{
if( option.OptionFiles.TryGetValue( relPath, out var files ) )
{
ret.UnionWith( files );
}
}
if( ret.Count == 0 )
{
ret.Add( relPath.ToGamePath() );
}
return ret;
} }
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) return files;
}
public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta )
{
var ret = new HashSet< Utf8GamePath >();
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
{ {
ModSettings ret = new() if( option.OptionFiles.TryGetValue( relPath, out var files ) )
{ {
Priority = namedSettings.Priority, ret.UnionWith( files );
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), }
}; }
foreach( var kvp in namedSettings.Settings ) if( ret.Count == 0 )
{
ret.Add( relPath.ToGamePath() );
}
return ret;
}
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
{
ModSettings ret = new()
{
Priority = namedSettings.Priority,
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
foreach( var setting in namedSettings.Settings.Keys )
{
if( !meta.Groups.TryGetValue( setting, out var info ) )
{ {
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) continue;
{ }
continue;
}
if( info.SelectionType == SelectType.Single ) if( info.SelectionType == SelectType.Single )
{
if( namedSettings.Settings[ setting ].Count == 0 )
{ {
if( namedSettings.Settings[ kvp.Key ].Count == 0 ) ret.Settings[ setting ] = 0;
{
ret.Settings[ kvp.Key ] = 0;
}
else
{
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
}
} }
else else
{ {
foreach( var idx in namedSettings.Settings[ kvp.Key ] var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() );
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) ret.Settings[ setting ] = idx < 0 ? 0 : idx;
.Where( idx => idx >= 0 ) ) }
{ }
ret.Settings[ kvp.Key ] |= 1 << idx; else
} {
foreach( var idx in namedSettings.Settings[ setting ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ setting ] |= 1 << idx;
} }
} }
return ret;
} }
return ret;
} }
} }

View file

@ -4,134 +4,133 @@ using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Structs;
namespace Penumbra.Mod namespace Penumbra.Mod;
// Contains descriptive data about the mod as well as possible settings and fileswaps.
public class ModMeta
{ {
// Contains descriptive data about the mod as well as possible settings and fileswaps. public uint FileVersion { get; set; }
public class ModMeta
public string Name
{ {
public uint FileVersion { get; set; } get => _name;
set
public string Name
{ {
get => _name; _name = value;
set LowerName = value.ToLowerInvariant();
{
_name = value;
LowerName = value.ToLowerInvariant();
}
} }
}
private string _name = "Mod"; private string _name = "Mod";
[JsonIgnore] [JsonIgnore]
public string LowerName { get; private set; } = "mod"; public string LowerName { get; private set; } = "mod";
private string _author = ""; private string _author = "";
public string Author public string Author
{
get => _author;
set
{ {
get => _author; _author = value;
set LowerAuthor = value.ToLowerInvariant();
{
_author = value;
LowerAuthor = value.ToLowerInvariant();
}
} }
}
[JsonIgnore] [JsonIgnore]
public string LowerAuthor { get; private set; } = ""; public string LowerAuthor { get; private set; } = "";
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public string Version { get; set; } = ""; public string Version { get; set; } = "";
public string Website { get; set; } = ""; public string Website { get; set; } = "";
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )] [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new(); public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new(); public Dictionary< string, OptionGroup > Groups { get; set; } = new();
[JsonIgnore] [JsonIgnore]
private int FileHash { get; set; } private int FileHash { get; set; }
[JsonIgnore] [JsonIgnore]
public bool HasGroupsWithConfig { get; private set; } public bool HasGroupsWithConfig { get; private set; }
public bool RefreshFromFile( FileInfo filePath ) public bool RefreshFromFile( FileInfo filePath )
{
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{ {
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{
return true;
}
if( newMeta.FileHash == FileHash )
{
return false;
}
FileVersion = newMeta.FileVersion;
Name = newMeta.Name;
Author = newMeta.Author;
Description = newMeta.Description;
Version = newMeta.Version;
Website = newMeta.Website;
FileSwaps = newMeta.FileSwaps;
Groups = newMeta.Groups;
FileHash = newMeta.FileHash;
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
return true; return true;
} }
public static ModMeta? LoadFromFile( FileInfo filePath ) if( newMeta.FileHash == FileHash )
{ {
try return false;
{
var text = File.ReadAllText( filePath.FullName );
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.FileHash = text.GetHashCode();
meta.RefreshHasGroupsWithConfig();
}
return meta;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod meta:\n{e}" );
return null;
}
} }
public bool RefreshHasGroupsWithConfig() FileVersion = newMeta.FileVersion;
Name = newMeta.Name;
Author = newMeta.Author;
Description = newMeta.Description;
Version = newMeta.Version;
Website = newMeta.Website;
FileSwaps = newMeta.FileSwaps;
Groups = newMeta.Groups;
FileHash = newMeta.FileHash;
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
return true;
}
public static ModMeta? LoadFromFile( FileInfo filePath )
{
try
{ {
var oldValue = HasGroupsWithConfig; var text = File.ReadAllText( filePath.FullName );
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
return oldValue != HasGroupsWithConfig; var meta = JsonConvert.DeserializeObject< ModMeta >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.FileHash = text.GetHashCode();
meta.RefreshHasGroupsWithConfig();
}
return meta;
} }
catch( Exception e )
public void SaveToFile( FileInfo filePath )
{ {
try PluginLog.Error( $"Could not load mod meta:\n{e}" );
return null;
}
}
public bool RefreshHasGroupsWithConfig()
{
var oldValue = HasGroupsWithConfig;
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
return oldValue != HasGroupsWithConfig;
}
public void SaveToFile( FileInfo filePath )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
var newHash = text.GetHashCode();
if( newHash != FileHash )
{ {
var text = JsonConvert.SerializeObject( this, Formatting.Indented ); File.WriteAllText( filePath.FullName, text );
var newHash = text.GetHashCode(); FileHash = newHash;
if( newHash != FileHash )
{
File.WriteAllText( filePath.FullName, text );
FileHash = newHash;
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
} }
} }
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
} }
} }

View file

@ -1,75 +1,73 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Mod namespace Penumbra.Mod;
// Contains the settings for a given mod.
public class ModSettings
{ {
// Contains the settings for a given mod. public bool Enabled { get; set; }
public class ModSettings public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
// For backwards compatibility
private Dictionary< string, int > Conf
{ {
public bool Enabled { get; set; } set => Settings = value;
public int Priority { get; set; } }
public Dictionary< string, int > Settings { get; set; } = new();
// For backwards compatibility public ModSettings DeepCopy()
private Dictionary< string, int > Conf {
var settings = new ModSettings
{ {
set => Settings = value; Enabled = Enabled,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
};
return settings;
}
public static ModSettings DefaultSettings( ModMeta meta )
{
return new ModSettings
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
};
}
public bool FixSpecificSetting( string name, ModMeta meta )
{
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
} }
public ModSettings DeepCopy() if( Settings.TryGetValue( name, out var oldSetting ) )
{ {
var settings = new ModSettings Settings[ name ] = group.SelectionType switch
{ {
Enabled = Enabled, SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
Priority = Priority, SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), _ => Settings[ group.GroupName ],
}; };
return settings; return oldSetting != Settings[ group.GroupName ];
} }
public static ModSettings DefaultSettings( ModMeta meta ) Settings[ name ] = 0;
return true;
}
public bool FixInvalidSettings( ModMeta meta )
{
if( meta.Groups.Count == 0 )
{ {
return new() return false;
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
};
} }
public bool FixSpecificSetting( string name, ModMeta meta ) return Settings.Keys.ToArray().Union( meta.Groups.Keys )
{ .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
}
if( Settings.TryGetValue( name, out var oldSetting ) )
{
Settings[ name ] = group.SelectionType switch
{
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
_ => Settings[ group.GroupName ],
};
return oldSetting != Settings[ group.GroupName ];
}
Settings[ name ] = 0;
return true;
}
public bool FixInvalidSettings( ModMeta meta )
{
if( meta.Groups.Count == 0 )
{
return false;
}
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
}
} }
} }

View file

@ -1,45 +1,43 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Mod namespace Penumbra.Mod;
// Contains settings with the option selections stored by names instead of index.
// This is meant to make them possibly more portable when we support importing collections from other users.
// Enabled does not exist, because disabled mods would not be exported in this way.
public class NamedModSettings
{ {
// Contains settings with the option selections stored by names instead of index. public int Priority { get; set; }
// This is meant to make them possibly more portable when we support importing collections from other users. public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
// Enabled does not exist, because disabled mods would not be exported in this way.
public class NamedModSettings public void AddFromModSetting( ModSettings s, ModMeta meta )
{ {
public int Priority { get; set; } Priority = s.Priority;
public Dictionary< string, HashSet< string > > Settings { get; set; } = new(); Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
public void AddFromModSetting( ModSettings s, ModMeta meta ) foreach( var kvp in Settings )
{ {
Priority = s.Priority; if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
foreach( var kvp in Settings )
{ {
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) continue;
{ }
continue;
}
var setting = s.Settings[ kvp.Key ]; var setting = s.Settings[ kvp.Key ];
if( info.SelectionType == SelectType.Single ) if( info.SelectionType == SelectType.Single )
{
var name = setting < info.Options.Count
? info.Options[ setting ].OptionName
: info.Options[ 0 ].OptionName;
kvp.Value.Add( name );
}
else
{
for( var i = 0; i < info.Options.Count; ++i )
{ {
var name = setting < info.Options.Count if( ( ( setting >> i ) & 1 ) != 0 )
? info.Options[ setting ].OptionName
: info.Options[ 0 ].OptionName;
kvp.Value.Add( name );
}
else
{
for( var i = 0; i < info.Options.Count; ++i )
{ {
if( ( ( setting >> i ) & 1 ) != 0 ) kvp.Value.Add( info.Options[ i ].OptionName );
{
kvp.Value.Add( info.Options[ i ].OptionName );
}
} }
} }
} }

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Interop;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Util; using Penumbra.Util;
@ -250,16 +249,16 @@ public class CollectionManager
public bool CreateCharacterCollection( string characterName ) public bool CreateCharacterCollection( string characterName )
{ {
if( !CharacterCollection.ContainsKey( characterName ) ) if( CharacterCollection.ContainsKey( characterName ) )
{ {
CharacterCollection[ characterName ] = ModCollection.Empty; return false;
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
Penumbra.Config.Save();
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
return true;
} }
return false; CharacterCollection[ characterName ] = ModCollection.Empty;
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
Penumbra.Config.Save();
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
return true;
} }
public void RemoveCharacterCollection( string characterName ) public void RemoveCharacterCollection( string characterName )
@ -299,7 +298,7 @@ public class CollectionManager
private bool LoadForcedCollection( Configuration config ) private bool LoadForcedCollection( Configuration config )
{ {
if( config.ForcedCollection == string.Empty ) if( config.ForcedCollection.Length == 0 )
{ {
ForcedCollection = ModCollection.Empty; ForcedCollection = ModCollection.Empty;
return false; return false;
@ -320,7 +319,7 @@ public class CollectionManager
private bool LoadDefaultCollection( Configuration config ) private bool LoadDefaultCollection( Configuration config )
{ {
if( config.DefaultCollection == string.Empty ) if( config.DefaultCollection.Length == 0 )
{ {
DefaultCollection = ModCollection.Empty; DefaultCollection = ModCollection.Empty;
return false; return false;
@ -345,7 +344,7 @@ public class CollectionManager
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() ) foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
{ {
Penumbra.PlayerWatcher.AddPlayerToWatch( player ); Penumbra.PlayerWatcher.AddPlayerToWatch( player );
if( collectionName == string.Empty ) if( collectionName.Length == 0 )
{ {
CharacterCollection.Add( player, ModCollection.Empty ); CharacterCollection.Add( player, ModCollection.Empty );
} }

View file

@ -1,255 +1,256 @@
using Dalamud.Plugin;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Interop;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods namespace Penumbra.Mods;
// A ModCollection is a named set of ModSettings to all of the users' installed mods.
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made.
// Active ModCollections build a cache of currently relevant data.
public class ModCollection
{ {
// A ModCollection is a named set of ModSettings to all of the users' installed mods. public const string DefaultCollection = "Default";
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. public string Name { get; set; }
// Active ModCollections build a cache of currently relevant data.
public class ModCollection public Dictionary< string, ModSettings > Settings { get; }
public ModCollection()
{ {
public const string DefaultCollection = "Default"; Name = DefaultCollection;
Settings = new Dictionary< string, ModSettings >();
}
public string Name { get; set; } public ModCollection( string name, Dictionary< string, ModSettings > settings )
{
Name = name;
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
}
public Dictionary< string, ModSettings > Settings { get; } public Mod.Mod GetMod( ModData mod )
{
public ModCollection() if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
{ {
Name = DefaultCollection; return ret;
Settings = new Dictionary< string, ModSettings >();
} }
public ModCollection( string name, Dictionary< string, ModSettings > settings ) if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{ {
Name = name; return new Mod.Mod( settings, mod );
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
} }
public Mod.Mod GetMod( ModData mod ) var newSettings = ModSettings.DefaultSettings( mod.Meta );
{ Settings.Add( mod.BasePath.Name, newSettings );
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) ) Save();
{ return new Mod.Mod( newSettings, mod );
return ret; }
}
private bool CleanUnavailableSettings( Dictionary< string, ModData > data )
{
var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray();
foreach( var s in removeList )
{
Settings.Remove( s.Key );
}
return removeList.Length > 0;
}
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data )
{
Cache = new ModCollectionCache( Name, modDirectory );
var changedSettings = false;
foreach( var mod in data )
{
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{ {
return new Mod.Mod( settings, mod ); Cache.AddMod( settings, mod, false );
} }
else
{
changedSettings = true;
var newSettings = ModSettings.DefaultSettings( mod.Meta );
Settings.Add( mod.BasePath.Name, newSettings );
Cache.AddMod( newSettings, mod, false );
}
}
var newSettings = ModSettings.DefaultSettings( mod.Meta ); if( changedSettings )
Settings.Add( mod.BasePath.Name, newSettings ); {
Save(); Save();
return new Mod.Mod( newSettings, mod );
} }
private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) CalculateEffectiveFileList( modDirectory, true, false );
}
public void ClearCache()
=> Cache = null;
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
{
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
{ {
var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray(); return;
foreach( var s in removeList )
{
Settings.Remove( s.Key );
}
return removeList.Length > 0;
} }
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data ) if( clear )
{ {
Cache = new ModCollectionCache( Name, modDirectory ); settings.Settings.Clear();
var changedSettings = false;
foreach( var mod in data )
{
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
Cache.AddMod( settings, mod, false );
}
else
{
changedSettings = true;
var newSettings = ModSettings.DefaultSettings( mod.Meta );
Settings.Add( mod.BasePath.Name, newSettings );
Cache.AddMod( newSettings, mod, false );
}
}
if( changedSettings )
{
Save();
}
CalculateEffectiveFileList( modDirectory, true, false );
} }
public void ClearCache() if( settings.FixInvalidSettings( meta ) )
=> Cache = null;
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
{ {
if( !Settings.TryGetValue( modPath.Name, out var settings ) ) Save();
{ }
return; }
}
if (clear) public void UpdateSetting( ModData mod )
settings.Settings.Clear(); => UpdateSetting( mod.BasePath, mod.Meta, false );
if( settings.FixInvalidSettings( meta ) )
{ public void UpdateSettings( bool forceSave )
Save(); {
} if( Cache == null )
{
return;
} }
public void UpdateSetting( ModData mod ) var changes = false;
=> UpdateSetting( mod.BasePath, mod.Meta, false ); foreach( var mod in Cache.AvailableMods.Values )
public void UpdateSettings( bool forceSave )
{ {
if( Cache == null ) changes |= mod.FixSettings();
{
return;
}
var changes = false;
foreach( var mod in Cache.AvailableMods.Values )
{
changes |= mod.FixSettings();
}
if( forceSave || changes )
{
Save();
}
} }
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection ) if( forceSave || changes )
{ {
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, Save();
withMetaManipulations, activeCollection ); }
Cache ??= new ModCollectionCache( Name, modDir ); }
UpdateSettings( false );
Cache.CalculateEffectiveFileList(); public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
if( withMetaManipulations ) {
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name,
withMetaManipulations, activeCollection );
Cache ??= new ModCollectionCache( Name, modDir );
UpdateSettings( false );
Cache.CalculateEffectiveFileList();
if( withMetaManipulations )
{
Cache.UpdateMetaManipulations();
if( activeCollection )
{ {
Cache.UpdateMetaManipulations(); Penumbra.ResidentResources.Reload();
if( activeCollection )
{
Penumbra.ResidentResources.Reload();
}
} }
} }
}
[JsonIgnore] [JsonIgnore]
public ModCollectionCache? Cache { get; private set; } public ModCollectionCache? Cache { get; private set; }
public static ModCollection? LoadFromFile( FileInfo file ) public static ModCollection? LoadFromFile( FileInfo file )
{
if( !file.Exists )
{ {
if( !file.Exists ) PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
{
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
return null;
}
try
{
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
return collection;
}
catch( Exception e )
{
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
}
return null; return null;
} }
private void SaveToFile( FileInfo file ) try
{ {
try var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
{ return collection;
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); }
} catch( Exception e )
catch( Exception e ) {
{ PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
}
} }
public static DirectoryInfo CollectionDir() return null;
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ) );
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
=> new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) );
public FileInfo FileName()
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
$"{Name.RemoveInvalidPathSymbols()}.json" ) );
public void Save()
{
try
{
var dir = CollectionDir();
dir.Create();
var file = FileName( dir, Name );
SaveToFile( file );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
}
}
public static ModCollection? Load( string name )
{
var file = FileName( CollectionDir(), name );
return file.Exists ? LoadFromFile( file ) : null;
}
public void Delete()
{
var file = FileName( CollectionDir(), Name );
if( file.Exists )
{
try
{
file.Delete();
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
}
}
}
public void AddMod( ModData data )
{
if( Cache == null )
{
return;
}
Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings )
? settings
: ModSettings.DefaultSettings( data.Meta ),
data );
}
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
public static readonly ModCollection Empty = new() { Name = "" };
} }
private void SaveToFile( FileInfo file )
{
try
{
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
}
catch( Exception e )
{
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
}
}
public static DirectoryInfo CollectionDir()
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ));
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
=> new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ));
public FileInfo FileName()
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
$"{Name.RemoveInvalidPathSymbols()}.json" ));
public void Save()
{
try
{
var dir = CollectionDir();
dir.Create();
var file = FileName( dir, Name );
SaveToFile( file );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
}
}
public static ModCollection? Load( string name )
{
var file = FileName( CollectionDir(), name );
return file.Exists ? LoadFromFile( file ) : null;
}
public void Delete()
{
var file = FileName( CollectionDir(), Name );
if( file.Exists )
{
try
{
file.Delete();
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
}
}
}
public void AddMod( ModData data )
{
if( Cache == null )
{
return;
}
Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings )
? settings
: ModSettings.DefaultSettings( data.Meta ),
data );
}
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
public static readonly ModCollection Empty = new() { Name = "" };
} }

View file

@ -10,7 +10,6 @@ using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
@ -20,17 +19,16 @@ namespace Penumbra.Mods;
public class ModCollectionCache public class ModCollectionCache
{ {
// Shared caches to avoid allocations. // Shared caches to avoid allocations.
private static readonly BitArray FileSeen = new(256); private static readonly BitArray FileSeen = new(256);
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256); private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256);
public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
private readonly SortedList< string, object? > _changedItems = new(); private readonly SortedList< string, object? > _changedItems = new();
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); public readonly HashSet< FullPath > MissingFiles = new();
public readonly HashSet< FullPath > MissingFiles = new(); public readonly HashSet< ulong > Checksums = new();
public readonly HashSet< ulong > Checksums = new(); public readonly MetaManager MetaManipulations;
public readonly MetaManager MetaManipulations;
public IReadOnlyDictionary< string, object? > ChangedItems public IReadOnlyDictionary< string, object? > ChangedItems
{ {
@ -61,7 +59,6 @@ public class ModCollectionCache
public void CalculateEffectiveFileList() public void CalculateEffectiveFileList()
{ {
ResolvedFiles.Clear(); ResolvedFiles.Clear();
SwappedFiles.Clear();
MissingFiles.Clear(); MissingFiles.Clear();
RegisteredFiles.Clear(); RegisteredFiles.Clear();
_changedItems.Clear(); _changedItems.Clear();
@ -85,7 +82,7 @@ public class ModCollectionCache
private void SetChangedItems() private void SetChangedItems()
{ {
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 ) if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
{ {
return; return;
} }
@ -98,12 +95,7 @@ public class ModCollectionCache
var identifier = GameData.GameData.GetIdentifier(); var identifier = GameData.GameData.GetIdentifier();
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) ) foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
{ {
identifier.Identify( _changedItems, resolved ); identifier.Identify( _changedItems, resolved.ToGamePath() );
}
foreach( var swapped in SwappedFiles.Keys )
{
identifier.Identify( _changedItems, swapped );
} }
} }
catch( Exception e ) catch( Exception e )
@ -134,12 +126,12 @@ public class ModCollectionCache
AddRemainingFiles( mod ); AddRemainingFiles( mod );
} }
private bool FilterFile( GamePath gamePath ) private static bool FilterFile( Utf8GamePath gamePath )
{ {
// If audio streaming is not disabled, replacing .scd files crashes the game, // If audio streaming is not disabled, replacing .scd files crashes the game,
// so only add those files if it is disabled. // so only add those files if it is disabled.
if( !Penumbra.Config.DisableSoundStreaming if( !Penumbra.Config.DisableSoundStreaming
&& gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) ) && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
{ {
return true; return true;
} }
@ -148,7 +140,7 @@ public class ModCollectionCache
} }
private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file )
{ {
if( FilterFile( gamePath ) ) if( FilterFile( gamePath ) )
{ {
@ -187,9 +179,8 @@ public class ModCollectionCache
{ {
foreach( var (file, paths) in option.OptionFiles ) foreach( var (file, paths) in option.OptionFiles )
{ {
var fullPath = new FullPath( mod.Data.BasePath, var fullPath = new FullPath( mod.Data.BasePath, file );
NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
if( idx < 0 ) if( idx < 0 )
{ {
AddMissingFile( fullPath ); AddMissingFile( fullPath );
@ -259,7 +250,7 @@ public class ModCollectionCache
{ {
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) ) if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
{ {
AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO AddFile( mod, gamePath, file );
} }
else else
{ {
@ -294,7 +285,7 @@ public class ModCollectionCache
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) ) if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
{ {
RegisteredFiles.Add( key, mod ); RegisteredFiles.Add( key, mod );
SwappedFiles.Add( key, value ); ResolvedFiles.Add( key, value );
} }
else else
{ {
@ -341,54 +332,54 @@ public class ModCollectionCache
public void RemoveMod( DirectoryInfo basePath ) public void RemoveMod( DirectoryInfo basePath )
{ {
if( AvailableMods.TryGetValue( basePath.Name, out var mod ) ) if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) )
{ {
AvailableMods.Remove( basePath.Name ); return;
if( mod.Settings.Enabled ) }
{
CalculateEffectiveFileList(); AvailableMods.Remove( basePath.Name );
if( mod.Data.Resources.MetaManipulations.Count > 0 ) if( !mod.Settings.Enabled )
{ {
UpdateMetaManipulations(); return;
} }
}
CalculateEffectiveFileList();
if( mod.Data.Resources.MetaManipulations.Count > 0 )
{
UpdateMetaManipulations();
} }
} }
private class PriorityComparer : IComparer< Mod.Mod >
{
public int Compare( Mod.Mod? x, Mod.Mod? y )
=> ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 );
}
private static readonly PriorityComparer Comparer = new();
public void AddMod( ModSettings settings, ModData data, bool updateFileList = true ) public void AddMod( ModSettings settings, ModData data, bool updateFileList = true )
{ {
if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) ) if( AvailableMods.ContainsKey( data.BasePath.Name ) )
{ {
var newMod = new Mod.Mod( settings, data ); return;
AvailableMods[ data.BasePath.Name ] = newMod; }
if( updateFileList && settings.Enabled ) AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data );
{
CalculateEffectiveFileList(); if( !updateFileList || !settings.Enabled )
if( data.Resources.MetaManipulations.Count > 0 ) {
{ return;
UpdateMetaManipulations(); }
}
} CalculateEffectiveFileList();
if( data.Resources.MetaManipulations.Count > 0 )
{
UpdateMetaManipulations();
} }
} }
public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath )
{ {
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
{ {
return null; return null;
} }
if( candidate.FullName.Length >= 260 || !candidate.Exists ) if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.IsRooted && !candidate.Exists )
{ {
return null; return null;
} }
@ -396,9 +387,6 @@ public class ModCollectionCache
return candidate; return candidate;
} }
public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; => GetCandidateForGameFile( gameResourcePath );
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mod; using Penumbra.Mod;
@ -347,15 +348,7 @@ namespace Penumbra.Mods
return true; return true;
} }
public bool CheckCrc64( ulong crc ) public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
{
if( Collections.ActiveCollection.Cache?.Checksums.Contains( crc ) ?? false )
return true;
return Collections.ForcedCollection.Cache?.Checksums.Contains( crc ) ?? false;
}
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
{ {
var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath );

View file

@ -4,7 +4,6 @@ using System.ComponentModel;
using System.IO; using System.IO;
using Dalamud.Logging; using Dalamud.Logging;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Structs;
namespace Penumbra.Mods; namespace Penumbra.Mods;

View file

@ -18,22 +18,6 @@ using System.Linq;
namespace Penumbra; namespace Penumbra;
public class Penumbra2 // : IDalamudPlugin
{
public string Name
=> "Penumbra";
private const string CommandName = "/penumbra";
public static Configuration Config { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!;
public void Dispose()
{
ResourceLoader.Dispose();
}
}
public class Penumbra : IDalamudPlugin public class Penumbra : IDalamudPlugin
{ {
public string Name public string Name
@ -54,6 +38,7 @@ public class Penumbra : IDalamudPlugin
public ResourceLoader ResourceLoader { get; } public ResourceLoader ResourceLoader { get; }
public ResourceLogger ResourceLogger { get; }
//public PathResolver PathResolver { get; } //public PathResolver PathResolver { get; }
public SettingsInterface SettingsInterface { get; } public SettingsInterface SettingsInterface { get; }
@ -81,19 +66,18 @@ public class Penumbra : IDalamudPlugin
CharacterUtility = new CharacterUtility(); CharacterUtility = new CharacterUtility();
MetaDefaults = new MetaDefaults(); MetaDefaults = new MetaDefaults();
ResourceLoader = new ResourceLoader( this ); ResourceLoader = new ResourceLoader( this );
ResourceLogger = new ResourceLogger( ResourceLoader );
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
ModManager = new ModManager(); ModManager = new ModManager();
ModManager.DiscoverMods(); ModManager.DiscoverMods();
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames ); ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames );
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
{ {
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
} ); } );
ResourceLoader.EnableReplacements();
ResourceLoader.EnableLogging();
if( Config.DebugMode ) if( Config.DebugMode )
{ {
ResourceLoader.EnableDebug(); ResourceLoader.EnableDebug();
@ -112,7 +96,7 @@ public class Penumbra : IDalamudPlugin
CreateWebServer(); CreateWebServer();
} }
if( !Config.EnablePlayerWatch || !Config.IsEnabled ) if( !Config.EnablePlayerWatch || !Config.EnableMods )
{ {
PlayerWatcher.Disable(); PlayerWatcher.Disable();
} }
@ -122,16 +106,25 @@ public class Penumbra : IDalamudPlugin
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name ); PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings ); ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings );
}; };
ResourceLoader.EnableHooks();
if (Config.EnableMods)
ResourceLoader.EnableReplacements();
if (Config.DebugMode)
ResourceLoader.EnableDebug();
if (Config.EnableFullResourceLogging)
ResourceLoader.EnableFullLogging();
} }
public bool Enable() public bool Enable()
{ {
if( Config.IsEnabled ) if( Config.EnableMods )
{ {
return false; return false;
} }
Config.IsEnabled = true; Config.EnableMods = true;
ResourceLoader.EnableReplacements();
ResidentResources.Reload(); ResidentResources.Reload();
if( Config.EnablePlayerWatch ) if( Config.EnablePlayerWatch )
{ {
@ -145,12 +138,13 @@ public class Penumbra : IDalamudPlugin
public bool Disable() public bool Disable()
{ {
if( !Config.IsEnabled ) if( !Config.EnableMods )
{ {
return false; return false;
} }
Config.IsEnabled = false; Config.EnableMods = false;
ResourceLoader.DisableReplacements();
ResidentResources.Reload(); ResidentResources.Reload();
if( Config.EnablePlayerWatch ) if( Config.EnablePlayerWatch )
{ {
@ -219,7 +213,9 @@ public class Penumbra : IDalamudPlugin
Dalamud.Commands.RemoveHandler( CommandName ); Dalamud.Commands.RemoveHandler( CommandName );
//PathResolver.Dispose(); //PathResolver.Dispose();
ResourceLogger.Dispose();
ResourceLoader.Dispose(); ResourceLoader.Dispose();
ShutdownWebServer(); ShutdownWebServer();
} }
@ -322,8 +318,8 @@ public class Penumbra : IDalamudPlugin
} }
case "toggle": case "toggle":
{ {
SetEnabled( !Config.IsEnabled ); SetEnabled( !Config.EnableMods );
Dalamud.Chat.Print( Config.IsEnabled Dalamud.Chat.Print( Config.EnableMods
? modsEnabled ? modsEnabled
: modsDisabled ); : modsDisabled );
break; break;

View file

@ -1,103 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using Penumbra.GameData.Util;
using Penumbra.Util;
namespace Penumbra.Structs
{
public enum SelectType
{
Single,
Multi,
}
public struct Option
{
public string OptionName;
public string OptionDesc;
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )]
public Dictionary< RelPath, HashSet< GamePath > > OptionFiles;
public bool AddFile( RelPath filePath, GamePath gamePath )
{
if( OptionFiles.TryGetValue( filePath, out var set ) )
{
return set.Add( gamePath );
}
OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath };
return true;
}
}
public struct OptionGroup
{
public string GroupName;
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
public SelectType SelectionType;
public List< Option > Options;
private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
// Selection contains the path, merge all GamePaths for this config.
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
return true;
}
// If the group contains the file in another selection, return true to skip it for default files.
for( var i = 0; i < Options.Count; ++i )
{
if( i == selection )
{
continue;
}
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
return true;
}
}
return false;
}
private bool ApplyMultiGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
var doNotAdd = false;
for( var i = 0; i < Options.Count; ++i )
{
if( ( selection & ( 1 << i ) ) != 0 )
{
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
{
paths.UnionWith( groupPaths );
doNotAdd = true;
}
}
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
{
doNotAdd = true;
}
}
return doNotAdd;
}
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
internal bool ApplyGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
{
return SelectionType switch
{
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
};
}
}
}

View file

@ -1,8 +1,8 @@
using System.Numerics; using System.Numerics;
using System.Security.Cryptography.X509Certificates;
using System.Windows.Forms; using System.Windows.Forms;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.ByteString;
namespace Penumbra.UI.Custom namespace Penumbra.UI.Custom
{ {
@ -20,6 +20,19 @@ namespace Penumbra.UI.Custom
ImGui.SetTooltip( "Click to copy to clipboard." ); ImGui.SetTooltip( "Click to copy to clipboard." );
} }
} }
public static unsafe void CopyOnClickSelectable( Utf8String text )
{
if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 )
{
ImGuiNative.igSetClipboardText( text.Path );
}
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( "Click to copy to clipboard." );
}
}
} }
public static partial class ImGuiCustom public static partial class ImGuiCustom

View file

@ -21,11 +21,21 @@ public partial class SettingsInterface
private string _filePathFilter = string.Empty; private string _filePathFilter = string.Empty;
private string _filePathFilterLower = string.Empty; private string _filePathFilterLower = string.Empty;
private readonly float _leftTextLength = private const float LeftTextLength = 600;
ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40;
private float _arrowLength = 0; private float _arrowLength = 0;
private static void DrawLine( Utf8GamePath path, FullPath name )
{
ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( path.Path );
ImGui.TableNextColumn();
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft );
ImGui.SameLine();
ImGuiCustom.CopyOnClickSelectable( name.InternalName );
}
private static void DrawLine( string path, string name ) private static void DrawLine( string path, string name )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -45,13 +55,13 @@ public partial class SettingsInterface
_arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale; _arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale;
} }
ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) ) if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) )
{ {
_gamePathFilterLower = _gamePathFilter.ToLowerInvariant(); _gamePathFilterLower = _gamePathFilter.ToLowerInvariant();
} }
ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X ); ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X );
ImGui.SetNextItemWidth( -1 ); ImGui.SetNextItemWidth( -1 );
if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) ) if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) )
{ {
@ -59,7 +69,7 @@ public partial class SettingsInterface
} }
} }
private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp )
{ {
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
{ {
@ -69,7 +79,7 @@ public partial class SettingsInterface
return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower ); return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower );
} }
private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp ) private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp )
{ {
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
{ {
@ -94,11 +104,6 @@ public partial class SettingsInterface
void DrawFileLines( ModCollectionCache cache ) void DrawFileLines( ModCollectionCache cache )
{ {
foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) ) foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) )
{
DrawLine( gp, fp.FullName );
}
foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) )
{ {
DrawLine( gp, fp ); DrawLine( gp, fp );
} }
@ -139,75 +144,67 @@ public partial class SettingsInterface
var activeCollection = modManager.Collections.ActiveCollection.Cache; var activeCollection = modManager.Collections.ActiveCollection.Cache;
var forcedCollection = modManager.Collections.ForcedCollection.Cache; var forcedCollection = modManager.Collections.ForcedCollection.Cache;
var (activeResolved, activeSwap, activeMeta) = activeCollection != null var (activeResolved, activeMeta) = activeCollection != null
? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count ) ? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count )
: ( 0, 0, 0 ); : ( 0, 0 );
var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null var (forcedResolved, forcedMeta) = forcedCollection != null
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count ) ? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count )
: ( 0, 0, 0 ); : ( 0, 0 );
var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta; var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta;
if( totalLines == 0 ) if( totalLines == 0 )
{ {
return; return;
} }
if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) ) if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
{ {
raii.Push( ImGui.EndTable ); return;
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale ); }
if( _filePathFilter.Any() || _gamePathFilter.Any() ) raii.Push( ImGui.EndTable );
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, LeftTextLength * ImGuiHelpers.GlobalScale );
if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 )
{
DrawFilteredRows( activeCollection, forcedCollection );
}
else
{
ImGuiListClipperPtr clipper;
unsafe
{ {
DrawFilteredRows( activeCollection, forcedCollection ); clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() );
} }
else
clipper.Begin( totalLines );
while( clipper.Step() )
{ {
ImGuiListClipperPtr clipper; for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
unsafe
{ {
clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() ); var row = actualRow;
} ImGui.TableNextRow();
if( row < activeResolved )
clipper.Begin( totalLines );
while( clipper.Step() )
{
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
{ {
var row = actualRow; var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
ImGui.TableNextRow(); DrawLine( gamePath, file );
if( row < activeResolved ) }
{ else if( ( row -= activeResolved ) < activeMeta )
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row ); {
DrawLine( gamePath, file.FullName ); var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
} DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
else if( ( row -= activeResolved ) < activeSwap ) }
{ else if( ( row -= activeMeta ) < forcedResolved )
var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row ); {
DrawLine( gamePath, swap ); var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
} DrawLine( gamePath, file );
else if( ( row -= activeSwap ) < activeMeta ) }
{ else
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row ); {
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name ); row -= forcedResolved;
} var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
else if( ( row -= activeMeta ) < forcedResolved ) DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
{
var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
DrawLine( gamePath, file.FullName );
}
else if( ( row -= forcedResolved ) < forcedSwap )
{
var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row );
DrawLine( gamePath, swap );
}
else
{
row -= forcedSwap;
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
}
} }
} }
} }

View file

@ -11,7 +11,6 @@ using Penumbra.GameData.Util;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mod; using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util; using Penumbra.Util;
using ImGui = ImGuiNET.ImGui; using ImGui = ImGuiNET.ImGui;
@ -56,7 +55,7 @@ public partial class SettingsInterface
private Option? _selectedOption; private Option? _selectedOption;
private string _currentGamePaths = ""; private string _currentGamePaths = "";
private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList;
private readonly Selector _selector; private readonly Selector _selector;
private readonly SettingsInterface _base; private readonly SettingsInterface _base;
@ -218,7 +217,10 @@ public partial class SettingsInterface
indent.Push( 15f ); indent.Push( 15f );
foreach( var file in files ) foreach( var file in files )
{ {
ImGui.Selectable( file ); unsafe
{
ImGuiNative.igSelectable_Bool( file.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
}
} }
foreach( var manip in manipulations ) foreach( var manip in manipulations )
@ -258,13 +260,13 @@ public partial class SettingsInterface
foreach( var (source, target) in Meta.FileSwaps ) foreach( var (source, target) in Meta.FileSwaps )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( source ); ImGuiCustom.CopyOnClickSelectable( source.Path );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight ); ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( target ); ImGuiCustom.CopyOnClickSelectable( target.InternalName );
ImGui.TableNextRow(); ImGui.TableNextRow();
} }
@ -278,7 +280,8 @@ public partial class SettingsInterface
} }
_fullFilenameList = Mod.Data.Resources.ModFiles _fullFilenameList = Mod.Data.Resources.ModFiles
.Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); .Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) )
.ToArray();
if( Meta.Groups.Count == 0 ) if( Meta.Groups.Count == 0 )
{ {
@ -339,24 +342,23 @@ public partial class SettingsInterface
} }
} }
private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders )
{ {
removeFolders = 0; removeFolders = 0;
var defaultIndex = var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) );
gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) );
if( defaultIndex < 0 ) if( defaultIndex < 0 )
{ {
return defaultIndex; return defaultIndex;
} }
string path = gamePaths[ defaultIndex ]; var path = gamePaths[ defaultIndex ].Path;
if( path.Length == TextDefaultGamePath.Length ) if( path.Length == TextDefaultGamePath.Length )
{ {
return defaultIndex; return defaultIndex;
} }
if( path[ TextDefaultGamePath.Length ] != '-' if( path[ TextDefaultGamePath.Length ] != ( byte )'-'
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) ) || !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) )
{ {
return -1; return -1;
} }
@ -373,8 +375,9 @@ public partial class SettingsInterface
var option = ( Option )_selectedOption; var option = ( Option )_selectedOption;
var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); var gamePaths = _currentGamePaths.Split( ';' )
if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) .Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray();
if( gamePaths.Length == 0 )
{ {
return; return;
} }
@ -517,18 +520,18 @@ public partial class SettingsInterface
{ {
Selectable( 0, ColorGreen ); Selectable( 0, ColorGreen );
using var indent = ImGuiRaii.PushIndent( indentWidth ); using var indent = ImGuiRaii.PushIndent( indentWidth );
var tmpPaths = gamePaths.ToArray(); foreach( var gamePath in gamePaths.ToArray() )
foreach( var gamePath in tmpPaths )
{ {
string tmp = gamePath; var tmp = gamePath.ToString();
var old = tmp;
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue ) if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
&& tmp != gamePath ) && tmp != old )
{ {
gamePaths.Remove( gamePath ); gamePaths.Remove( gamePath );
if( tmp.Length > 0 ) if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) )
{ {
gamePaths.Add( new GamePath( tmp ) ); gamePaths.Add( p );
} }
else if( gamePaths.Count == 0 ) else if( gamePaths.Count == 0 )
{ {

View file

@ -3,136 +3,179 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util; using Penumbra.GameData.Util;
using Penumbra.Mod;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.UI namespace Penumbra.UI;
public partial class SettingsInterface
{ {
public partial class SettingsInterface private partial class PluginDetails
{ {
private partial class PluginDetails private const string LabelDescEdit = "##descedit";
private const string LabelNewSingleGroupEdit = "##newSingleGroup";
private const string LabelNewMultiGroup = "##newMultiGroup";
private const string LabelGamePathsEditBox = "##gamePathsEdit";
private const string ButtonAddToGroup = "Add to Group";
private const string ButtonRemoveFromGroup = "Remove from Group";
private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines.";
private const string TextNoOptionAvailable = "[Not Available]";
private const string TextDefaultGamePath = "default";
private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true );
private const char GamePathsSeparator = ';';
private static readonly string TooltipFilesTabEdit =
$"{TooltipFilesTab}\n"
+ $"Red Files are replaced in another group or a different option in this group, but not contained in the current option.";
private static readonly string TooltipGamePathsEdit =
$"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n"
+ $"Use '{TextDefaultGamePath}' to add the original file path."
+ $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories.";
private const float MultiEditBoxWidth = 300f;
private bool DrawEditGroupSelector()
{ {
private const string LabelDescEdit = "##descedit"; ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale );
private const string LabelNewSingleGroupEdit = "##newSingleGroup"; if( Meta!.Groups.Count == 0 )
private const string LabelNewMultiGroup = "##newMultiGroup";
private const string LabelGamePathsEditBox = "##gamePathsEdit";
private const string ButtonAddToGroup = "Add to Group";
private const string ButtonRemoveFromGroup = "Remove from Group";
private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines.";
private const string TextNoOptionAvailable = "[Not Available]";
private const string TextDefaultGamePath = "default";
private const char GamePathsSeparator = ';';
private static readonly string TooltipFilesTabEdit =
$"{TooltipFilesTab}\n"
+ $"Red Files are replaced in another group or a different option in this group, but not contained in the current option.";
private static readonly string TooltipGamePathsEdit =
$"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n"
+ $"Use '{TextDefaultGamePath}' to add the original file path."
+ $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories.";
private const float MultiEditBoxWidth = 300f;
private bool DrawEditGroupSelector()
{ {
ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale ); ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
if( Meta!.Groups.Count == 0 ) return false;
{
ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
return false;
}
if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex
, Meta.Groups.Values.Select( g => g.GroupName ).ToArray()
, Meta.Groups.Count ) )
{
SelectGroup();
SelectOption( 0 );
}
return true;
} }
private bool DrawEditOptionSelector() if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex
, Meta.Groups.Values.Select( g => g.GroupName ).ToArray()
, Meta.Groups.Count ) )
{ {
SelectGroup();
SelectOption( 0 );
}
return true;
}
private bool DrawEditOptionSelector()
{
ImGui.SameLine();
ImGui.SetNextItemWidth( OptionSelectionWidth );
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
{
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 );
return false;
}
var group = ( OptionGroup )_selectedGroup!;
if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(),
group.Options.Count ) )
{
SelectOption();
}
return true;
}
private void DrawFileListTabEdit()
{
if( ImGui.BeginTabItem( LabelFileListTab ) )
{
UpdateFilenameList();
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab );
}
ImGui.SetNextItemWidth( -1 );
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) )
{
for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i )
{
DrawFileAndGamePaths( i );
}
}
ImGui.EndListBox();
DrawGroupRow();
ImGui.EndTabItem();
}
else
{
_fullFilenameList = null;
}
}
private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group )
{
var groupName = group.GroupName;
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) )
{
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
}
return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup );
}
private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart )
{
var newOption = "";
ImGui.SetCursorPosX( nameBoxStart );
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64,
ImGuiInputTextFlags.EnterReturnsTrue )
&& newOption.Length != 0 )
{
group.Options.Add( new Option()
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } );
_selector.SaveCurrentMod();
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
}
}
private void DrawMultiSelectorEdit( OptionGroup group )
{
var nameBoxStart = CheckMarkSize;
var flag = Mod!.Settings.Settings[ group.GroupName ];
using var raii = DrawMultiSelectorEditBegin( group );
for( var i = 0; i < group.Options.Count; ++i )
{
var opt = group.Options[ i ];
var label = $"##{group.GroupName}_{i}";
DrawMultiSelectorCheckBox( group, i, flag, label );
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth( OptionSelectionWidth ); var newName = opt.OptionName;
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
if( nameBoxStart == CheckMarkSize )
{ {
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 ); nameBoxStart = ImGui.GetCursorPosX();
return false;
} }
var group = ( OptionGroup )_selectedGroup!;
if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(),
group.Options.Count ) )
{
SelectOption();
}
return true;
}
private void DrawFileListTabEdit()
{
if( ImGui.BeginTabItem( LabelFileListTab ) )
{
UpdateFilenameList();
if( ImGui.IsItemHovered() )
{
ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab );
}
ImGui.SetNextItemWidth( -1 );
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) )
{
for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i )
{
DrawFileAndGamePaths( i );
}
}
ImGui.EndListBox();
DrawGroupRow();
ImGui.EndTabItem();
}
else
{
_fullFilenameList = null;
}
}
private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group )
{
var groupName = group.GroupName;
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) )
{
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
}
return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup );
}
private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart )
{
var newOption = "";
ImGui.SetCursorPosX( nameBoxStart );
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64, if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
ImGuiInputTextFlags.EnterReturnsTrue )
&& newOption.Length != 0 )
{ {
group.Options.Add( new Option() if( newName.Length == 0 )
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } ); {
_selector.SaveCurrentMod(); Penumbra.ModManager.RemoveModOption( i, group, Mod.Data );
}
else if( newName != opt.OptionName )
{
group.Options[ i ] = new Option()
{ OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles };
_selector.SaveCurrentMod();
}
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() ) if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
{ {
_selector.Cache.TriggerFilterReset(); _selector.Cache.TriggerFilterReset();
@ -140,244 +183,201 @@ namespace Penumbra.UI
} }
} }
private void DrawMultiSelectorEdit( OptionGroup group ) DrawMultiSelectorEditAdd( group, nameBoxStart );
}
private void DrawSingleSelectorEditGroup( OptionGroup group )
{
var groupName = group.GroupName;
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
var nameBoxStart = CheckMarkSize; if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
var flag = Mod!.Settings.Settings[ group.GroupName ];
using var raii = DrawMultiSelectorEditBegin( group );
for( var i = 0; i < group.Options.Count; ++i )
{ {
var opt = group.Options[ i ]; _selector.Cache.TriggerFilterReset();
var label = $"##{group.GroupName}_{i}";
DrawMultiSelectorCheckBox( group, i, flag, label );
ImGui.SameLine();
var newName = opt.OptionName;
if( nameBoxStart == CheckMarkSize )
{
nameBoxStart = ImGui.GetCursorPosX();
}
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{
if( newName.Length == 0 )
{
Penumbra.ModManager.RemoveModOption( i, group, Mod.Data );
}
else if( newName != opt.OptionName )
{
group.Options[ i ] = new Option()
{ OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles };
_selector.SaveCurrentMod();
}
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
}
}
DrawMultiSelectorEditAdd( group, nameBoxStart );
}
private void DrawSingleSelectorEditGroup( OptionGroup group )
{
var groupName = group.GroupName;
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
} }
} }
}
private float DrawSingleSelectorEdit( OptionGroup group ) private float DrawSingleSelectorEdit( OptionGroup group )
{
var oldSetting = Mod!.Settings.Settings[ group.GroupName ];
var code = oldSetting;
if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) )
{ {
var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; if( code == group.Options.Count )
var code = oldSetting;
if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) )
{ {
if( code == group.Options.Count ) if( newName.Length > 0 )
{ {
if( newName.Length > 0 ) Mod.Settings.Settings[ group.GroupName ] = code;
group.Options.Add( new Option()
{ {
Mod.Settings.Settings[ group.GroupName ] = code; OptionName = newName,
group.Options.Add( new Option() OptionDesc = "",
{ OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
OptionName = newName, } );
OptionDesc = "", _selector.SaveCurrentMod();
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), }
} ); }
_selector.SaveCurrentMod(); else
} {
if( newName.Length == 0 )
{
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
} }
else else
{ {
if( newName.Length == 0 ) if( newName != group.Options[ code ].OptionName )
{ {
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data ); group.Options[ code ] = new Option()
}
else
{
if( newName != group.Options[ code ].OptionName )
{ {
group.Options[ code ] = new Option() OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
{ OptionFiles = group.Options[ code ].OptionFiles,
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, };
OptionFiles = group.Options[ code ].OptionFiles, _selector.SaveCurrentMod();
};
_selector.SaveCurrentMod();
}
} }
} }
if( Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{
_selector.Cache.TriggerFilterReset();
}
} }
if( code != oldSetting ) if( Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{ {
Save(); _selector.Cache.TriggerFilterReset();
}
}
if( code != oldSetting )
{
Save();
}
ImGui.SameLine();
var labelEditPos = ImGui.GetCursorPosX();
DrawSingleSelectorEditGroup( group );
return labelEditPos;
}
private void DrawAddSingleGroupField( float labelEditPos )
{
var newGroup = "";
ImGui.SetCursorPosX( labelEditPos );
if( labelEditPos == CheckMarkSize )
{
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
}
if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single );
// Adds empty group, so can not change filters.
}
}
private void DrawAddMultiGroupField()
{
var newGroup = "";
ImGui.SetCursorPosX( CheckMarkSize );
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi );
// Adds empty group, so can not change filters.
}
}
private void DrawGroupSelectorsEdit()
{
var labelEditPos = CheckMarkSize;
var groups = Meta.Groups.Values.ToArray();
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) )
{
labelEditPos = DrawSingleSelectorEdit( g );
}
DrawAddSingleGroupField( labelEditPos );
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) )
{
DrawMultiSelectorEdit( g );
}
DrawAddMultiGroupField();
}
private void DrawFileSwapTabEdit()
{
if( !ImGui.BeginTabItem( LabelFileSwapTab ) )
{
return;
}
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem );
ImGui.SetNextItemWidth( -1 );
if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
{
return;
}
raii.Push( ImGui.EndListBox );
var swaps = Meta.FileSwaps.Keys.ToArray();
ImGui.PushFont( UiBuilder.IconFont );
var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X;
ImGui.PopFont();
var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2;
for( var idx = 0; idx < swaps.Length + 1; ++idx )
{
var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ];
var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ];
var keyString = key.ToString();
var valueString = value.ToString();
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString,
GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) )
{
if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 )
{
if( idx < swaps.Length )
{
Meta.FileSwaps.Remove( key );
}
if( !newKey.IsEmpty )
{
Meta.FileSwaps[ newKey ] = value;
}
_selector.SaveCurrentMod();
_selector.ReloadCurrentMod();
}
}
if( idx >= swaps.Length )
{
continue;
} }
ImGui.SameLine(); ImGui.SameLine();
var labelEditPos = ImGui.GetCursorPosX(); ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
DrawSingleSelectorEditGroup( group ); ImGui.SameLine();
return labelEditPos; ImGui.SetNextItemWidth( width );
} if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
GamePath.MaxGamePathLength,
private void DrawAddSingleGroupField( float labelEditPos ) ImGuiInputTextFlags.EnterReturnsTrue ) )
{
var newGroup = "";
ImGui.SetCursorPosX( labelEditPos );
if( labelEditPos == CheckMarkSize )
{ {
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale ); var newValue = new FullPath( valueString.ToLowerInvariant() );
} if( newValue.CompareTo( value ) != 0 )
if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single );
// Adds empty group, so can not change filters.
}
}
private void DrawAddMultiGroupField()
{
var newGroup = "";
ImGui.SetCursorPosX( CheckMarkSize );
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi );
// Adds empty group, so can not change filters.
}
}
private void DrawGroupSelectorsEdit()
{
var labelEditPos = CheckMarkSize;
var groups = Meta.Groups.Values.ToArray();
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) )
{
labelEditPos = DrawSingleSelectorEdit( g );
}
DrawAddSingleGroupField( labelEditPos );
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) )
{
DrawMultiSelectorEdit( g );
}
DrawAddMultiGroupField();
}
private void DrawFileSwapTabEdit()
{
if( !ImGui.BeginTabItem( LabelFileSwapTab ) )
{
return;
}
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem );
ImGui.SetNextItemWidth( -1 );
if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
{
return;
}
raii.Push( ImGui.EndListBox );
var swaps = Meta.FileSwaps.Keys.ToArray();
ImGui.PushFont( UiBuilder.IconFont );
var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X;
ImGui.PopFont();
var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2;
for( var idx = 0; idx < swaps.Length + 1; ++idx )
{
var key = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : swaps[ idx ];
var value = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : Meta.FileSwaps[ key ];
string keyString = key;
string valueString = value;
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString,
GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) )
{ {
var newKey = new GamePath( keyString ); Meta.FileSwaps[ key ] = newValue;
if( newKey.CompareTo( key ) != 0 ) _selector.SaveCurrentMod();
{ _selector.Cache.TriggerListReset();
if( idx < swaps.Length )
{
Meta.FileSwaps.Remove( key );
}
if( newKey != string.Empty )
{
Meta.FileSwaps[ newKey ] = value;
}
_selector.SaveCurrentMod();
_selector.ReloadCurrentMod();
}
}
if( idx >= swaps.Length )
{
continue;
}
ImGui.SameLine();
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
ImGui.SameLine();
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
GamePath.MaxGamePathLength,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
var newValue = new GamePath( valueString );
if( newValue.CompareTo( value ) != 0 )
{
Meta.FileSwaps[ key ] = newValue;
_selector.SaveCurrentMod();
_selector.Cache.TriggerListReset();
}
} }
} }
} }

View file

@ -488,7 +488,7 @@ public partial class SettingsInterface
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup ); using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) ) if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) )
{ {
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
} }
@ -509,7 +509,7 @@ public partial class SettingsInterface
{ {
var change = false; var change = false;
var metaManips = false; var metaManips = false;
foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) ) foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) )
{ {
var (mod, _, _) = Cache.GetMod( currentIdx++ ); var (mod, _, _) = Cache.GetMod( currentIdx++ );
if( mod != null ) if( mod != null )

View file

@ -3,8 +3,10 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Logging;
using ImGuiNET; using ImGuiNET;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Interop; using Penumbra.Interop;
@ -131,7 +133,7 @@ public partial class SettingsInterface
private void DrawEnabledBox() private void DrawEnabledBox()
{ {
var enabled = _config.IsEnabled; var enabled = _config.EnableMods;
if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) if( ImGui.Checkbox( "Enable Mods", ref enabled ) )
{ {
_base._penumbra.SetEnabled( enabled ); _base._penumbra.SetEnabled( enabled );
@ -317,14 +319,84 @@ public partial class SettingsInterface
+ "You usually should not need to do this." ); + "You usually should not need to do this." );
} }
private void DrawEnableFullResourceLoggingBox()
{
var tmp = _config.EnableFullResourceLogging;
if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != _config.EnableFullResourceLogging )
{
if( tmp )
{
_base._penumbra.ResourceLoader.EnableFullLogging();
}
else
{
_base._penumbra.ResourceLoader.DisableFullLogging();
}
_config.EnableFullResourceLogging = tmp;
_configChanged = true;
}
ImGui.SameLine();
ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." );
}
private void DrawEnableDebugModeBox()
{
var tmp = _config.DebugMode;
if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != _config.DebugMode )
{
if( tmp )
{
_base._penumbra.ResourceLoader.EnableDebug();
}
else
{
_base._penumbra.ResourceLoader.DisableDebug();
}
_config.DebugMode = tmp;
_configChanged = true;
}
ImGui.SameLine();
ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." );
}
private void DrawRequestedResourceLogging()
{
var tmp = _config.EnableResourceLogging;
if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) )
{
_base._penumbra.ResourceLogger.SetState( tmp );
}
ImGui.SameLine();
ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n"
+ "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n"
+ "Red boundary indicates invalid regex." );
ImGui.SameLine();
var tmpString = Penumbra.Config.ResourceLoggingFilter;
using var color = ImGuiRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_base._penumbra.ResourceLogger.ValidRegex );
using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale,
!_base._penumbra.ResourceLogger.ValidRegex );
if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) )
{
_base._penumbra.ResourceLogger.SetFilter( tmpString );
}
}
private void DrawAdvancedSettings() private void DrawAdvancedSettings()
{ {
DrawTempFolder(); DrawTempFolder();
DrawRequestedResourceLogging();
DrawDisableSoundStreamingBox(); DrawDisableSoundStreamingBox();
DrawLogLoadedFilesBox(); DrawLogLoadedFilesBox();
DrawDisableNotificationsBox(); DrawDisableNotificationsBox();
DrawEnableHttpApiBox(); DrawEnableHttpApiBox();
DrawReloadResourceButton(); DrawReloadResourceButton();
DrawEnableDebugModeBox();
DrawEnableFullResourceLoggingBox();
} }
public static unsafe void Text( Utf8String s ) public static unsafe void Text( Utf8String s )

View file

@ -1,9 +1,7 @@
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Penumbra.Mods;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
using Penumbra.Util;
namespace Penumbra.UI; namespace Penumbra.UI;

View file

@ -5,36 +5,35 @@ using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.UI.Custom; using Penumbra.UI.Custom;
namespace Penumbra.UI namespace Penumbra.UI;
public partial class SettingsInterface
{ {
public partial class SettingsInterface internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 )
{ {
internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0) var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None;
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
if( ret != MouseButton.None )
{ {
var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; _penumbra.Api.InvokeClick( ret, data );
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; }
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
if( ret != MouseButton.None ) if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
{ {
_penumbra.Api.InvokeClick( ret, data ); ImGui.BeginTooltip();
} using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
_penumbra.Api.InvokeTooltip( data );
}
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() ) if( data is Item it )
{ {
ImGui.BeginTooltip(); var modelId = $"({( ( Quad )it.ModelMain ).A})";
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip ); var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
_penumbra.Api.InvokeTooltip( data );
}
if( data is Item it ) ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset );
{ ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
var modelId = $"({( ( Quad )it.ModelMain ).A})";
var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset );
ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
}
} }
} }
} }

View file

@ -1,87 +1,33 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Penumbra.Util namespace Penumbra.Util;
public static class ArrayExtensions
{ {
public static class ArrayExtensions public static int IndexOf< T >( this T[] array, Predicate< T > match )
{ {
public static T[] Slice< T >( this T[] source, int index, int length ) for( var i = 0; i < array.Length; ++i )
{ {
var slice = new T[length]; if( match( array[ i ] ) )
Array.Copy( source, index * length, slice, 0, length );
return slice;
}
public static void Swap< T >( this T[] array, int idx1, int idx2 )
{
var tmp = array[ idx1 ];
array[ idx1 ] = array[ idx2 ];
array[ idx2 ] = tmp;
}
public static void Swap< T >( this List< T > array, int idx1, int idx2 )
{
var tmp = array[ idx1 ];
array[ idx1 ] = array[ idx2 ];
array[ idx2 ] = tmp;
}
public static int IndexOf< T >( this T[] array, Predicate< T > match )
{
for( var i = 0; i < array.Length; ++i )
{ {
if( match( array[ i ] ) ) return i;
{
return i;
}
} }
return -1;
} }
public static void Swap< T >( this T[] array, T lhs, T rhs ) return -1;
}
public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate )
{
for( var i = 0; i < array.Count; ++i )
{ {
var idx1 = Array.IndexOf( array, lhs ); if( predicate.Invoke( array[ i ] ) )
if( idx1 < 0 )
{ {
return; return i;
} }
var idx2 = Array.IndexOf( array, rhs );
if( idx2 < 0 )
{
return;
}
array.Swap( idx1, idx2 );
} }
public static void Swap< T >( this List< T > array, T lhs, T rhs ) return -1;
{
var idx1 = array.IndexOf( lhs );
if( idx1 < 0 )
{
return;
}
var idx2 = array.IndexOf( rhs );
if( idx2 < 0 )
{
return;
}
array.Swap( idx1, idx2 );
}
public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate )
{
for( var i = 0; i < array.Count; ++i )
{
if( predicate.Invoke( array[ i ] ) )
return i;
}
return -1;
}
} }
} }

View file

@ -3,135 +3,54 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
namespace Penumbra.Util namespace Penumbra.Util;
public static class BinaryReaderExtensions
{ {
public static class BinaryReaderExtensions /// <summary>
/// Reads a structure from the current stream position.
/// </summary>
/// <param name="br"></param>
/// <typeparam name="T">The structure to read in to</typeparam>
/// <returns>The file data as a structure</returns>
public static T ReadStructure< T >( this BinaryReader br ) where T : struct
{ {
/// <summary> ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() );
/// Reads a structure from the current stream position.
/// </summary>
/// <param name="br"></param>
/// <typeparam name="T">The structure to read in to</typeparam>
/// <returns>The file data as a structure</returns>
public static T ReadStructure< T >( this BinaryReader br ) where T : struct
{
ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() );
return MemoryMarshal.Read< T >( data ); return MemoryMarshal.Read< T >( data );
}
/// <summary>
/// Reads many structures from the current stream position.
/// </summary>
/// <param name="br"></param>
/// <param name="count">The number of T to read from the stream</param>
/// <typeparam name="T">The structure to read in to</typeparam>
/// <returns>A list containing the structures read from the stream</returns>
public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct
{
var size = Marshal.SizeOf< T >();
var data = br.ReadBytes( size * count );
var list = new List< T >( count );
for( var i = 0; i < count; i++ )
{
var offset = size * i;
var span = new ReadOnlySpan< byte >( data, offset, size );
list.Add( MemoryMarshal.Read< T >( span ) );
} }
/// <summary> return list;
/// Reads many structures from the current stream position. }
/// </summary>
/// <param name="br"></param>
/// <param name="count">The number of T to read from the stream</param>
/// <typeparam name="T">The structure to read in to</typeparam>
/// <returns>A list containing the structures read from the stream</returns>
public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct
{
var size = Marshal.SizeOf< T >();
var data = br.ReadBytes( size * count );
var list = new List< T >( count ); /// <summary>
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
for( var i = 0; i < count; i++ ) /// </summary>
{ public static void Seek( this BinaryReader br, long offset )
var offset = size * i; {
var span = new ReadOnlySpan< byte >( data, offset, size ); br.BaseStream.Position = offset;
list.Add( MemoryMarshal.Read< T >( span ) );
}
return list;
}
public static T[] ReadStructuresAsArray< T >( this BinaryReader br, int count ) where T : struct
{
var size = Marshal.SizeOf< T >();
var data = br.ReadBytes( size * count );
// im a pirate arr
var arr = new T[count];
for( var i = 0; i < count; i++ )
{
var offset = size * i;
var span = new ReadOnlySpan< byte >( data, offset, size );
arr[ i ] = MemoryMarshal.Read< T >( span );
}
return arr;
}
/// <summary>
/// Moves the BinaryReader position to offset, reads a string, then
/// sets the reader position back to where it was when it started
/// </summary>
/// <param name="br"></param>
/// <param name="offset">The offset to read a string starting from.</param>
/// <returns></returns>
public static string ReadStringOffsetData( this BinaryReader br, long offset )
=> Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
/// <summary>
/// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then
/// sets the reader position back to where it was when it started
/// </summary>
/// <param name="br"></param>
/// <param name="offset">The offset to read data starting from.</param>
/// <returns></returns>
public static byte[] ReadRawOffsetData( this BinaryReader br, long offset )
{
var originalPosition = br.BaseStream.Position;
br.BaseStream.Position = offset;
var chars = new List< byte >();
byte current;
while( ( current = br.ReadByte() ) != 0 )
{
chars.Add( current );
}
br.BaseStream.Position = originalPosition;
return chars.ToArray();
}
/// <summary>
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
/// </summary>
public static void Seek( this BinaryReader br, long offset )
{
br.BaseStream.Position = offset;
}
/// <summary>
/// Reads a byte and moves the stream position back to where it started before the operation
/// </summary>
/// <param name="br">The reader to use to read the byte</param>
/// <returns>The byte that was read</returns>
public static byte PeekByte( this BinaryReader br )
{
var data = br.ReadByte();
br.BaseStream.Position--;
return data;
}
/// <summary>
/// Reads bytes and moves the stream position back to where it started before the operation
/// </summary>
/// <param name="br">The reader to use to read the bytes</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>The read bytes</returns>
public static byte[] PeekBytes( this BinaryReader br, int count )
{
var data = br.ReadBytes( count );
br.BaseStream.Position -= count;
return data;
}
} }
} }

View file

@ -2,36 +2,34 @@ using System.Collections.Generic;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
namespace Penumbra.Util namespace Penumbra.Util;
public static class ChatUtil
{ {
public static class ChatUtil public static void LinkItem( Item item )
{ {
public static void LinkItem( Item item ) var payloadList = new List< Payload >
{ {
var payloadList = new List< Payload > new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ),
{ new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ),
new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ), new ItemPayload( item.RowId, false ),
new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ), new UIForegroundPayload( 500 ),
new ItemPayload( item.RowId, false ), new UIGlowPayload( 501 ),
new UIForegroundPayload( 500 ), new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
new UIGlowPayload( 501 ), new UIForegroundPayload( 0 ),
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), new UIGlowPayload( 0 ),
new UIForegroundPayload( 0 ), new TextPayload( item.Name ),
new UIGlowPayload( 0 ), new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ),
new TextPayload( item.Name ), new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), };
new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
};
var payload = new SeString( payloadList ); var payload = new SeString( payloadList );
Dalamud.Chat.PrintChat( new XivChatEntry Dalamud.Chat.PrintChat( new XivChatEntry
{ {
Message = payload, Message = payload,
} ); } );
}
} }
} }

View file

@ -1,57 +0,0 @@
using System.Linq;
using System.Runtime.CompilerServices;
namespace Penumbra.Util
{
/// <summary>
/// Performs the 32-bit reversed variant of the cyclic redundancy check algorithm
/// </summary>
public class Crc32
{
private const uint Poly = 0xedb88320;
private static readonly uint[] CrcArray =
Enumerable.Range( 0, 256 ).Select( i =>
{
var k = ( uint )i;
for( var j = 0; j < 8; j++ )
{
k = ( k & 1 ) != 0 ? ( k >> 1 ) ^ Poly : k >> 1;
}
return k;
} ).ToArray();
public uint Checksum
=> ~_crc32;
private uint _crc32 = 0xFFFFFFFF;
/// <summary>
/// Initializes Crc32's state
/// </summary>
public void Init()
{
_crc32 = 0xFFFFFFFF;
}
/// <summary>
/// Updates Crc32's state with new data
/// </summary>
/// <param name="data">Data to calculate the new CRC from</param>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Update( byte[] data )
{
foreach( var b in data )
{
Update( b );
}
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public void Update( byte b )
{
_crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ ( ( _crc32 >> 8 ) & 0x00FFFFFF );
}
}
}

View file

@ -5,78 +5,77 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace Penumbra.Util namespace Penumbra.Util;
public static class DialogExtensions
{ {
public static class DialogExtensions public static Task< DialogResult > ShowDialogAsync( this CommonDialog form )
{ {
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form ) using var process = Process.GetCurrentProcess();
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
}
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner )
{
var taskSource = new TaskCompletionSource< DialogResult >();
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
th.Start();
return taskSource.Task;
}
[STAThread]
private static void DialogThread( CommonDialog form, IWin32Window owner,
TaskCompletionSource< DialogResult > taskSource )
{
Application.SetCompatibleTextRenderingDefault( false );
Application.EnableVisualStyles();
using var hiddenForm = new HiddenForm( form, owner, taskSource );
Application.Run( hiddenForm );
Application.ExitThread();
}
public class DialogHandle : IWin32Window
{
public IntPtr Handle { get; set; }
public DialogHandle( IntPtr handle )
=> Handle = handle;
}
public class HiddenForm : Form
{
private readonly CommonDialog _form;
private readonly IWin32Window _owner;
private readonly TaskCompletionSource< DialogResult > _taskSource;
public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
{ {
using var process = Process.GetCurrentProcess(); _form = form;
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) ); _owner = owner;
_taskSource = taskSource;
Opacity = 0;
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
Size = new Size( 0, 0 );
Shown += HiddenForm_Shown;
} }
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner ) private void HiddenForm_Shown( object? sender, EventArgs _ )
{ {
var taskSource = new TaskCompletionSource< DialogResult >(); Hide();
var th = new Thread( () => DialogThread( form, owner, taskSource ) ); try
th.Start();
return taskSource.Task;
}
[STAThread]
private static void DialogThread( CommonDialog form, IWin32Window owner,
TaskCompletionSource< DialogResult > taskSource )
{
Application.SetCompatibleTextRenderingDefault( false );
Application.EnableVisualStyles();
using var hiddenForm = new HiddenForm( form, owner, taskSource );
Application.Run( hiddenForm );
Application.ExitThread();
}
public class DialogHandle : IWin32Window
{
public IntPtr Handle { get; set; }
public DialogHandle( IntPtr handle )
=> Handle = handle;
}
public class HiddenForm : Form
{
private readonly CommonDialog _form;
private readonly IWin32Window _owner;
private readonly TaskCompletionSource< DialogResult > _taskSource;
public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
{ {
_form = form; var result = _form.ShowDialog( _owner );
_owner = owner; _taskSource.SetResult( result );
_taskSource = taskSource; }
catch( Exception e )
Opacity = 0; {
FormBorderStyle = FormBorderStyle.None; _taskSource.SetException( e );
ShowInTaskbar = false;
Size = new Size( 0, 0 );
Shown += HiddenForm_Shown;
} }
private void HiddenForm_Shown( object? sender, EventArgs _ ) Close();
{
Hide();
try
{
var result = _form.ShowDialog( _owner );
_taskSource.SetResult( result );
}
catch( Exception e )
{
_taskSource.SetException( e );
}
Close();
}
} }
} }
} }

View file

@ -1,14 +0,0 @@
using System;
using Dalamud.Logging;
namespace Penumbra.Util
{
public static class GeneralUtil
{
public static void PrintDebugAddress( string name, IntPtr address )
{
var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64();
PluginLog.Debug( "{Name} found at 0x{Address:X16}, +0x{Offset:X}", name, address.ToInt64(), address.ToInt64() - module );
}
}
}

View file

@ -1,12 +0,0 @@
using System.IO;
namespace Penumbra.Util
{
public static class MemoryStreamExtensions
{
public static void Write( this MemoryStream stream, byte[] data )
{
stream.Write( data, 0, data.Length );
}
}
}

View file

@ -16,7 +16,6 @@ public static class ModelChanger
public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl"; public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl";
public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled); public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled);
public static bool ValidStrings( string from, string to ) public static bool ValidStrings( string from, string to )
=> from.Length != 0 => from.Length != 0
&& to.Length != 0 && to.Length != 0
@ -40,8 +39,8 @@ public static class ModelChanger
try try
{ {
var data = File.ReadAllBytes( file.FullName ); var data = File.ReadAllBytes( file.FullName );
var mdlFile = new MdlFile( data ); var mdlFile = new MdlFile( data );
Func< string, bool > compare = MaterialRegex.IsMatch; Func< string, bool > compare = MaterialRegex.IsMatch;
if( from.Length > 0 ) if( from.Length > 0 )
{ {
@ -53,9 +52,9 @@ public static class ModelChanger
var replaced = 0; var replaced = 0;
for( var i = 0; i < mdlFile.Materials.Length; ++i ) for( var i = 0; i < mdlFile.Materials.Length; ++i )
{ {
if( compare(mdlFile.Materials[i]) ) if( compare( mdlFile.Materials[ i ] ) )
{ {
mdlFile.Materials[i] = to; mdlFile.Materials[ i ] = to;
++replaced; ++replaced;
} }
} }

View file

@ -3,432 +3,405 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using Lumina;
using Lumina.Data.Structs; using Lumina.Data.Structs;
namespace Penumbra.Util namespace Penumbra.Util;
public class PenumbraSqPackStream : IDisposable
{ {
public class PenumbraSqPackStream : IDisposable public Stream BaseStream { get; protected set; }
protected BinaryReader Reader { get; set; }
public PenumbraSqPackStream( FileInfo file )
: this( file.OpenRead() )
{ }
public PenumbraSqPackStream( Stream stream )
{ {
public Stream BaseStream { get; protected set; } BaseStream = stream;
Reader = new BinaryReader( BaseStream );
}
protected BinaryReader Reader { get; set; } public SqPackHeader GetSqPackHeader()
{
BaseStream.Position = 0;
public PenumbraSqPackStream( FileInfo file ) return Reader.ReadStructure< SqPackHeader >();
: this( file.OpenRead() ) }
{ }
public PenumbraSqPackStream( Stream stream ) public SqPackFileInfo GetFileMetadata( long offset )
{ {
BaseStream = stream; BaseStream.Position = offset;
Reader = new BinaryReader( BaseStream );
}
public SqPackHeader GetSqPackHeader() return Reader.ReadStructure< SqPackFileInfo >();
{ }
BaseStream.Position = 0;
return Reader.ReadStructure< SqPackHeader >(); public T ReadFile< T >( long offset ) where T : PenumbraFileResource
} {
using var ms = new MemoryStream();
public SqPackFileInfo GetFileMetadata( long offset ) BaseStream.Position = offset;
var fileInfo = Reader.ReadStructure< SqPackFileInfo >();
var file = Activator.CreateInstance< T >();
// check if we need to read the extended model header or just default to the standard file header
if( fileInfo.Type == FileType.Model )
{ {
BaseStream.Position = offset; BaseStream.Position = offset;
return Reader.ReadStructure< SqPackFileInfo >(); var modelFileInfo = Reader.ReadStructure< ModelBlock >();
file.FileInfo = new PenumbraFileInfo
{
HeaderSize = modelFileInfo.Size,
Type = modelFileInfo.Type,
BlockCount = modelFileInfo.UsedNumberOfBlocks,
RawFileSize = modelFileInfo.RawFileSize,
Offset = offset,
// todo: is this useful?
ModelBlock = modelFileInfo,
};
}
else
{
file.FileInfo = new PenumbraFileInfo
{
HeaderSize = fileInfo.Size,
Type = fileInfo.Type,
BlockCount = fileInfo.NumberOfBlocks,
RawFileSize = fileInfo.RawFileSize,
Offset = offset,
};
} }
public T ReadFile< T >( long offset ) where T : PenumbraFileResource switch( fileInfo.Type )
{ {
using var ms = new MemoryStream(); case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
BaseStream.Position = offset; case FileType.Standard:
ReadStandardFile( file, ms );
break;
var fileInfo = Reader.ReadStructure< SqPackFileInfo >(); case FileType.Model:
var file = Activator.CreateInstance< T >(); ReadModelFile( file, ms );
break;
// check if we need to read the extended model header or just default to the standard file header case FileType.Texture:
if( fileInfo.Type == FileType.Model ) ReadTextureFile( file, ms );
{ break;
BaseStream.Position = offset;
var modelFileInfo = Reader.ReadStructure< ModelBlock >(); default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
file.FileInfo = new PenumbraFileInfo
{
HeaderSize = modelFileInfo.Size,
Type = modelFileInfo.Type,
BlockCount = modelFileInfo.UsedNumberOfBlocks,
RawFileSize = modelFileInfo.RawFileSize,
Offset = offset,
// todo: is this useful?
ModelBlock = modelFileInfo,
};
}
else
{
file.FileInfo = new PenumbraFileInfo
{
HeaderSize = fileInfo.Size,
Type = fileInfo.Type,
BlockCount = fileInfo.NumberOfBlocks,
RawFileSize = fileInfo.RawFileSize,
Offset = offset,
};
}
switch( fileInfo.Type )
{
case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
case FileType.Standard:
ReadStandardFile( file, ms );
break;
case FileType.Model:
ReadModelFile( file, ms );
break;
case FileType.Texture:
ReadTextureFile( file, ms );
break;
default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
}
file.Data = ms.ToArray();
if( file.Data.Length != file.FileInfo.RawFileSize )
{
Debug.WriteLine( "Read data size does not match file size." );
}
file.FileStream = new MemoryStream( file.Data, false );
file.Reader = new BinaryReader( file.FileStream );
file.FileStream.Position = 0;
file.LoadFile();
return file;
} }
private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms ) file.Data = ms.ToArray();
if( file.Data.Length != file.FileInfo.RawFileSize )
{ {
var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount ); Debug.WriteLine( "Read data size does not match file size." );
foreach( var block in blocks )
{
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
}
// reset position ready for reading
ms.Position = 0;
} }
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms ) file.FileStream = new MemoryStream( file.Data, false );
file.Reader = new BinaryReader( file.FileStream );
file.FileStream.Position = 0;
file.LoadFile();
return file;
}
private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms )
{
var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount );
foreach( var block in blocks )
{ {
var mdlBlock = resource.FileInfo!.ModelBlock; ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize; }
// 1/1/3/3/3 stack/runtime/vertex/egeo/index // reset position ready for reading
// TODO: consider testing if this is more reliable than the Explorer method ms.Position = 0;
// of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2] }
// i don't want to move this to that method right now, because i know sometimes the index is 0
// but it seems to work fine in explorer... private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
int totalBlocks = mdlBlock.StackBlockNum; {
totalBlocks += mdlBlock.RuntimeBlockNum; var mdlBlock = resource.FileInfo!.ModelBlock;
for( var i = 0; i < 3; i++ ) var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
// 1/1/3/3/3 stack/runtime/vertex/egeo/index
// TODO: consider testing if this is more reliable than the Explorer method
// of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2]
// i don't want to move this to that method right now, because i know sometimes the index is 0
// but it seems to work fine in explorer...
int totalBlocks = mdlBlock.StackBlockNum;
totalBlocks += mdlBlock.RuntimeBlockNum;
for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.VertexBufferBlockNum[ i ];
}
for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
}
for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
}
var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks );
var currentBlock = 0;
var vertexDataOffsets = new int[3];
var indexDataOffsets = new int[3];
var vertexBufferSizes = new int[3];
var indexBufferSizes = new int[3];
ms.Seek( 0x44, SeekOrigin.Begin );
Reader.Seek( baseOffset + mdlBlock.StackOffset );
var stackStart = ms.Position;
for( var i = 0; i < mdlBlock.StackBlockNum; i++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
var stackEnd = ms.Position;
var stackSize = ( int )( stackEnd - stackStart );
Reader.Seek( baseOffset + mdlBlock.RuntimeOffset );
var runtimeStart = ms.Position;
for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
var runtimeEnd = ms.Position;
var runtimeSize = ( int )( runtimeEnd - runtimeStart );
for( var i = 0; i < 3; i++ )
{
if( mdlBlock.VertexBufferBlockNum[ i ] != 0 )
{ {
totalBlocks += mdlBlock.VertexBufferBlockNum[ i ]; var currentVertexOffset = ( int )ms.Position;
} if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
}
for( var i = 0; i < 3; i++ )
{
totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
}
var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks );
var currentBlock = 0;
var vertexDataOffsets = new int[3];
var indexDataOffsets = new int[3];
var vertexBufferSizes = new int[3];
var indexBufferSizes = new int[3];
ms.Seek( 0x44, SeekOrigin.Begin );
Reader.Seek( baseOffset + mdlBlock.StackOffset );
var stackStart = ms.Position;
for( var i = 0; i < mdlBlock.StackBlockNum; i++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
var stackEnd = ms.Position;
var stackSize = ( int )( stackEnd - stackStart );
Reader.Seek( baseOffset + mdlBlock.RuntimeOffset );
var runtimeStart = ms.Position;
for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
var runtimeEnd = ms.Position;
var runtimeSize = ( int )( runtimeEnd - runtimeStart );
for( var i = 0; i < 3; i++ )
{
if( mdlBlock.VertexBufferBlockNum[ i ] != 0 )
{ {
var currentVertexOffset = ( int )ms.Position; vertexDataOffsets[ i ] = currentVertexOffset;
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] ) }
{ else
vertexDataOffsets[ i ] = currentVertexOffset; {
} vertexDataOffsets[ i ] = 0;
else
{
vertexDataOffsets[ i ] = 0;
}
Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] );
for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ )
{
var lastPos = Reader.BaseStream.Position;
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
} }
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 ) Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] );
for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ )
{ {
for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ ) var lastPos = Reader.BaseStream.Position;
{ vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
var lastPos = Reader.BaseStream.Position; Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
ReadFileBlock( ms ); currentBlock++;
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
}
if( mdlBlock.IndexBufferBlockNum[ i ] != 0 )
{
var currentIndexOffset = ( int )ms.Position;
if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] )
{
indexDataOffsets[ i ] = currentIndexOffset;
}
else
{
indexDataOffsets[ i ] = 0;
}
// i guess this is only needed in the vertex area, for i = 0
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
{
var lastPos = Reader.BaseStream.Position;
indexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
} }
} }
ms.Seek( 0, SeekOrigin.Begin ); if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
ms.Write( BitConverter.GetBytes( mdlBlock.Version ) );
ms.Write( BitConverter.GetBytes( stackSize ) );
ms.Write( BitConverter.GetBytes( runtimeSize ) );
ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) );
ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) );
for( var i = 0; i < 3; i++ )
{ {
ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) ); for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ )
{
var lastPos = Reader.BaseStream.Position;
ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
} }
for( var i = 0; i < 3; i++ ) if( mdlBlock.IndexBufferBlockNum[ i ] != 0 )
{ {
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) ); var currentIndexOffset = ( int )ms.Position;
} if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] )
{
indexDataOffsets[ i ] = currentIndexOffset;
}
else
{
indexDataOffsets[ i ] = 0;
}
for( var i = 0; i < 3; i++ ) // i guess this is only needed in the vertex area, for i = 0
{ // Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
}
for( var i = 0; i < 3; i++ ) for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
{ {
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) ); var lastPos = Reader.BaseStream.Position;
indexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
} }
ms.Write( new[] { mdlBlock.NumLods } );
ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) );
ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) );
ms.Write( new byte[] { 0 } );
} }
private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms ) ms.Seek( 0, SeekOrigin.Begin );
ms.Write( BitConverter.GetBytes( mdlBlock.Version ) );
ms.Write( BitConverter.GetBytes( stackSize ) );
ms.Write( BitConverter.GetBytes( runtimeSize ) );
ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) );
ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) );
for( var i = 0; i < 3; i++ )
{ {
var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount ); ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) );
}
// if there is a mipmap header, the comp_offset for( var i = 0; i < 3; i++ )
// will not be 0 {
var mipMapSize = blocks[ 0 ].CompressedOffset; ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
if( mipMapSize != 0 ) }
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
}
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) );
}
ms.Write( new[] { mdlBlock.NumLods } );
ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) );
ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) );
ms.Write( new byte[] { 0 } );
}
private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms )
{
var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount );
// if there is a mipmap header, the comp_offset
// will not be 0
var mipMapSize = blocks[ 0 ].CompressedOffset;
if( mipMapSize != 0 )
{
var originalPos = BaseStream.Position;
BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ms.Write( Reader.ReadBytes( ( int )mipMapSize ) );
BaseStream.Position = originalPos;
}
// i is for texture blocks, j is 'data blocks'...
for( byte i = 0; i < blocks.Count; i++ )
{
// start from comp_offset
var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ReadFileBlock( runningBlockTotal, ms, true );
for( var j = 1; j < blocks[ i ].BlockCount; j++ )
{ {
var originalPos = BaseStream.Position; runningBlockTotal += ( uint )Reader.ReadInt16();
BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ms.Write( Reader.ReadBytes( ( int )mipMapSize ) );
BaseStream.Position = originalPos;
}
// i is for texture blocks, j is 'data blocks'...
for( byte i = 0; i < blocks.Count; i++ )
{
// start from comp_offset
var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ReadFileBlock( runningBlockTotal, ms, true ); ReadFileBlock( runningBlockTotal, ms, true );
for( var j = 1; j < blocks[ i ].BlockCount; j++ )
{
runningBlockTotal += ( uint )Reader.ReadInt16();
ReadFileBlock( runningBlockTotal, ms, true );
}
// unknown
Reader.ReadInt16();
} }
// unknown
Reader.ReadInt16();
} }
}
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false ) protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
=> ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition ); => ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false ) protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false )
{
var originalPosition = BaseStream.Position;
BaseStream.Position = offset;
var blockHeader = Reader.ReadStructure< DatBlockHeader >();
// uncompressed block
if( blockHeader.CompressedSize == 32000 )
{ {
var originalPosition = BaseStream.Position; dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
BaseStream.Position = offset;
var blockHeader = Reader.ReadStructure< DatBlockHeader >();
// uncompressed block
if( blockHeader.CompressedSize == 32000 )
{
dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
return blockHeader.UncompressedSize;
}
var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize );
using( var compressedStream = new MemoryStream( data ) )
{
using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress );
zlibStream.CopyTo( dest );
}
if( resetPosition )
{
BaseStream.Position = originalPosition;
}
return blockHeader.UncompressedSize; return blockHeader.UncompressedSize;
} }
public void Dispose() var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize );
using( var compressedStream = new MemoryStream( data ) )
{ {
Reader?.Dispose(); using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress );
zlibStream.CopyTo( dest );
} }
public class PenumbraFileInfo if( resetPosition )
{ {
public uint HeaderSize; BaseStream.Position = originalPosition;
public FileType Type;
public uint RawFileSize;
public uint BlockCount;
public long Offset { get; internal set; }
public ModelBlock ModelBlock { get; internal set; }
} }
public class PenumbraFileResource return blockHeader.UncompressedSize;
}
public void Dispose()
{
Reader.Dispose();
}
public class PenumbraFileInfo
{
public uint HeaderSize;
public FileType Type;
public uint RawFileSize;
public uint BlockCount;
public long Offset { get; internal set; }
public ModelBlock ModelBlock { get; internal set; }
}
public class PenumbraFileResource
{
public PenumbraFileResource()
{ }
public PenumbraFileInfo? FileInfo { get; internal set; }
public byte[] Data { get; internal set; } = new byte[0];
public MemoryStream? FileStream { get; internal set; }
public BinaryReader? Reader { get; internal set; }
/// <summary>
/// Called once the files are read out from the dats. Used to further parse the file into usable data structures.
/// </summary>
public virtual void LoadFile()
{ {
public PenumbraFileResource() // this function is intentionally left blank
{ }
public PenumbraFileInfo? FileInfo { get; internal set; }
public byte[] Data { get; internal set; } = new byte[0];
public Span< byte > DataSpan
=> Data.AsSpan();
public MemoryStream? FileStream { get; internal set; }
public BinaryReader? Reader { get; internal set; }
public ParsedFilePath? FilePath { get; internal set; }
/// <summary>
/// Called once the files are read out from the dats. Used to further parse the file into usable data structures.
/// </summary>
public virtual void LoadFile()
{
// this function is intentionally left blank
}
public virtual void SaveFile( string path )
{
File.WriteAllBytes( path, Data );
}
public string GetFileHash()
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = sha256.ComputeHash( Data );
var sb = new StringBuilder();
foreach( var b in hash )
{
sb.Append( $"{b:x2}" );
}
return sb.ToString();
}
}
[StructLayout( LayoutKind.Sequential )]
private struct DatBlockHeader
{
public uint Size;
public uint unknown1;
public uint CompressedSize;
public uint UncompressedSize;
};
[StructLayout( LayoutKind.Sequential )]
private struct LodBlock
{
public uint CompressedOffset;
public uint CompressedSize;
public uint DecompressedSize;
public uint BlockOffset;
public uint BlockCount;
} }
} }
[StructLayout( LayoutKind.Sequential )]
private struct DatBlockHeader
{
public uint Size;
public uint unknown1;
public uint CompressedSize;
public uint UncompressedSize;
};
[StructLayout( LayoutKind.Sequential )]
private struct LodBlock
{
public uint CompressedOffset;
public uint CompressedSize;
public uint DecompressedSize;
public uint BlockOffset;
public uint BlockCount;
}
} }

View file

@ -3,44 +3,43 @@ using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace Penumbra.Util namespace Penumbra.Util;
public class SingleOrArrayConverter< T > : JsonConverter
{ {
public class SingleOrArrayConverter< T > : JsonConverter public override bool CanConvert( Type objectType )
=> objectType == typeof( HashSet< T > );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{ {
public override bool CanConvert( Type objectType ) var token = JToken.Load( reader );
=> objectType == typeof( HashSet< T > );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) if( token.Type == JTokenType.Array )
{ {
var token = JToken.Load( reader ); return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
if( token.Type == JTokenType.Array )
{
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
}
var tmp = token.ToObject< T >();
return tmp != null
? new HashSet< T > { tmp }
: new HashSet< T >();
} }
public override bool CanWrite var tmp = token.ToObject< T >();
=> true; return tmp != null
? new HashSet< T > { tmp }
: new HashSet< T >();
}
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) public override bool CanWrite
=> true;
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
{
writer.WriteStartArray();
if( value != null )
{ {
writer.WriteStartArray(); var v = ( HashSet< T > )value;
if( value != null ) foreach( var val in v )
{ {
var v = ( HashSet< T > )value; serializer.Serialize( writer, val?.ToString() );
foreach( var val in v )
{
serializer.Serialize( writer, val?.ToString() );
}
} }
writer.WriteEndArray();
} }
writer.WriteEndArray();
} }
} }

View file

@ -2,67 +2,66 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace Penumbra.Util namespace Penumbra.Util;
public static class StringPathExtensions
{ {
public static class StringPathExtensions private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars());
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
{ {
private static readonly HashSet< char > Invalid = new( Path.GetInvalidFileNameChars() ); StringBuilder sb = new(s.Length);
foreach( var c in s )
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
{ {
StringBuilder sb = new( s.Length ); if( Invalid.Contains( c ) )
foreach( var c in s )
{ {
if( Invalid.Contains( c ) ) sb.Append( replacement );
{ }
sb.Append( replacement ); else
} {
else sb.Append( c );
{
sb.Append( c );
}
} }
return sb.ToString();
} }
public static string RemoveInvalidPathSymbols( this string s ) return sb.ToString();
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) ); }
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" ) public static string RemoveInvalidPathSymbols( this string s )
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" )
{
StringBuilder sb = new(s.Length);
foreach( var c in s )
{ {
StringBuilder sb = new( s.Length ); if( c >= 128 )
foreach( var c in s )
{ {
if( c >= 128 ) sb.Append( replacement );
{ }
sb.Append( replacement ); else
} {
else sb.Append( c );
{
sb.Append( c );
}
} }
return sb.ToString();
} }
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" ) return sb.ToString();
{ }
StringBuilder sb = new( s.Length );
foreach( var c in s )
{
if( c >= 128 || Invalid.Contains( c ) )
{
sb.Append( replacement );
}
else
{
sb.Append( c );
}
}
return sb.ToString(); public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
{
StringBuilder sb = new(s.Length);
foreach( var c in s )
{
if( c >= 128 || Invalid.Contains( c ) )
{
sb.Append( replacement );
}
else
{
sb.Append( c );
}
} }
return sb.ToString();
} }
} }

View file

@ -1,34 +1,33 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace Penumbra.Util namespace Penumbra.Util;
public static class TempFile
{ {
public static class TempFile public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" )
{ {
public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" ) const uint maxTries = 15;
for( var i = 0; i < maxTries; ++i )
{ {
const uint maxTries = 15; var name = Path.GetRandomFileName();
for( var i = 0; i < maxTries; ++i ) var path = new FileInfo( Path.Combine( baseDir.FullName,
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
if( !path.Exists )
{ {
var name = Path.GetRandomFileName(); return path;
var path = new FileInfo( Path.Combine( baseDir.FullName,
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
if( !path.Exists )
{
return path;
}
} }
throw new IOException();
} }
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" ) throw new IOException();
{ }
var fileName = TempFileName( baseDir, suffix );
using var stream = fileName.OpenWrite(); public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
stream.Write( data, 0, data.Length ); {
fileName.Refresh(); var fileName = TempFileName( baseDir, suffix );
return fileName; using var stream = fileName.OpenWrite();
} stream.Write( data, 0, data.Length );
fileName.Refresh();
return fileName;
} }
} }