More sophisticated fix against E4S crashes with working mods in E4S.

This commit is contained in:
Ottermandias 2022-01-04 00:30:18 +01:00
parent a1f02975cb
commit f601812666
13 changed files with 273 additions and 210 deletions

View file

@ -22,7 +22,7 @@ namespace Penumbra.GameData.Util
}
else
{
_path = "";
_path = string.Empty;
}
}

View file

@ -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()

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 );

View file

@ -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" );

View file

@ -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 ) )
{

View file

@ -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;

View file

@ -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 )
{

85
Penumbra/Util/FullPath.cs Normal file
View file

@ -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;
}

View file

@ -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;
}