From f6018126666d38f0a02be7c8fa206d15d8e78271 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 4 Jan 2022 00:30:18 +0100 Subject: [PATCH] More sophisticated fix against E4S crashes with working mods in E4S. --- Penumbra.GameData/Util/GamePath.cs | 2 +- Penumbra/Interop/ResourceLoader.cs | 56 ++----- Penumbra/Meta/MetaCollection.cs | 2 +- Penumbra/Meta/MetaManager.cs | 21 ++- Penumbra/Mod/ModResources.cs | 137 ++++++++--------- Penumbra/Mods/ModCollectionCache.cs | 26 ++-- Penumbra/Mods/ModManager.cs | 8 + Penumbra/UI/MenuTabs/TabDebug.cs | 2 - Penumbra/UI/MenuTabs/TabEffective.cs | 2 +- .../TabInstalled/TabInstalledDetails.cs | 2 +- Penumbra/UI/MenuTabs/TabResourceManager.cs | 2 +- Penumbra/Util/FullPath.cs | 85 +++++++++++ Penumbra/Util/RelPath.cs | 138 +++++++++--------- 13 files changed, 273 insertions(+), 210 deletions(-) create mode 100644 Penumbra/Util/FullPath.cs diff --git a/Penumbra.GameData/Util/GamePath.cs b/Penumbra.GameData/Util/GamePath.cs index b602d8d6..002740f1 100644 --- a/Penumbra.GameData/Util/GamePath.cs +++ b/Penumbra.GameData/Util/GamePath.cs @@ -22,7 +22,7 @@ namespace Penumbra.GameData.Util } else { - _path = ""; + _path = string.Empty; } } diff --git a/Penumbra/Interop/ResourceLoader.cs b/Penumbra/Interop/ResourceLoader.cs index 5cfc23f3..fbb05077 100644 --- a/Penumbra/Interop/ResourceLoader.cs +++ b/Penumbra/Interop/ResourceLoader.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Hooking; using Dalamud.Logging; -using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Util; using Penumbra.Mods; using Penumbra.Structs; @@ -19,10 +18,10 @@ public class ResourceLoader : IDisposable public Penumbra Penumbra { get; set; } public bool IsEnabled { get; set; } - public bool HacksEnabled { get; set; } public Crc32 Crc32 { get; } + public static readonly IntPtr CustomFileFlag = new(0xDEADBEEF); // Delegate prototypes [UnmanagedFunctionPointer( CallingConvention.ThisCall )] @@ -40,7 +39,7 @@ public class ResourceLoader : IDisposable , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); [UnmanagedFunctionPointer( CallingConvention.ThisCall )] - public delegate bool CheckFileStatePrototype( IntPtr unk1, ulong unk2 ); + public delegate IntPtr CheckFileStatePrototype( IntPtr unk1, ulong crc ); [UnmanagedFunctionPointer( CallingConvention.ThisCall )] public delegate byte LoadTexFileExternPrototype( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr unk4 ); @@ -64,7 +63,6 @@ public class ResourceLoader : IDisposable // Unmanaged functions public ReadFilePrototype? ReadFile { get; private set; } - public CheckFileStatePrototype? CheckFileState { get; private set; } public LoadTexFileLocalPrototype? LoadTexFileLocal { get; private set; } public LoadMdlFileLocalPrototype? LoadMdlFileLocal { get; private set; } @@ -128,37 +126,21 @@ public class ResourceLoader : IDisposable LoadMdlFileExternHook = new Hook< LoadMdlFileExternPrototype >( loadMdlFileExternAddress, LoadMdlFileExternDetour ); } - private bool CheckForTerritory() + private IntPtr CheckFileStateDetour( IntPtr ptr, ulong crc64 ) { - var territory = Dalamud.GameData.GetExcelSheet< TerritoryType >()?.GetRow( Dalamud.ClientState.TerritoryType ); - var bad = territory?.Unknown40 ?? false; - switch( bad ) - { - case true when HacksEnabled: - CheckFileStateHook?.Disable(); - LoadTexFileExternHook?.Disable(); - LoadMdlFileExternHook?.Disable(); - HacksEnabled = false; - return bad; - case false when Penumbra.Config.IsEnabled && !HacksEnabled: - CheckFileStateHook?.Enable(); - LoadTexFileExternHook?.Enable(); - LoadMdlFileExternHook?.Enable(); - HacksEnabled = true; - break; - } - - return bad; + var modManager = Service< ModManager >.Get(); + return modManager.CheckCrc64( crc64 ) ? CustomFileFlag : CheckFileStateHook!.Original( ptr, crc64 ); } - private static bool CheckFileStateDetour( IntPtr _, ulong _2 ) - => true; + private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ) + : LoadTexFileExternHook!.Original( resourceHandle, unk1, unk2, unk3, ptr ); - private byte LoadTexFileExternDetour( IntPtr resourceHandle, int unk1, IntPtr unk2, bool unk3, IntPtr _ ) - => LoadTexFileLocal!.Invoke( resourceHandle, unk1, unk2, unk3 ); - - private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr _ ) - => LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ); + private byte LoadMdlFileExternDetour( IntPtr resourceHandle, IntPtr unk1, bool unk2, IntPtr ptr ) + => ptr.Equals( CustomFileFlag ) + ? LoadMdlFileLocal!.Invoke( resourceHandle, unk1, unk2 ) + : LoadMdlFileExternHook!.Original( resourceHandle, unk1, unk2, ptr ); private unsafe void* GetResourceSyncHandler( IntPtr pFileManager, @@ -223,11 +205,6 @@ public class ResourceLoader : IDisposable bool isUnknown ) { - if( CheckForTerritory() ) - { - return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); - } - string file; var modManager = Service< ModManager >.Get(); @@ -277,11 +254,6 @@ public class ResourceLoader : IDisposable private unsafe byte ReadSqpackHandler( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ) { - if( CheckForTerritory() ) - { - return ReadSqpackHook?.Original( pFileHandler, pFileDesc, priority, isSync ) ?? 0; - } - if( ReadFile == null || pFileDesc == null || pFileDesc->ResourceHandle == null ) { PluginLog.Error( "THIS SHOULD NOT HAPPEN" ); @@ -340,7 +312,6 @@ public class ResourceLoader : IDisposable LoadMdlFileExternHook.Enable(); IsEnabled = true; - HacksEnabled = true; } public void Disable() @@ -357,7 +328,6 @@ public class ResourceLoader : IDisposable LoadTexFileExternHook?.Disable(); LoadMdlFileExternHook?.Disable(); IsEnabled = false; - HacksEnabled = false; } public void Dispose() diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs index 3e47ff47..e0722424 100644 --- a/Penumbra/Meta/MetaCollection.cs +++ b/Penumbra/Meta/MetaCollection.cs @@ -147,7 +147,7 @@ namespace Penumbra.Meta // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, // combining them with the given ModMeta. - public void Update( IEnumerable< FileInfo > files, DirectoryInfo basePath, ModMeta modMeta ) + public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) { DefaultData.Clear(); GroupData.Clear(); diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index f0d2cad9..57bb0602 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -17,7 +17,7 @@ namespace Penumbra.Meta { public readonly object Data; public bool Changed; - public FileInfo? CurrentFile; + public FullPath? CurrentFile; public FileInformation( object data ) => Data = data; @@ -35,7 +35,7 @@ namespace Penumbra.Meta _ => throw new NotImplementedException(), }; DisposeFile( CurrentFile ); - CurrentFile = TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" ); + CurrentFile = new FullPath(TempFile.WriteNew( dir, data, $"_{originalPath.Filename()}" )); Changed = false; } } @@ -45,7 +45,7 @@ namespace Penumbra.Meta private readonly MetaDefaults _default; private readonly DirectoryInfo _dir; private readonly ResidentResources _resourceManagement; - private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; + private readonly Dictionary< GamePath, FullPath > _resolvedFiles; private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); @@ -53,9 +53,9 @@ namespace Penumbra.Meta public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); - public IEnumerable< (GamePath, FileInfo) > Files + public IEnumerable< (GamePath, FullPath) > Files => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) - .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile! ) ); + .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) ); public int Count => _currentManipulations.Count; @@ -63,9 +63,8 @@ namespace Penumbra.Meta public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) => _currentManipulations.TryGetValue( manip, out mod! ); - private static void DisposeFile( FileInfo? file ) + private static void DisposeFile( FullPath? file ) { - file?.Refresh(); if( !( file?.Exists ?? false ) ) { return; @@ -73,11 +72,11 @@ namespace Penumbra.Meta try { - file.Delete(); + File.Delete( file.Value.FullName ); } catch( Exception e ) { - PluginLog.Error( $"Could not delete temporary file \"{file.FullName}\":\n{e}" ); + PluginLog.Error( $"Could not delete temporary file \"{file.Value.FullName}\":\n{e}" ); } } @@ -120,7 +119,7 @@ namespace Penumbra.Meta private void ClearDirectory() => ClearDirectory( _dir ); - public MetaManager( string name, Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo tempDir ) + public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir ) { _resolvedFiles = resolvedFiles; _default = Service< MetaDefaults >.Get(); @@ -139,7 +138,7 @@ namespace Penumbra.Meta foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) { kvp.Value.Write( _dir, kvp.Key ); - _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!; + _resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value; } } diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index 859a92a5..d47851a5 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -3,86 +3,87 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Penumbra.Meta; +using Penumbra.Util; -namespace Penumbra.Mod +namespace Penumbra.Mod; + +[Flags] +public enum ResourceChange { - [Flags] - public enum ResourceChange + None = 0, + Files = 1, + Meta = 2, +} + +// Contains static mod data that should only change on filesystem changes. +public class ModResources +{ + public List< FullPath > ModFiles { get; private set; } = new(); + public List< FullPath > MetaFiles { get; private set; } = new(); + + public MetaCollection MetaManipulations { get; private set; } = new(); + + + private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) { - None = 0, - Files = 1, - Meta = 2, + MetaManipulations.Update( MetaFiles, basePath, meta ); + MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); } - // Contains static mod data that should only change on filesystem changes. - public class ModResources + public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) { - public List< FileInfo > ModFiles { get; private set; } = new(); - public List< FileInfo > MetaFiles { get; private set; } = new(); - - public MetaCollection MetaManipulations { get; private set; } = new(); - - - private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) + var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); + if( newManipulations == null ) { - MetaManipulations.Update( MetaFiles, basePath, meta ); - MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); + ForceManipulationsUpdate( meta, basePath ); } - - public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) + else { - var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); - if( newManipulations == null ) + MetaManipulations = newManipulations; + if( validate && !MetaManipulations.Validate( meta ) ) { ForceManipulationsUpdate( meta, basePath ); } - else - { - MetaManipulations = newManipulations; - if( validate && !MetaManipulations.Validate( meta ) ) - { - ForceManipulationsUpdate( meta, basePath ); - } - } - } - - // Update the current set of files used by the mod, - // returns true if anything changed. - public ResourceChange RefreshModFiles( DirectoryInfo basePath ) - { - List< FileInfo > tmpFiles = new( ModFiles.Count ); - List< FileInfo > tmpMetas = new( MetaFiles.Count ); - // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo - foreach( var file in basePath.EnumerateDirectories() - .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .OrderBy( f => f.FullName ) ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - tmpMetas.Add( file ); - break; - default: - tmpFiles.Add( file ); - break; - } - } - - ResourceChange changes = 0; - if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) - { - ModFiles = tmpFiles; - changes |= ResourceChange.Files; - } - - if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) - { - MetaFiles = tmpMetas; - changes |= ResourceChange.Meta; - } - - return changes; } } + + // Update the current set of files used by the mod, + // returns true if anything changed. + public ResourceChange RefreshModFiles( DirectoryInfo basePath ) + { + List< FullPath > tmpFiles = new(ModFiles.Count); + List< FullPath > tmpMetas = new(MetaFiles.Count); + // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo + foreach( var file in basePath.EnumerateDirectories() + .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + .Select( f => new FullPath( f ) ) + .OrderBy( f => f.FullName ) ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + tmpMetas.Add( file ); + break; + default: + tmpFiles.Add( file ); + break; + } + } + + ResourceChange changes = 0; + if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) + { + ModFiles = tmpFiles; + changes |= ResourceChange.Files; + } + + if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) + { + MetaFiles = tmpMetas; + changes |= ResourceChange.Meta; + } + + return changes; + } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 17fe47b3..1b3e6c1c 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Logging; using Penumbra.GameData.Util; using Penumbra.Meta; @@ -26,9 +25,10 @@ namespace Penumbra.Mods public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); + public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new(); public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public readonly HashSet< FileInfo > MissingFiles = new(); + public readonly HashSet< FullPath > MissingFiles = new(); + public readonly HashSet< ulong > Checksums = new(); public readonly MetaManager MetaManipulations; public IReadOnlyDictionary< string, object? > ChangedItems @@ -75,6 +75,9 @@ namespace Penumbra.Mods } AddMetaFiles(); + Checksums.Clear(); + foreach( var file in ResolvedFiles ) + Checksums.Add( file.Value.Crc64 ); } private void SetChangedItems() @@ -128,7 +131,7 @@ namespace Penumbra.Mods AddRemainingFiles( mod ); } - private void AddFile( Mod.Mod mod, GamePath gamePath, FileInfo file ) + private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file ) { if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) { @@ -145,7 +148,7 @@ namespace Penumbra.Mods } } - private void AddMissingFile( FileInfo file ) + private void AddMissingFile( FullPath file ) { switch( file.Extension.ToLowerInvariant() ) { @@ -162,16 +165,15 @@ namespace Penumbra.Mods { foreach( var (file, paths) in option.OptionFiles ) { - var fullPath = Path.Combine( mod.Data.BasePath.FullName, file ); - var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.FullName == fullPath ); + var fullPath = new FullPath(mod.Data.BasePath, file); + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals(fullPath) ); if( idx < 0 ) { - AddMissingFile( new FileInfo( fullPath ) ); + AddMissingFile( fullPath ); continue; } var registeredFile = mod.Data.Resources.ModFiles[ idx ]; - registeredFile.Refresh(); if( !registeredFile.Exists ) { AddMissingFile( registeredFile ); @@ -230,10 +232,9 @@ namespace Penumbra.Mods } var file = mod.Data.Resources.ModFiles[ i ]; - file.Refresh(); if( file.Exists ) { - AddFile( mod, new GamePath( file, mod.Data.BasePath ), file ); + AddFile( mod, file.ToGamePath( mod.Data.BasePath ), file ); } else { @@ -350,14 +351,13 @@ namespace Penumbra.Mods } } - public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath ) + public FullPath? GetCandidateForGameFile( GamePath gameResourcePath ) { if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) { return null; } - candidate.Refresh(); if( candidate.FullName.Length >= 260 || !candidate.Exists ) { return null; diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 87b78d4b..b7ab6544 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -347,6 +347,14 @@ 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 ) { var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath ); diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index ea46be36..49961160 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -165,7 +165,6 @@ public partial class SettingsInterface manager.TempPath != null ? Directory.Exists( manager.TempPath.FullName ).ToString() : false.ToString() ); PrintValue( "Mod Manager Temp Path IsWritable", manager.TempWritable.ToString() ); PrintValue( "Resource Loader Enabled", _penumbra.ResourceLoader.IsEnabled.ToString() ); - PrintValue( "Resource Loader Hacks Enabled", _penumbra.ResourceLoader.HacksEnabled.ToString() ); } private void DrawDebugTabRedraw() @@ -298,7 +297,6 @@ public partial class SettingsInterface ImGui.TableNextColumn(); ImGui.Text( file ); ImGui.TableNextColumn(); - info.CurrentFile?.Refresh(); ImGui.Text( info.CurrentFile?.Exists ?? false ? "Exists" : "Missing" ); ImGui.TableNextColumn(); ImGui.Text( info.Changed ? "Data Changed" : "Unchanged" ); diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 51143c4d..63edcebf 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -64,7 +64,7 @@ namespace Penumbra.UI } } - private bool CheckFilters( KeyValuePair< GamePath, FileInfo > kvp ) + private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp ) { if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) ) { diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index b95e0f6f..cf079a24 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -55,7 +55,7 @@ namespace Penumbra.UI private Option? _selectedOption; private string _currentGamePaths = ""; - private (FileInfo name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; + private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; private readonly Selector _selector; private readonly SettingsInterface _base; diff --git a/Penumbra/UI/MenuTabs/TabResourceManager.cs b/Penumbra/UI/MenuTabs/TabResourceManager.cs index ecdaa87e..9cd6812b 100644 --- a/Penumbra/UI/MenuTabs/TabResourceManager.cs +++ b/Penumbra/UI/MenuTabs/TabResourceManager.cs @@ -94,7 +94,7 @@ public partial class SettingsInterface using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem ); - var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E5B440 ); + var resourceHandler = *( ResourceManager** )( Dalamud.SigScanner.Module.BaseAddress + 0x1E603C0 ); if( resourceHandler == null ) { diff --git a/Penumbra/Util/FullPath.cs b/Penumbra/Util/FullPath.cs new file mode 100644 index 00000000..829bc5bb --- /dev/null +++ b/Penumbra/Util/FullPath.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using Penumbra.GameData.Util; + +namespace Penumbra.Util; + +public readonly struct FullPath : IComparable, IEquatable< FullPath > +{ + public readonly string FullName; + public readonly string InternalName; + public readonly ulong Crc64; + + public FullPath( DirectoryInfo baseDir, RelPath relPath ) + { + FullName = Path.Combine( baseDir.FullName, relPath ); + InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); + Crc64 = ComputeCrc64( InternalName ); + } + + public FullPath( FileInfo file ) + { + FullName = file.FullName; + InternalName = FullName.Replace( '\\', '/' ).ToLowerInvariant().Trim(); + Crc64 = ComputeCrc64( InternalName ); + } + + public bool Exists + => File.Exists( FullName ); + + public string Extension + => Path.GetExtension( FullName ); + + public string Name + => Path.GetFileName( FullName ); + + public GamePath ToGamePath( DirectoryInfo dir ) + => FullName.StartsWith(dir.FullName) ? GamePath.GenerateUnchecked( InternalName[(dir.FullName.Length+1)..]) : GamePath.GenerateUnchecked( string.Empty ); + + private static ulong ComputeCrc64( string name ) + { + if( name.Length == 0 ) + { + return 0; + } + + var lastSlash = name.LastIndexOf( '/' ); + if( lastSlash == -1 ) + { + return Lumina.Misc.Crc32.Get( name ); + } + + var folder = name[ ..lastSlash ]; + var file = name[ ( lastSlash + 1 ).. ]; + return ( ( ulong )Lumina.Misc.Crc32.Get( folder ) << 32 ) | Lumina.Misc.Crc32.Get( file ); + } + + public int CompareTo( object? obj ) + => obj switch + { + FullPath p => string.Compare( InternalName, p.InternalName, StringComparison.InvariantCulture ), + FileInfo f => string.Compare( FullName, f.FullName, StringComparison.InvariantCultureIgnoreCase ), + _ => -1, + }; + + public bool Equals( FullPath other ) + { + if( Crc64 != other.Crc64 ) + { + return false; + } + + if( FullName.Length == 0 || other.FullName.Length == 0 ) + { + return true; + } + + return InternalName.Equals( other.InternalName ); + } + + public override int GetHashCode() + => Crc64.GetHashCode(); + + public override string ToString() + => FullName; +} \ No newline at end of file diff --git a/Penumbra/Util/RelPath.cs b/Penumbra/Util/RelPath.cs index 2c08cd9a..6d6ea369 100644 --- a/Penumbra/Util/RelPath.cs +++ b/Penumbra/Util/RelPath.cs @@ -3,79 +3,81 @@ using System.IO; using System.Linq; using Penumbra.GameData.Util; -namespace Penumbra.Util +namespace Penumbra.Util; + +public readonly struct RelPath : IComparable { - public readonly struct RelPath : IComparable + public const int MaxRelPathLength = 256; + + private readonly string _path; + + private RelPath( string path, bool _ ) + => _path = path; + + private RelPath( string? path ) { - public const int MaxRelPathLength = 256; - - private readonly string _path; - - private RelPath( string path, bool _ ) - => _path = path; - - private RelPath( string? path ) + if( path != null && path.Length < MaxRelPathLength ) { - if( path != null && path.Length < MaxRelPathLength ) - { - _path = Trim( ReplaceSlash( path ) ); - } - else - { - _path = ""; - } + _path = Trim( ReplaceSlash( path ) ); } - - public RelPath( FileInfo file, DirectoryInfo baseDir ) - => _path = CheckPre( file, baseDir ) ? Trim( Substring( file, baseDir ) ) : ""; - - public RelPath( GamePath gamePath ) - => _path = ReplaceSlash( gamePath ); - - public GamePath ToGamePath( int skipFolders = 0 ) + else { - string p = this; - if( skipFolders > 0 ) - { - p = string.Join( "/", p.Split( '\\' ).Skip( skipFolders ) ); - return GamePath.GenerateUncheckedLower( p ); - } - - return GamePath.GenerateUncheckedLower( p.Replace( '\\', '/' ) ); + _path = ""; } - - private static bool CheckPre( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.StartsWith( baseDir.FullName ) && file.FullName.Length < MaxRelPathLength; - - private static string Substring( FileInfo file, DirectoryInfo baseDir ) - => file.FullName.Substring( baseDir.FullName.Length ); - - private static string ReplaceSlash( string path ) - => path.Replace( '/', '\\' ); - - private static string Trim( string path ) - => path.TrimStart( '\\' ); - - public static implicit operator string( RelPath relPath ) - => relPath._path; - - public static explicit operator RelPath( string relPath ) - => new( relPath ); - - public bool Empty - => _path.Length == 0; - - public int CompareTo( object? rhs ) - { - return rhs switch - { - string path => string.Compare( _path, path, StringComparison.InvariantCulture ), - RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), - _ => -1, - }; - } - - public override string ToString() - => _path; } + + public RelPath( FullPath file, DirectoryInfo baseDir ) + => _path = CheckPre( file.FullName, baseDir ) ? ReplaceSlash( Trim( Substring( file.FullName, baseDir ) ) ) : string.Empty; + + public RelPath( FileInfo file, DirectoryInfo baseDir ) + => _path = CheckPre( file.FullName, baseDir ) ? Trim( Substring( file.FullName, baseDir ) ) : string.Empty; + + public RelPath( GamePath gamePath ) + => _path = ReplaceSlash( gamePath ); + + public GamePath ToGamePath( int skipFolders = 0 ) + { + string p = this; + if( skipFolders > 0 ) + { + p = string.Join( "/", p.Split( '\\' ).Skip( skipFolders ) ); + return GamePath.GenerateUncheckedLower( p ); + } + + return GamePath.GenerateUncheckedLower( p.Replace( '\\', '/' ) ); + } + + private static bool CheckPre( string file, DirectoryInfo baseDir ) + => file.StartsWith( baseDir.FullName ) && file.Length < MaxRelPathLength; + + private static string Substring( string file, DirectoryInfo baseDir ) + => file.Substring( baseDir.FullName.Length ); + + private static string ReplaceSlash( string path ) + => path.Replace( '/', '\\' ); + + private static string Trim( string path ) + => path.TrimStart( '\\' ); + + public static implicit operator string( RelPath relPath ) + => relPath._path; + + public static explicit operator RelPath( string relPath ) + => new(relPath); + + public bool Empty + => _path.Length == 0; + + public int CompareTo( object? rhs ) + { + return rhs switch + { + string path => string.Compare( _path, path, StringComparison.InvariantCulture ), + RelPath path => string.Compare( _path, path._path, StringComparison.InvariantCulture ), + _ => -1, + }; + } + + public override string ToString() + => _path; } \ No newline at end of file