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 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 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!;
@ -30,5 +31,4 @@ namespace Penumbra
[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
}
}

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
{
@ -45,15 +44,15 @@ public class MetaManager : IDisposable
public const string TmpDirectory = "penumbrametatmp";
private readonly DirectoryInfo _dir;
private readonly Dictionary< GamePath, FullPath > _resolvedFiles;
private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles;
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
private readonly Dictionary< GamePath, FileInformation > _currentFiles = 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,16 +3,14 @@ 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 )
{
if( config.Version != 0 )
@ -82,5 +80,4 @@ namespace Penumbra
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,14 +1,13 @@
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; }
@ -23,13 +22,12 @@ namespace Penumbra.Mod
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< GamePath > GetFiles( FileInfo file )
public HashSet< Utf8GamePath > GetFiles( FileInfo file )
{
var relPath = new RelPath( file, Data.BasePath );
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,16 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.Util;
using Penumbra.GameData.ByteString;
using Penumbra.Meta;
namespace Penumbra.Mod
{
// The ModCache contains volatile information dependent on all current settings in a collection.
public class ModCache
{
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
namespace Penumbra.Mod;
public void AddConflict( Mod precedingMod, GamePath gamePath )
// 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 )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
{
@ -18,7 +18,7 @@ namespace Penumbra.Mod
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() );
}
}
@ -30,7 +30,7 @@ namespace Penumbra.Mod
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
Conflicts[ precedingMod ] = ( new List< Utf8GamePath >(), new List< MetaManipulation > { manipulation } );
}
}
@ -54,5 +54,4 @@ namespace Penumbra.Mod
return kvp.Value;
} );
}
}
}

View file

@ -2,25 +2,23 @@ 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;
@ -51,7 +49,7 @@ namespace Penumbra.Mod
}
else
{
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
_filesBySize[ fileLength ] = new List< FileInfo > { file };
}
}
}
@ -59,8 +57,7 @@ namespace Penumbra.Mod
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;
return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
}
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
@ -90,18 +87,18 @@ namespace Penumbra.Mod
{
try
{
var newDir = CreateNewModDir( mod, group.GroupName!, option.OptionName );
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 );
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 );
var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
unseenPaths.Remove( oldPath );
if( File.Exists( oldPath ) )
{
foreach( var path in paths )
{
var newPath = Path.Combine( newDir.FullName, path );
var newPath = Path.Combine( newDir.FullName, path.ToString() );
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
File.Copy( oldPath, newPath, true );
}
@ -121,7 +118,7 @@ namespace Penumbra.Mod
public static void SplitMod( ModData mod )
{
if( !mod.Meta.Groups.Any() )
if( mod.Meta.Groups.Count == 0 )
{
return;
}
@ -135,7 +132,7 @@ namespace Penumbra.Mod
}
}
if( !unseenPaths.Any() )
if( unseenPaths.Count == 0 )
{
return;
}
@ -148,8 +145,10 @@ namespace Penumbra.Mod
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 ) } ),
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 );
}
@ -161,7 +160,7 @@ namespace Penumbra.Mod
{
OptionName = Required,
OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
};
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
@ -189,35 +188,35 @@ namespace Penumbra.Mod
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 ) )
foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
{
if( pair.Value.Count == 2 )
if( value.Count == 2 )
{
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
{
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
dedup.ReplaceFile( value[ 0 ], value[ 1 ] );
}
}
else
{
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray();
var deleted = Enumerable.Repeat( false, value.Count ).ToArray();
var hashes = value.Select( dedup.ComputeHash ).ToArray();
for( var i = 0; i < pair.Value.Count; ++i )
for( var i = 0; i < value.Count; ++i )
{
if( deleted[ i ] )
{
continue;
}
for( var j = i + 1; j < pair.Value.Count; ++j )
for( var j = i + 1; j < value.Count; ++j )
{
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
{
continue;
}
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
dedup.ReplaceFile( value[ i ], value[ j ] );
deleted[ j ] = true;
}
}
@ -230,8 +229,11 @@ namespace Penumbra.Mod
private void ReplaceFile( FileInfo f1, FileInfo f2 )
{
RelPath relName1 = new( f1, _baseDir );
RelPath relName2 = new( f2, _baseDir );
if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 )
|| !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) )
{
return;
}
var inOption1 = false;
var inOption2 = false;
@ -302,7 +304,7 @@ namespace Penumbra.Mod
}
}
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false )
private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false )
{
var groupEnumerator = exceptDuplicates
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
@ -322,16 +324,16 @@ namespace Penumbra.Mod
if( requiredIdx >= 0 )
{
var required = info.Options[ requiredIdx ];
foreach( var kvp in required.OptionFiles.ToArray() )
foreach( var (key, value) in required.OptionFiles.ToArray() )
{
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) )
if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) )
{
continue;
}
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( kvp.Key.ToGamePath() ) == 0 )
if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 )
{
required.OptionFiles.Remove( kvp.Key );
required.OptionFiles.Remove( key );
}
}
@ -354,7 +356,7 @@ namespace Penumbra.Mod
Multi = 2,
};
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both,
bool skipDuplicates = true )
{
if( meta.Groups.Count == 0 )
@ -384,18 +386,18 @@ namespace Penumbra.Mod
}
}
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 == newRelPath )
if( oldRelPath.Equals( newRelPath ) )
{
return true;
}
try
{
var newFullPath = Path.Combine( basePath, newRelPath );
var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
new FileInfo( newFullPath ).Directory!.Create();
File.Move( Path.Combine( basePath, oldRelPath ), newFullPath );
File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath );
}
catch( Exception e )
{
@ -429,10 +431,10 @@ namespace Penumbra.Mod
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
{
var firstOption = true;
HashSet< (RelPath, GamePath) > groupList = new();
HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new();
foreach( var option in group.Options )
{
HashSet< (RelPath, GamePath) > optionList = new();
HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new();
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
{
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
@ -450,14 +452,14 @@ namespace Penumbra.Mod
firstOption = false;
}
var newPath = new Dictionary< RelPath, GamePath >();
var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
foreach( var (path, gamePath) in groupList )
{
var relPath = new RelPath( gamePath );
var relPath = new Utf8RelPath( gamePath );
if( newPath.TryGetValue( path, out var usedGamePath ) )
{
var required = FindOrCreateDuplicates( meta );
var usedRelPath = new RelPath( usedGamePath );
var usedRelPath = new Utf8RelPath( usedGamePath );
required.AddFile( usedRelPath, gamePath );
required.AddFile( usedRelPath, usedGamePath );
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
@ -498,13 +500,15 @@ namespace Penumbra.Mod
{
OptionDesc = string.Empty,
OptionName = optionDir.Name,
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
};
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( Utf8RelPath.FromFile( file, baseDir, out var rel )
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
{
option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
}
}
if( option.OptionFiles.Any() )
@ -519,9 +523,9 @@ namespace Penumbra.Mod
}
}
foreach(var collection in Penumbra.ModManager.Collections.Collections.Values)
collection.UpdateSetting(baseDir, meta, true);
foreach( var collection in Penumbra.ModManager.Collections.Collections.Values )
{
collection.UpdateSetting( baseDir, meta, true );
}
}
}

View file

@ -3,13 +3,13 @@ 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;
@ -28,7 +28,7 @@ namespace Penumbra.Mod
get
{
var path = SortOrderPath;
return path.Any() ? $"{path}/{SortOrderName}" : SortOrderName;
return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
}
}
@ -40,17 +40,17 @@ namespace Penumbra.Mod
}
public string FullPath
=> SortOrderPath.Any() ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
=> 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
{
// 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;
@ -77,24 +77,24 @@ namespace Penumbra.Mod
{
var identifier = GameData.GameData.GetIdentifier();
ChangedItems.Clear();
foreach( var file in Resources.ModFiles.Select( f => new RelPath( f, BasePath ) ) )
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 );
identifier.Identify( ChangedItems, path.ToGamePath() );
}
}
foreach( var path in Meta.FileSwaps.Keys )
{
identifier.Identify( ChangedItems, path );
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" ) );
=> new(Path.Combine( basePath.FullName, "meta.json" ));
public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
{
@ -132,5 +132,4 @@ namespace Penumbra.Mod
public override string ToString()
=> SortOrder.FullPath;
}
}

View file

@ -1,15 +1,13 @@
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 )
{
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
@ -23,10 +21,10 @@ namespace Penumbra.Mod
return anyChanges;
}
public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta )
public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta )
{
var doNotAdd = false;
var files = new HashSet< GamePath >();
var files = new HashSet< Utf8GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
@ -34,15 +32,15 @@ namespace Penumbra.Mod
if( !doNotAdd )
{
files.Add( new GamePath( relPath ) );
files.Add( relPath.ToGamePath() );
}
return files;
}
public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta )
public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta )
{
var ret = new HashSet< GamePath >();
var ret = new HashSet< Utf8GamePath >();
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
{
if( option.OptionFiles.TryGetValue( relPath, out var files ) )
@ -67,37 +65,36 @@ namespace Penumbra.Mod
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
foreach( var kvp in namedSettings.Settings )
foreach( var setting in namedSettings.Settings.Keys )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
if( !meta.Groups.TryGetValue( setting, out var info ) )
{
continue;
}
if( info.SelectionType == SelectType.Single )
{
if( namedSettings.Settings[ kvp.Key ].Count == 0 )
if( namedSettings.Settings[ setting ].Count == 0 )
{
ret.Settings[ kvp.Key ] = 0;
ret.Settings[ setting ] = 0;
}
else
{
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : 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[ kvp.Key ]
foreach( var idx in namedSettings.Settings[ setting ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ kvp.Key ] |= 1 << idx;
ret.Settings[ setting ] |= 1 << idx;
}
}
}
return ret;
}
}
}

View file

@ -4,14 +4,14 @@ 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
@ -48,8 +48,8 @@ namespace Penumbra.Mod
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();
@ -133,5 +133,4 @@ namespace Penumbra.Mod
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
}
}

View file

@ -1,13 +1,12 @@
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();
@ -31,7 +30,7 @@ namespace Penumbra.Mod
public static ModSettings DefaultSettings( ModMeta meta )
{
return new()
return new ModSettings
{
Enabled = false,
Priority = 0,
@ -71,5 +70,4 @@ namespace Penumbra.Mod
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
}
}
}

View file

@ -1,14 +1,13 @@
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();
@ -44,5 +43,4 @@ namespace Penumbra.Mod
}
}
}
}
}

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,8 +249,11 @@ public class CollectionManager
public bool CreateCharacterCollection( string characterName )
{
if( !CharacterCollection.ContainsKey( characterName ) )
if( CharacterCollection.ContainsKey( characterName ) )
{
return false;
}
CharacterCollection[ characterName ] = ModCollection.Empty;
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
Penumbra.Config.Save();
@ -259,9 +261,6 @@ public class CollectionManager
return true;
}
return false;
}
public void RemoveCharacterCollection( string characterName )
{
if( CharacterCollection.TryGetValue( characterName, out var collection ) )
@ -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,23 +1,22 @@
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; }
@ -103,8 +102,11 @@ namespace Penumbra.Mods
return;
}
if (clear)
if( clear )
{
settings.Settings.Clear();
}
if( settings.FixInvalidSettings( meta ) )
{
Save();
@ -188,14 +190,14 @@ namespace Penumbra.Mods
}
public static DirectoryInfo CollectionDir()
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ) );
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ));
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
=> new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) );
=> new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ));
public FileInfo FileName()
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
$"{Name.RemoveInvalidPathSymbols()}.json" ) );
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
$"{Name.RemoveInvalidPathSymbols()}.json" ));
public void Save()
{
@ -247,9 +249,8 @@ namespace Penumbra.Mods
data );
}
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
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;
@ -21,13 +20,12 @@ 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 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 Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
public readonly HashSet< FullPath > MissingFiles = new();
public readonly HashSet< ulong > Checksums = new();
public readonly MetaManager MetaManipulations;
@ -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,8 +179,7 @@ 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 fullPath = new FullPath( mod.Data.BasePath, file );
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
if( idx < 0 )
{
@ -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 ) )
{
return;
}
AvailableMods.Remove( basePath.Name );
if( mod.Settings.Enabled )
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 )
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,24 +144,27 @@ 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 );
}
@ -180,14 +188,9 @@ public partial class SettingsInterface
if( row < activeResolved )
{
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
DrawLine( gamePath, file.FullName );
DrawLine( gamePath, file );
}
else if( ( row -= activeResolved ) < activeSwap )
{
var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row );
DrawLine( gamePath, swap );
}
else if( ( row -= activeSwap ) < activeMeta )
else if( ( row -= activeResolved ) < activeMeta )
{
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
@ -195,16 +198,11 @@ public partial class SettingsInterface
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 );
DrawLine( gamePath, file );
}
else
{
row -= forcedSwap;
row -= forcedResolved;
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
}
@ -213,5 +211,4 @@ public partial class SettingsInterface
}
}
}
}
}

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;
}
@ -518,17 +521,17 @@ public partial class SettingsInterface
Selectable( 0, ColorGreen );
using var indent = ImGuiRaii.PushIndent( indentWidth );
var tmpPaths = gamePaths.ToArray();
foreach( var gamePath in tmpPaths )
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,16 +3,17 @@ 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 const string LabelDescEdit = "##descedit";
@ -24,6 +25,7 @@ namespace Penumbra.UI
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 =
@ -131,7 +133,7 @@ namespace Penumbra.UI
&& newOption.Length != 0 )
{
group.Options.Add( new Option()
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } );
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } );
_selector.SaveCurrentMod();
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
{
@ -212,7 +214,7 @@ namespace Penumbra.UI
{
OptionName = newName,
OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
} );
_selector.SaveCurrentMod();
}
@ -330,24 +332,23 @@ namespace Penumbra.UI
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;
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 ) )
{
var newKey = new GamePath( keyString );
if( newKey.CompareTo( key ) != 0 )
if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 )
{
if( idx < swaps.Length )
{
Meta.FileSwaps.Remove( key );
}
if( newKey != string.Empty )
if( !newKey.IsEmpty )
{
Meta.FileSwaps[ newKey ] = value;
}
@ -371,7 +372,7 @@ namespace Penumbra.UI
GamePath.MaxGamePathLength,
ImGuiInputTextFlags.EnterReturnsTrue ) )
{
var newValue = new GamePath( valueString );
var newValue = new FullPath( valueString.ToLowerInvariant() );
if( newValue.CompareTo( value ) != 0 )
{
Meta.FileSwaps[ key ] = newValue;
@ -382,5 +383,4 @@ namespace Penumbra.UI
}
}
}
}
}

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,11 +5,11 @@ 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;
@ -36,5 +36,4 @@ namespace Penumbra.UI
ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
}
}
}
}

View file

@ -1,31 +1,10 @@
using System;
using System.Collections.Generic;
namespace Penumbra.Util
namespace Penumbra.Util;
public static class ArrayExtensions
{
public static class ArrayExtensions
{
public static T[] Slice< T >( this T[] source, int index, int length )
{
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 )
@ -39,49 +18,16 @@ namespace Penumbra.Util
return -1;
}
public static void Swap< T >( this T[] array, T lhs, T rhs )
{
var idx1 = Array.IndexOf( array, lhs );
if( idx1 < 0 )
{
return;
}
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;
}
}
}

View file

@ -3,12 +3,11 @@ 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>
@ -47,60 +46,6 @@ namespace Penumbra.Util
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>
@ -108,30 +53,4 @@ namespace Penumbra.Util
{
br.BaseStream.Position = offset;
}
/// <summary>
/// Reads a byte and moves the stream position back to where it started before the operation
/// </summary>
/// <param name="br">The reader to use to read the byte</param>
/// <returns>The byte that was read</returns>
public static byte PeekByte( this BinaryReader br )
{
var data = br.ReadByte();
br.BaseStream.Position--;
return data;
}
/// <summary>
/// Reads bytes and moves the stream position back to where it started before the operation
/// </summary>
/// <param name="br">The reader to use to read the bytes</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>The read bytes</returns>
public static byte[] PeekBytes( this BinaryReader br, int count )
{
var data = br.ReadBytes( count );
br.BaseStream.Position -= count;
return data;
}
}
}

View file

@ -2,13 +2,12 @@ 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 )
{
var payloadList = new List< Payload >
@ -33,5 +32,4 @@ namespace Penumbra.Util
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,10 +5,10 @@ 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 )
{
using var process = Process.GetCurrentProcess();
@ -78,5 +78,4 @@ namespace Penumbra.Util
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
@ -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,14 +3,12 @@ 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; }
@ -351,7 +349,7 @@ namespace Penumbra.Util
public void Dispose()
{
Reader?.Dispose();
Reader.Dispose();
}
public class PenumbraFileInfo
@ -375,15 +373,10 @@ namespace Penumbra.Util
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>
@ -391,25 +384,6 @@ namespace Penumbra.Util
{
// 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 )]
@ -430,5 +404,4 @@ namespace Penumbra.Util
public uint BlockOffset;
public uint BlockCount;
}
}
}

View file

@ -3,10 +3,10 @@ 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 > );
@ -42,5 +42,4 @@ namespace Penumbra.Util
writer.WriteEndArray();
}
}
}

View file

@ -2,15 +2,15 @@ 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() );
private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars());
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
{
StringBuilder sb = new( s.Length );
StringBuilder sb = new(s.Length);
foreach( var c in s )
{
if( Invalid.Contains( c ) )
@ -31,7 +31,7 @@ namespace Penumbra.Util
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" )
{
StringBuilder sb = new( s.Length );
StringBuilder sb = new(s.Length);
foreach( var c in s )
{
if( c >= 128 )
@ -49,7 +49,7 @@ namespace Penumbra.Util
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
{
StringBuilder sb = new( s.Length );
StringBuilder sb = new(s.Length);
foreach( var c in s )
{
if( c >= 128 || Invalid.Contains( c ) )
@ -64,5 +64,4 @@ namespace Penumbra.Util
return sb.ToString();
}
}
}

View file

@ -1,10 +1,10 @@
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 = "" )
{
const uint maxTries = 15;
@ -30,5 +30,4 @@ namespace Penumbra.Util
fileName.Refresh();
return fileName;
}
}
}