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.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Util;
namespace Penumbra.GameData.ByteString;
[JsonConverter( typeof( FullPathConverter ) )]
public readonly struct FullPath : IComparable, IEquatable< FullPath >
{
public readonly string FullName;
public readonly Utf8String InternalName;
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() ) )
{ }
@ -19,10 +23,11 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
: this( file.FullName )
{ }
public FullPath( string 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 );
}
@ -35,9 +40,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
public string Name
=> 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 ) )
{
return false;
@ -45,13 +50,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
var substring = InternalName.Substring( dir.FullName.Length + 1 );
path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) );
path = new Utf8GamePath( substring );
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 ) )
{
return false;
@ -59,7 +64,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
var substring = InternalName.Substring( dir.FullName.Length + 1 );
path = new NewRelPath( substring );
path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) );
return true;
}
@ -88,9 +93,35 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
return InternalName.Equals( other.InternalName );
}
public bool IsRooted
=> new Utf8GamePath( InternalName ).IsRooted();
public override int GetHashCode()
=> InternalName.Crc32;
public override string ToString()
=> 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Util;
namespace Penumbra.GameData.ByteString;
// NewGamePath wrap some additional validity checking around Utf8String,
// provide some filesystem helpers, and conversion to Json.
[JsonConverter( typeof( NewGamePathConverter ) )]
public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable
[JsonConverter( typeof( Utf8GamePathConverter ) )]
public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable
{
public const int MaxGamePathLength = 256;
public readonly Utf8String Path;
public static readonly NewGamePath Empty = new(Utf8String.Empty);
public readonly Utf8String Path;
public static readonly Utf8GamePath Empty = new(Utf8String.Empty);
internal NewGamePath( Utf8String s )
internal Utf8GamePath( Utf8String s )
=> Path = s;
public int Length
@ -25,16 +26,16 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
public bool IsEmpty
=> Path.IsEmpty;
public NewGamePath ToLower()
public Utf8GamePath ToLower()
=> 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 );
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 );
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 initial slashes either, since they are assumed to be by choice.
// 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;
if( !utf.IsAscii || utf.Length > MaxGamePathLength )
@ -51,14 +52,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
return false;
}
path = new NewGamePath( lower ? utf.AsciiToLower() : utf );
path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf );
return true;
}
public NewGamePath Clone()
public Utf8GamePath 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;
if( s.IsNullOrEmpty() )
@ -83,11 +87,11 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
return false;
}
path = new NewGamePath( ascii );
path = new Utf8GamePath( ascii );
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;
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 );
}
public bool Equals( NewGamePath other )
public bool Equals( Utf8GamePath other )
=> Path.Equals( other.Path );
public override int GetHashCode()
=> Path.GetHashCode();
public int CompareTo( NewGamePath other )
public int CompareTo( Utf8GamePath other )
=> Path.CompareTo( other.Path );
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[ 1 ] == ':';
private class NewGamePathConverter : JsonConverter
public class Utf8GamePathConverter : JsonConverter
{
public override bool CanConvert( Type objectType )
=> objectType == typeof( NewGamePath );
=> objectType == typeof( Utf8GamePath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{
var token = JToken.Load( reader ).ToString();
return FromString( token, out var p, true )
? 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
@ -150,10 +154,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
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() );
}
}
}
public GamePath ToGamePath()
=> GamePath.GenerateUnchecked( ToString() );
}

View file

@ -1,23 +1,28 @@
using System;
using System.IO;
using Dalamud.Utility;
using Microsoft.VisualBasic.CompilerServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Penumbra.GameData.ByteString;
[JsonConverter( typeof( NewRelPathConverter ) )]
public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable
[JsonConverter( typeof( Utf8RelPathConverter ) )]
public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable
{
public const int MaxRelPathLength = 250;
public readonly Utf8String Path;
public static readonly NewRelPath Empty = new(Utf8String.Empty);
public readonly Utf8String Path;
public static readonly Utf8RelPath Empty = new(Utf8String.Empty);
internal NewRelPath( Utf8String path )
internal Utf8RelPath( Utf8String 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;
if( s.IsNullOrEmpty() )
@ -42,11 +47,11 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
return false;
}
path = new NewRelPath( ascii );
path = new Utf8RelPath( ascii );
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;
if( !file.FullName.StartsWith( baseDir.FullName ) )
@ -58,7 +63,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
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;
if( !file.FullName.StartsWith( baseDir.FullName ) )
@ -70,10 +75,10 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
return FromString( substring, out path );
}
public NewRelPath( NewGamePath gamePath )
public Utf8RelPath( Utf8GamePath gamePath )
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
public unsafe NewGamePath ToGamePath( int skipFolders = 0 )
public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 )
{
var idx = 0;
while( skipFolders > 0 )
@ -82,7 +87,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
--skipFolders;
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.AsciiToLowerInPlace( ptr, length );
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 );
public bool Equals( NewRelPath other )
public bool Equals( Utf8RelPath other )
=> Path.Equals( other.Path );
public override string ToString()
@ -106,17 +111,17 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
public void Dispose()
=> Path.Dispose();
private class NewRelPathConverter : JsonConverter
public class Utf8RelPathConverter : JsonConverter
{
public override bool CanConvert( Type objectType )
=> objectType == typeof( NewRelPath );
=> objectType == typeof( Utf8RelPath );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
{
var token = JToken.Load( reader ).ToString();
return FromString( token, out var 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
@ -124,7 +129,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
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() );
}

View file

@ -75,6 +75,19 @@ public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, ICompa
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 )
{
if( chars.Length > Length )

View file

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

View file

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

View file

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using Dalamud.Configuration;
using Dalamud.Logging;
@ -14,13 +12,19 @@ public class Configuration : IPluginConfiguration
public int Version { get; set; } = CurrentVersion;
public bool IsEnabled { get; set; } = true;
public bool EnableMods { get; set; } = true;
#if DEBUG
public bool DebugMode { get; set; } = true;
#else
public bool DebugMode { get; set; } = false;
#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 ShowAdvanced { get; set; }
public bool DisableFileSystemNotifications { get; set; }

View file

@ -8,14 +8,15 @@ using Dalamud.Game.Gui;
using Dalamud.Interface;
using Dalamud.IoC;
using Dalamud.Plugin;
// 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
[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 ObjectTable Objects { 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 Penumbra.Structs;
using Penumbra.Mod;
namespace Penumbra.Importer.Models
{

View file

@ -6,10 +6,10 @@ using System.Text;
using Dalamud.Logging;
using ICSharpCode.SharpZipLib.Zip;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Importer.Models;
using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util;
using FileMode = System.IO.FileMode;
@ -336,14 +336,18 @@ internal class TexToolsImport
{
OptionName = opt.Name!,
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! );
if( optDir.Exists )
{
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* ManipulatedResource;
public NewGamePath OriginalPath;
public Utf8GamePath OriginalPath;
public FullPath ManipulatedPath;
public ResourceCategory Category;
public object? ResolverInfo;
@ -44,7 +44,7 @@ public unsafe partial class ResourceLoader
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 )
{
@ -188,11 +188,11 @@ public unsafe partial class ResourceLoader
}
}
// Logging functions for EnableLogging.
private static void LogPath( NewGamePath path, bool synchronous )
=> PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
// Logging functions for EnableFullLogging.
private static void LogPath( Utf8GamePath path, bool synchronous )
=> 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();
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 )
=> PluginLog.Information( success
? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}"
: $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." );
? $"[ResourceLoader] Loaded {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" )]
private static void CompareHash( int local, int game, NewGamePath path )
private static void CompareHash( int local, int game, Utf8GamePath path )
{
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,
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." );
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 );
}
var valid = NewGamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
byte ret;
// 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.
@ -151,11 +151,10 @@ public unsafe partial class ResourceLoader
}
// 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( gamePath );
return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null );
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
return( resolved, null );
}
private void DisposeHooks()

View file

@ -4,8 +4,6 @@ using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Util;
namespace Penumbra.Interop;
@ -69,7 +67,7 @@ public unsafe partial class ResourceLoader
: 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 )
{

View file

@ -17,7 +17,7 @@ public unsafe partial class ResourceLoader : IDisposable
// Events can be used to make smarter logging.
public bool IsLoggingEnabled { get; private set; }
public void EnableLogging()
public void EnableFullLogging()
{
if( IsLoggingEnabled )
{
@ -31,7 +31,7 @@ public unsafe partial class ResourceLoader : IDisposable
EnableHooks();
}
public void DisableLogging()
public void DisableFullLogging()
{
if( !IsLoggingEnabled )
{
@ -99,13 +99,13 @@ public unsafe partial class ResourceLoader : IDisposable
}
// 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;
// Event fired whenever a resource is returned.
// 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.
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath,
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
object? resolveData );
public event ResourceLoadedDelegate? ResourceLoaded;
@ -118,10 +118,11 @@ public unsafe partial class ResourceLoader : IDisposable
public event FileLoadedDelegate? FileLoaded;
// 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()
{
DisableFullLogging();
DisposeHooks();
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 Penumbra.Structs;
namespace Penumbra.Interop.Structs;

View file

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

View file

@ -3,84 +3,81 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Dalamud.Plugin;
using Newtonsoft.Json.Linq;
using Penumbra.Mod;
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;
}
config.ModDirectory = config.CurrentCollection;
config.CurrentCollection = "Default";
config.DefaultCollection = "Default";
config.Version = 1;
ResettleCollectionJson( config );
return;
}
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" ) );
if( !collectionJson.Exists )
{
return;
}
return;
}
var defaultCollection = new ModCollection();
var defaultCollectionFile = defaultCollection.FileName();
if( defaultCollectionFile.Exists )
{
return;
}
var defaultCollection = new ModCollection();
var defaultCollectionFile = defaultCollection.FileName();
if( defaultCollectionFile.Exists )
{
return;
}
try
{
var text = File.ReadAllText( collectionJson.FullName );
var data = JArray.Parse( text );
try
{
var text = File.ReadAllText( collectionJson.FullName );
var data = JArray.Parse( text );
var maxPriority = 0;
foreach( var setting in data.Cast< JObject >() )
var maxPriority = 0;
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" ]!;
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()
{
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();
Enabled = enabled,
Priority = priority,
Settings = settings!,
};
defaultCollection.Settings.Add( modName, save );
maxPriority = Math.Max( maxPriority, priority );
}
catch( Exception e )
if( !config.InvertModListOrder )
{
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
throw;
foreach( var setting in defaultCollection.Settings.Values )
{
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.IO;
using Penumbra.GameData.Util;
using Penumbra.Util;
using Penumbra.GameData.ByteString;
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)
// and the resulting cache.
public class Mod
public ModSettings Settings { get; }
public ModData Data { get; }
public ModCache Cache { get; }
public Mod( ModSettings settings, ModData data )
{
public ModSettings Settings { get; }
public ModData Data { get; }
public ModCache Cache { get; }
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;
Settings = settings;
Data = data;
Cache = new ModCache();
}
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.Linq;
using Penumbra.GameData.Util;
using Penumbra.GameData.ByteString;
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 class ModCache
public Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
public void AddConflict( Mod precedingMod, Utf8GamePath gamePath )
{
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
public void AddConflict( Mod precedingMod, GamePath gamePath )
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
{
conflicts.Files.Add( gamePath );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
}
conflicts.Files.Add( gamePath );
}
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
else
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
{
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;
} );
Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() );
}
}
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.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Importer;
using Penumbra.Mods;
using Penumbra.Structs;
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";
private const string Required = "Required";
_hasher ??= SHA256.Create();
return _hasher;
}
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
{
_baseDir = baseDir;
_mod = mod;
BuildDict();
}
private readonly DirectoryInfo _baseDir;
private readonly ModMeta _mod;
private SHA256? _hasher;
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
private SHA256 Sha()
private void BuildDict()
{
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
_hasher ??= SHA256.Create();
return _hasher;
}
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;
if( _filesBySize.TryGetValue( fileLength, out var files ) )
{
var fileLength = file.Length;
if( _filesBySize.TryGetValue( fileLength, out var files ) )
{
files.Add( file );
}
else
{
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
}
files.Add( file );
}
else
{
_filesBySize[ fileLength ] = new List< FileInfo > { file };
}
}
}
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
{
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName );
return newDir;
}
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
{
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
}
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
{
Penumbra.ModManager.AddMod( newDir );
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
newMod.Move( newSortOrder );
newMod.ComputeChangedItems();
ModFileSystem.InvokeChange();
return newMod;
}
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
{
Penumbra.ModManager.AddMod( newDir );
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
newMod.Move( newSortOrder );
newMod.ComputeChangedItems();
ModFileSystem.InvokeChange();
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,
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 )
var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
unseenPaths.Remove( oldPath );
if( File.Exists( oldPath ) )
{
var oldPath = Path.Combine( mod.BasePath.FullName, fileName );
unseenPaths.Remove( oldPath );
if( File.Exists( oldPath ) )
foreach( var path in paths )
{
foreach( var path in paths )
{
var newPath = Path.Combine( newDir.FullName, path );
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;
}
var newPath = Path.Combine( newDir.FullName, path.ToString() );
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
File.Copy( oldPath, newPath, true );
}
}
}
CleanUpDuplicates( mod );
ClearEmptySubDirectories( dedup._baseDir );
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.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 );
RelPath relName2 = new( f2, _baseDir );
var inOption1 = false;
var inOption2 = false;
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
foreach( var option in group.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();
}
CreateModSplit( unseenPaths, mod, group, option );
}
}
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false )
if( unseenPaths.Count == 0 )
{
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 ) );
return;
}
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 );
if( requiredIdx >= 0 )
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 (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
{
if( value.Count == 2 )
{
var required = info.Options[ requiredIdx ];
foreach( var kvp in required.OptionFiles.ToArray() )
if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
{
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;
}
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,
Single = 1,
Multi = 2,
return;
}
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" ),
};
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
bool skipDuplicates = true )
foreach( var group in enumerator )
{
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;
}
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 )
{
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;
}
private static void RemoveUselessGroups( ModMeta meta )
try
{
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
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.
// 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 option in meta.Groups.Values.SelectMany( group => group.Options ) )
{
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;
HashSet< (RelPath, GamePath) > groupList = new();
foreach( var option in group.Options )
option.OptionFiles.Add( newRelPath, gamePaths );
option.OptionFiles.Remove( oldRelPath );
}
}
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();
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;
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
}
var newPath = new Dictionary< RelPath, GamePath >();
foreach( var (path, gamePath) in groupList )
if( firstOption )
{
var relPath = new RelPath( gamePath );
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 );
}
groupList = optionList;
}
else
{
groupList.IntersectWith( optionList );
}
firstOption = false;
}
RemoveUselessGroups( meta );
ClearEmptySubDirectories( baseDir );
var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
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();
ClearEmptySubDirectories( baseDir );
foreach( var groupDir in baseDir.EnumerateDirectories() )
var group = new OptionGroup
{
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,
SelectionType = SelectType.Single,
Options = new List< Option >(),
OptionDesc = string.Empty,
OptionName = optionDir.Name,
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
};
foreach( var optionDir in groupDir.EnumerateDirectories() )
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
var option = new Option
if( Utf8RelPath.FromFile( file, baseDir, out var rel )
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
{
OptionDesc = string.Empty,
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 );
option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
}
}
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)
collection.UpdateSetting(baseDir, meta, true);
if( group.Options.Any() )
{
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.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
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; }
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 );
get => _sortOrderName;
set => _sortOrderName = value.Replace( '/', '\\' );
}
// 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 string SortOrderPath
=> ParentFolder.FullName;
public string FullName
{
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 )
get
{
BasePath = basePath;
Meta = meta;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
SortOrder = new SortOrder( parentFolder, Meta.Name );
SortOrder.ParentFolder.AddMod( this );
ComputeChangedItems();
var path = SortOrderPath;
return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
}
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.IO;
using System.Linq;
using Penumbra.GameData.Util;
using Penumbra.Structs;
using Penumbra.Util;
using Penumbra.GameData.ByteString;
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 class ModFunctions
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
{
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();
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{
anyChanges |= settings.Remove( toRemove );
}
return anyChanges;
anyChanges |= settings.Remove( toRemove );
}
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;
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;
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
}
public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta )
if( !doNotAdd )
{
var ret = new HashSet< GamePath >();
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;
files.Add( relPath.ToGamePath() );
}
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,
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
ret.UnionWith( files );
}
}
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[ 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;
}
ret.Settings[ setting ] = 0;
}
else
{
foreach( var idx in namedSettings.Settings[ kvp.Key ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ kvp.Key ] |= 1 << idx;
}
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() );
ret.Settings[ setting ] = idx < 0 ? 0 : 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 Dalamud.Logging;
using Newtonsoft.Json;
using Penumbra.GameData.ByteString;
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 class ModMeta
public uint FileVersion { get; set; }
public string Name
{
public uint FileVersion { get; set; }
public string Name
get => _name;
set
{
get => _name;
set
{
_name = value;
LowerName = value.ToLowerInvariant();
}
_name = value;
LowerName = value.ToLowerInvariant();
}
}
private string _name = "Mod";
private string _name = "Mod";
[JsonIgnore]
public string LowerName { get; private set; } = "mod";
[JsonIgnore]
public string LowerName { get; private set; } = "mod";
private string _author = "";
private string _author = "";
public string Author
public string Author
{
get => _author;
set
{
get => _author;
set
{
_author = value;
LowerAuthor = value.ToLowerInvariant();
}
_author = value;
LowerAuthor = value.ToLowerInvariant();
}
}
[JsonIgnore]
public string LowerAuthor { get; private set; } = "";
[JsonIgnore]
public string LowerAuthor { get; private set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new();
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
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]
private int FileHash { get; set; }
[JsonIgnore]
private int FileHash { get; set; }
[JsonIgnore]
public bool HasGroupsWithConfig { get; private set; }
[JsonIgnore]
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;
}
public static ModMeta? LoadFromFile( FileInfo filePath )
if( newMeta.FileHash == FileHash )
{
try
{
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;
}
return false;
}
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;
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
return oldValue != HasGroupsWithConfig;
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;
}
public void SaveToFile( FileInfo filePath )
catch( Exception e )
{
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 );
var newHash = text.GetHashCode();
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}" );
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}" );
}
}
}

View file

@ -1,75 +1,73 @@
using System;
using System.Collections.Generic;
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 class ModSettings
public bool Enabled { get; set; }
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; }
public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
set => Settings = value;
}
// For backwards compatibility
private Dictionary< string, int > Conf
public ModSettings DeepCopy()
{
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,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
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 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()
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
};
return false;
}
public bool FixSpecificSetting( string name, ModMeta 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 ) );
}
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.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.
// 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
public int Priority { get; set; }
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
public void AddFromModSetting( ModSettings s, ModMeta meta )
{
public int Priority { get; set; }
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
Priority = s.Priority;
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;
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
foreach( var kvp in Settings )
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
continue;
}
var setting = s.Settings[ kvp.Key ];
if( info.SelectionType == SelectType.Single )
var setting = s.Settings[ kvp.Key ];
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
? 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 )
{
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.Linq;
using Dalamud.Logging;
using Penumbra.Interop;
using Penumbra.Mod;
using Penumbra.Util;
@ -250,16 +249,16 @@ public class CollectionManager
public bool CreateCharacterCollection( string characterName )
{
if( !CharacterCollection.ContainsKey( characterName ) )
if( CharacterCollection.ContainsKey( characterName ) )
{
CharacterCollection[ characterName ] = ModCollection.Empty;
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
Penumbra.Config.Save();
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
return true;
return false;
}
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 )
@ -299,7 +298,7 @@ public class CollectionManager
private bool LoadForcedCollection( Configuration config )
{
if( config.ForcedCollection == string.Empty )
if( config.ForcedCollection.Length == 0 )
{
ForcedCollection = ModCollection.Empty;
return false;
@ -320,7 +319,7 @@ public class CollectionManager
private bool LoadDefaultCollection( Configuration config )
{
if( config.DefaultCollection == string.Empty )
if( config.DefaultCollection.Length == 0 )
{
DefaultCollection = ModCollection.Empty;
return false;
@ -345,7 +344,7 @@ public class CollectionManager
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
{
Penumbra.PlayerWatcher.AddPlayerToWatch( player );
if( collectionName == string.Empty )
if( collectionName.Length == 0 )
{
CharacterCollection.Add( player, ModCollection.Empty );
}

View file

@ -1,255 +1,256 @@
using Dalamud.Plugin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Interop;
using Penumbra.Mod;
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.
// 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
public const string DefaultCollection = "Default";
public string Name { get; set; }
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 ModCollection()
public Mod.Mod GetMod( ModData mod )
{
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
{
Name = DefaultCollection;
Settings = new Dictionary< string, ModSettings >();
return ret;
}
public ModCollection( string name, Dictionary< string, ModSettings > settings )
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
Name = name;
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
return new Mod.Mod( settings, mod );
}
public Mod.Mod GetMod( ModData mod )
{
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
{
return ret;
}
var newSettings = ModSettings.DefaultSettings( mod.Meta );
Settings.Add( mod.BasePath.Name, newSettings );
Save();
return new Mod.Mod( newSettings, mod );
}
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 ) )
{
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 );
Settings.Add( mod.BasePath.Name, newSettings );
if( changedSettings )
{
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();
foreach( var s in removeList )
{
Settings.Remove( s.Key );
}
return removeList.Length > 0;
return;
}
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data )
if( clear )
{
Cache = new ModCollectionCache( Name, modDirectory );
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 );
settings.Settings.Clear();
}
public void ClearCache()
=> Cache = null;
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
if( settings.FixInvalidSettings( meta ) )
{
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
{
return;
}
Save();
}
}
if (clear)
settings.Settings.Clear();
if( settings.FixInvalidSettings( meta ) )
{
Save();
}
public void UpdateSetting( ModData mod )
=> UpdateSetting( mod.BasePath, mod.Meta, false );
public void UpdateSettings( bool forceSave )
{
if( Cache == null )
{
return;
}
public void UpdateSetting( ModData mod )
=> UpdateSetting( mod.BasePath, mod.Meta, false );
public void UpdateSettings( bool forceSave )
var changes = false;
foreach( var mod in Cache.AvailableMods.Values )
{
if( Cache == null )
{
return;
}
var changes = false;
foreach( var mod in Cache.AvailableMods.Values )
{
changes |= mod.FixSettings();
}
if( forceSave || changes )
{
Save();
}
changes |= mod.FixSettings();
}
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
if( forceSave || changes )
{
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name,
withMetaManipulations, activeCollection );
Cache ??= new ModCollectionCache( Name, modDir );
UpdateSettings( false );
Cache.CalculateEffectiveFileList();
if( withMetaManipulations )
Save();
}
}
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
{
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();
if( activeCollection )
{
Penumbra.ResidentResources.Reload();
}
Penumbra.ResidentResources.Reload();
}
}
}
[JsonIgnore]
public ModCollectionCache? Cache { get; private set; }
[JsonIgnore]
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." );
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}" );
}
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
return null;
}
private void SaveToFile( FileInfo file )
try
{
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}" );
}
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}" );
}
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 string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
public static readonly ModCollection Empty = new() { Name = "" };
return null;
}
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.Meta;
using Penumbra.Mod;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -20,17 +19,16 @@ namespace Penumbra.Mods;
public class ModCollectionCache
{
// Shared caches to avoid allocations.
private static readonly BitArray FileSeen = new(256);
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256);
private static readonly BitArray FileSeen = new(256);
private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256);
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
private readonly SortedList< string, object? > _changedItems = new();
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new();
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new();
public readonly HashSet< ulong > Checksums = new();
public readonly MetaManager MetaManipulations;
private readonly SortedList< string, object? > _changedItems = new();
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new();
public readonly HashSet< ulong > Checksums = new();
public readonly MetaManager MetaManipulations;
public IReadOnlyDictionary< string, object? > ChangedItems
{
@ -61,7 +59,6 @@ public class ModCollectionCache
public void CalculateEffectiveFileList()
{
ResolvedFiles.Clear();
SwappedFiles.Clear();
MissingFiles.Clear();
RegisteredFiles.Clear();
_changedItems.Clear();
@ -85,7 +82,7 @@ public class ModCollectionCache
private void SetChangedItems()
{
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 )
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
{
return;
}
@ -98,12 +95,7 @@ public class ModCollectionCache
var identifier = GameData.GameData.GetIdentifier();
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
{
identifier.Identify( _changedItems, resolved );
}
foreach( var swapped in SwappedFiles.Keys )
{
identifier.Identify( _changedItems, swapped );
identifier.Identify( _changedItems, resolved.ToGamePath() );
}
}
catch( Exception e )
@ -134,12 +126,12 @@ public class ModCollectionCache
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,
// so only add those files if it is disabled.
if( !Penumbra.Config.DisableSoundStreaming
&& gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) )
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
{
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 ) )
{
@ -187,9 +179,8 @@ public class ModCollectionCache
{
foreach( var (file, paths) in option.OptionFiles )
{
var fullPath = new FullPath( mod.Data.BasePath,
NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
var fullPath = new FullPath( mod.Data.BasePath, file );
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
if( idx < 0 )
{
AddMissingFile( fullPath );
@ -259,7 +250,7 @@ public class ModCollectionCache
{
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
{
AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO
AddFile( mod, gamePath, file );
}
else
{
@ -294,7 +285,7 @@ public class ModCollectionCache
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
{
RegisteredFiles.Add( key, mod );
SwappedFiles.Add( key, value );
ResolvedFiles.Add( key, value );
}
else
{
@ -341,54 +332,54 @@ public class ModCollectionCache
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 );
if( mod.Settings.Enabled )
{
CalculateEffectiveFileList();
if( mod.Data.Resources.MetaManipulations.Count > 0 )
{
UpdateMetaManipulations();
}
}
return;
}
AvailableMods.Remove( basePath.Name );
if( !mod.Settings.Enabled )
{
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 )
{
if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) )
if( AvailableMods.ContainsKey( data.BasePath.Name ) )
{
var newMod = new Mod.Mod( settings, data );
AvailableMods[ data.BasePath.Name ] = newMod;
return;
}
if( updateFileList && settings.Enabled )
{
CalculateEffectiveFileList();
if( data.Resources.MetaManipulations.Count > 0 )
{
UpdateMetaManipulations();
}
}
AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data );
if( !updateFileList || !settings.Enabled )
{
return;
}
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 ) )
{
return null;
}
if( candidate.FullName.Length >= 260 || !candidate.Exists )
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|| candidate.IsRooted && !candidate.Exists )
{
return null;
}
@ -396,9 +387,6 @@ public class ModCollectionCache
return candidate;
}
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
=> GetCandidateForGameFile( gameResourcePath );
}

View file

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

View file

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

View file

@ -18,22 +18,6 @@ using System.Linq;
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 string Name
@ -54,6 +38,7 @@ public class Penumbra : IDalamudPlugin
public ResourceLoader ResourceLoader { get; }
public ResourceLogger ResourceLogger { get; }
//public PathResolver PathResolver { get; }
public SettingsInterface SettingsInterface { get; }
@ -81,19 +66,18 @@ public class Penumbra : IDalamudPlugin
CharacterUtility = new CharacterUtility();
MetaDefaults = new MetaDefaults();
ResourceLoader = new ResourceLoader( this );
ResourceLogger = new ResourceLogger( ResourceLoader );
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
ModManager = new ModManager();
ModManager.DiscoverMods();
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames );
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
{
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
} );
ResourceLoader.EnableReplacements();
ResourceLoader.EnableLogging();
if( Config.DebugMode )
{
ResourceLoader.EnableDebug();
@ -112,7 +96,7 @@ public class Penumbra : IDalamudPlugin
CreateWebServer();
}
if( !Config.EnablePlayerWatch || !Config.IsEnabled )
if( !Config.EnablePlayerWatch || !Config.EnableMods )
{
PlayerWatcher.Disable();
}
@ -122,16 +106,25 @@ public class Penumbra : IDalamudPlugin
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
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()
{
if( Config.IsEnabled )
if( Config.EnableMods )
{
return false;
}
Config.IsEnabled = true;
Config.EnableMods = true;
ResourceLoader.EnableReplacements();
ResidentResources.Reload();
if( Config.EnablePlayerWatch )
{
@ -145,12 +138,13 @@ public class Penumbra : IDalamudPlugin
public bool Disable()
{
if( !Config.IsEnabled )
if( !Config.EnableMods )
{
return false;
}
Config.IsEnabled = false;
Config.EnableMods = false;
ResourceLoader.DisableReplacements();
ResidentResources.Reload();
if( Config.EnablePlayerWatch )
{
@ -219,8 +213,10 @@ public class Penumbra : IDalamudPlugin
Dalamud.Commands.RemoveHandler( CommandName );
//PathResolver.Dispose();
ResourceLogger.Dispose();
ResourceLoader.Dispose();
ShutdownWebServer();
}
@ -322,8 +318,8 @@ public class Penumbra : IDalamudPlugin
}
case "toggle":
{
SetEnabled( !Config.IsEnabled );
Dalamud.Chat.Print( Config.IsEnabled
SetEnabled( !Config.EnableMods );
Dalamud.Chat.Print( Config.EnableMods
? modsEnabled
: modsDisabled );
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.Security.Cryptography.X509Certificates;
using System.Windows.Forms;
using Dalamud.Interface;
using ImGuiNET;
using Penumbra.GameData.ByteString;
namespace Penumbra.UI.Custom
{
@ -20,6 +20,19 @@ namespace Penumbra.UI.Custom
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

View file

@ -21,11 +21,21 @@ public partial class SettingsInterface
private string _filePathFilter = string.Empty;
private string _filePathFilterLower = string.Empty;
private readonly float _leftTextLength =
ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40;
private const float LeftTextLength = 600;
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 )
{
ImGui.TableNextColumn();
@ -45,13 +55,13 @@ public partial class SettingsInterface
_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 ) )
{
_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 );
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 ) )
{
@ -69,7 +79,7 @@ public partial class SettingsInterface
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 ) )
{
@ -94,11 +104,6 @@ public partial class SettingsInterface
void DrawFileLines( ModCollectionCache cache )
{
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 );
}
@ -139,75 +144,67 @@ public partial class SettingsInterface
var activeCollection = modManager.Collections.ActiveCollection.Cache;
var forcedCollection = modManager.Collections.ForcedCollection.Cache;
var (activeResolved, activeSwap, activeMeta) = activeCollection != null
? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count )
: ( 0, 0, 0 );
var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count )
: ( 0, 0, 0 );
var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta;
var (activeResolved, activeMeta) = activeCollection != null
? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count )
: ( 0, 0 );
var (forcedResolved, forcedMeta) = forcedCollection != null
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count )
: ( 0, 0 );
var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta;
if( totalLines == 0 )
{
return;
}
if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
{
raii.Push( ImGui.EndTable );
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale );
return;
}
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;
unsafe
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
{
clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() );
}
clipper.Begin( totalLines );
while( clipper.Step() )
{
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
var row = actualRow;
ImGui.TableNextRow();
if( row < activeResolved )
{
var row = actualRow;
ImGui.TableNextRow();
if( row < activeResolved )
{
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
DrawLine( gamePath, file.FullName );
}
else if( ( row -= activeResolved ) < activeSwap )
{
var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row );
DrawLine( gamePath, swap );
}
else if( ( row -= activeSwap ) < activeMeta )
{
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
}
else if( ( row -= activeMeta ) < forcedResolved )
{
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 );
}
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
DrawLine( gamePath, file );
}
else if( ( row -= activeResolved ) < activeMeta )
{
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
}
else if( ( row -= activeMeta ) < forcedResolved )
{
var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
DrawLine( gamePath, file );
}
else
{
row -= forcedResolved;
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.Mod;
using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.UI.Custom;
using Penumbra.Util;
using ImGui = ImGuiNET.ImGui;
@ -56,7 +55,7 @@ public partial class SettingsInterface
private Option? _selectedOption;
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 SettingsInterface _base;
@ -218,7 +217,10 @@ public partial class SettingsInterface
indent.Push( 15f );
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 )
@ -258,13 +260,13 @@ public partial class SettingsInterface
foreach( var (source, target) in Meta.FileSwaps )
{
ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( source );
ImGuiCustom.CopyOnClickSelectable( source.Path );
ImGui.TableNextColumn();
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
ImGui.TableNextColumn();
ImGuiCustom.CopyOnClickSelectable( target );
ImGuiCustom.CopyOnClickSelectable( target.InternalName );
ImGui.TableNextRow();
}
@ -278,7 +280,8 @@ public partial class SettingsInterface
}
_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 )
{
@ -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;
var defaultIndex =
gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) );
var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) );
if( defaultIndex < 0 )
{
return defaultIndex;
}
string path = gamePaths[ defaultIndex ];
var path = gamePaths[ defaultIndex ].Path;
if( path.Length == TextDefaultGamePath.Length )
{
return defaultIndex;
}
if( path[ TextDefaultGamePath.Length ] != '-'
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) )
if( path[ TextDefaultGamePath.Length ] != ( byte )'-'
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) )
{
return -1;
}
@ -373,8 +375,9 @@ public partial class SettingsInterface
var option = ( Option )_selectedOption;
var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray();
if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 )
var gamePaths = _currentGamePaths.Split( ';' )
.Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray();
if( gamePaths.Length == 0 )
{
return;
}
@ -517,18 +520,18 @@ public partial class SettingsInterface
{
Selectable( 0, ColorGreen );
using var indent = ImGuiRaii.PushIndent( indentWidth );
var tmpPaths = gamePaths.ToArray();
foreach( var gamePath in tmpPaths )
using var indent = ImGuiRaii.PushIndent( indentWidth );
foreach( var gamePath in gamePaths.ToArray() )
{
string tmp = gamePath;
var tmp = gamePath.ToString();
var old = tmp;
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
&& tmp != gamePath )
&& tmp != old )
{
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 )
{

View file

@ -3,136 +3,179 @@ using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Util;
using Penumbra.Mod;
using Penumbra.Mods;
using Penumbra.Structs;
using Penumbra.UI.Custom;
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";
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 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 );
if( Meta!.Groups.Count == 0 )
{
ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale );
if( Meta!.Groups.Count == 0 )
{
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;
ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
return false;
}
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.SetNextItemWidth( OptionSelectionWidth );
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
var newName = opt.OptionName;
if( nameBoxStart == CheckMarkSize )
{
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 );
return false;
nameBoxStart = ImGui.GetCursorPosX();
}
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 )
if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
{
group.Options.Add( new Option()
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } );
_selector.SaveCurrentMod();
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();
@ -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;
var flag = Mod!.Settings.Settings[ group.GroupName ];
using var raii = DrawMultiSelectorEditBegin( group );
for( var i = 0; i < group.Options.Count; ++i )
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
{
var opt = group.Options[ i ];
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();
}
_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 ];
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( 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;
group.Options.Add( new Option()
{
OptionName = newName,
OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
} );
_selector.SaveCurrentMod();
}
OptionName = newName,
OptionDesc = "",
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
} );
_selector.SaveCurrentMod();
}
}
else
{
if( newName.Length == 0 )
{
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
}
else
{
if( newName.Length == 0 )
if( newName != group.Options[ code ].OptionName )
{
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
}
else
{
if( newName != group.Options[ code ].OptionName )
group.Options[ code ] = new Option()
{
group.Options[ code ] = new Option()
{
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
OptionFiles = group.Options[ code ].OptionFiles,
};
_selector.SaveCurrentMod();
}
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
OptionFiles = group.Options[ code ].OptionFiles,
};
_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();
var labelEditPos = ImGui.GetCursorPosX();
DrawSingleSelectorEditGroup( group );
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
ImGui.SameLine();
return labelEditPos;
}
private void DrawAddSingleGroupField( float labelEditPos )
{
var newGroup = "";
ImGui.SetCursorPosX( labelEditPos );
if( labelEditPos == CheckMarkSize )
ImGui.SetNextItemWidth( width );
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
GamePath.MaxGamePathLength,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
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 ? 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 newValue = new FullPath( valueString.ToLowerInvariant() );
if( newValue.CompareTo( value ) != 0 )
{
var newKey = new GamePath( keyString );
if( newKey.CompareTo( key ) != 0 )
{
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();
}
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 );
if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) )
if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) )
{
ImGui.CloseCurrentPopup();
}
@ -509,7 +509,7 @@ public partial class SettingsInterface
{
var change = 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++ );
if( mod != null )

View file

@ -3,8 +3,10 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text.RegularExpressions;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Logging;
using ImGuiNET;
using Penumbra.GameData.ByteString;
using Penumbra.Interop;
@ -131,7 +133,7 @@ public partial class SettingsInterface
private void DrawEnabledBox()
{
var enabled = _config.IsEnabled;
var enabled = _config.EnableMods;
if( ImGui.Checkbox( "Enable Mods", ref enabled ) )
{
_base._penumbra.SetEnabled( enabled );
@ -317,14 +319,84 @@ public partial class SettingsInterface
+ "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()
{
DrawTempFolder();
DrawRequestedResourceLogging();
DrawDisableSoundStreamingBox();
DrawLogLoadedFilesBox();
DrawDisableNotificationsBox();
DrawEnableHttpApiBox();
DrawReloadResourceButton();
DrawEnableDebugModeBox();
DrawEnableFullResourceLoggingBox();
}
public static unsafe void Text( Utf8String s )

View file

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

View file

@ -5,36 +5,35 @@ using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
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;
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
_penumbra.Api.InvokeClick( ret, data );
}
if( ret != MouseButton.None )
{
_penumbra.Api.InvokeClick( ret, data );
}
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
{
ImGui.BeginTooltip();
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
_penumbra.Api.InvokeTooltip( data );
}
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
{
ImGui.BeginTooltip();
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
_penumbra.Api.InvokeTooltip( data );
}
if( data is Item it )
{
var modelId = $"({( ( Quad )it.ModelMain ).A})";
var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
if( data is Item it )
{
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 );
}
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.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];
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 ] ) )
{
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( idx1 < 0 )
if( predicate.Invoke( array[ i ] ) )
{
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 )
{
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;
}
return -1;
}
}

View file

@ -3,135 +3,54 @@ using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
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>
/// 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 >() );
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>
/// 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 );
return list;
}
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 ) );
}
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;
}
/// <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;
}
}

View file

@ -2,36 +2,34 @@ using System.Collections.Generic;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin;
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 ItemPayload( item.RowId, false ),
new UIForegroundPayload( 500 ),
new UIGlowPayload( 501 ),
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
new UIForegroundPayload( 0 ),
new UIGlowPayload( 0 ),
new TextPayload( item.Name ),
new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ),
new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
};
new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ),
new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ),
new ItemPayload( item.RowId, false ),
new UIForegroundPayload( 500 ),
new UIGlowPayload( 501 ),
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
new UIForegroundPayload( 0 ),
new UIGlowPayload( 0 ),
new TextPayload( item.Name ),
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
{
Message = payload,
} );
}
Dalamud.Chat.PrintChat( new XivChatEntry
{
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.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();
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
_form = form;
_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 >();
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 )
Hide();
try
{
_form = form;
_owner = owner;
_taskSource = taskSource;
Opacity = 0;
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;
Size = new Size( 0, 0 );
Shown += HiddenForm_Shown;
var result = _form.ShowDialog( _owner );
_taskSource.SetResult( result );
}
catch( Exception e )
{
_taskSource.SetException( e );
}
private void HiddenForm_Shown( object? sender, EventArgs _ )
{
Hide();
try
{
var result = _form.ShowDialog( _owner );
_taskSource.SetResult( result );
}
catch( Exception e )
{
_taskSource.SetException( e );
}
Close();
}
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 static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled);
public static bool ValidStrings( string from, string to )
=> from.Length != 0
&& to.Length != 0
@ -40,8 +39,8 @@ public static class ModelChanger
try
{
var data = File.ReadAllBytes( file.FullName );
var mdlFile = new MdlFile( data );
var data = File.ReadAllBytes( file.FullName );
var mdlFile = new MdlFile( data );
Func< string, bool > compare = MaterialRegex.IsMatch;
if( from.Length > 0 )
{
@ -53,9 +52,9 @@ public static class ModelChanger
var replaced = 0;
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;
}
}

View file

@ -3,432 +3,405 @@ using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using Lumina;
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 )
: this( file.OpenRead() )
{ }
return Reader.ReadStructure< SqPackHeader >();
}
public PenumbraSqPackStream( Stream stream )
{
BaseStream = stream;
Reader = new BinaryReader( BaseStream );
}
public SqPackFileInfo GetFileMetadata( long offset )
{
BaseStream.Position = offset;
public SqPackHeader GetSqPackHeader()
{
BaseStream.Position = 0;
return Reader.ReadStructure< SqPackFileInfo >();
}
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;
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 >();
var file = Activator.CreateInstance< T >();
case FileType.Model:
ReadModelFile( file, ms );
break;
// 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;
case FileType.Texture:
ReadTextureFile( file, ms );
break;
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,
};
}
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;
default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
}
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 );
foreach( var block in blocks )
{
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
}
// reset position ready for reading
ms.Position = 0;
Debug.WriteLine( "Read data size does not match file size." );
}
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;
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
}
// 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++ )
// reset position ready for reading
ms.Position = 0;
}
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
{
var mdlBlock = resource.FileInfo!.ModelBlock;
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 ];
}
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;
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
{
var currentVertexOffset = ( int )ms.Position;
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
{
vertexDataOffsets[ i ] = currentVertexOffset;
}
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++;
}
vertexDataOffsets[ i ] = currentVertexOffset;
}
else
{
vertexDataOffsets[ i ] = 0;
}
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;
ReadFileBlock( ms );
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++;
}
var lastPos = Reader.BaseStream.Position;
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
currentBlock++;
}
}
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++ )
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
{
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++ )
{
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
}
// i guess this is only needed in the vertex area, for i = 0
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( indexBufferSizes[ 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.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
// will not be 0
var mipMapSize = blocks[ 0 ].CompressedOffset;
if( mipMapSize != 0 )
for( var i = 0; i < 3; i++ )
{
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
}
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;
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;
runningBlockTotal += ( uint )Reader.ReadInt16();
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 )
=> ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
=> 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;
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;
}
dest.Write( Reader.ReadBytes( ( int )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;
public FileType Type;
public uint RawFileSize;
public uint BlockCount;
public long Offset { get; internal set; }
public ModelBlock ModelBlock { get; internal set; }
BaseStream.Position = originalPosition;
}
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()
{ }
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;
// this function is intentionally left blank
}
}
[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.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 )
=> objectType == typeof( HashSet< T > );
var token = JToken.Load( reader );
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
if( token.Type == JTokenType.Array )
{
var token = JToken.Load( reader );
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 >();
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
}
public override bool CanWrite
=> true;
var tmp = token.ToObject< T >();
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();
if( value != null )
var v = ( HashSet< T > )value;
foreach( var val in v )
{
var v = ( HashSet< T > )value;
foreach( var val in v )
{
serializer.Serialize( writer, val?.ToString() );
}
serializer.Serialize( writer, val?.ToString() );
}
writer.WriteEndArray();
}
writer.WriteEndArray();
}
}

View file

@ -2,67 +2,66 @@ using System.Collections.Generic;
using System.IO;
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() );
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
StringBuilder sb = new(s.Length);
foreach( var c in s )
{
StringBuilder sb = new( s.Length );
foreach( var c in s )
if( Invalid.Contains( c ) )
{
if( Invalid.Contains( c ) )
{
sb.Append( replacement );
}
else
{
sb.Append( c );
}
sb.Append( replacement );
}
else
{
sb.Append( c );
}
return sb.ToString();
}
public static string RemoveInvalidPathSymbols( this string s )
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
return sb.ToString();
}
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 );
foreach( var c in s )
if( c >= 128 )
{
if( c >= 128 )
{
sb.Append( replacement );
}
else
{
sb.Append( 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();
}
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.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;
for( var i = 0; i < maxTries; ++i )
var name = Path.GetRandomFileName();
var path = new FileInfo( Path.Combine( baseDir.FullName,
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
if( !path.Exists )
{
var name = Path.GetRandomFileName();
var path = new FileInfo( Path.Combine( baseDir.FullName,
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
if( !path.Exists )
{
return path;
}
return path;
}
throw new IOException();
}
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
{
var fileName = TempFileName( baseDir, suffix );
using var stream = fileName.OpenWrite();
stream.Write( data, 0, data.Length );
fileName.Refresh();
return fileName;
}
throw new IOException();
}
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
{
var fileName = TempFileName( baseDir, suffix );
using var stream = fileName.OpenWrite();
stream.Write( data, 0, data.Length );
fileName.Refresh();
return fileName;
}
}