Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 64 additions & 18 deletions src/Lumina/GameData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,29 +122,45 @@ public GameData( string dataPath, ILogger logger, LuminaOptions? options = null!
Logger = logger ?? throw new ArgumentNullException( nameof( logger ) );
}

/// <inheritdoc cref="ParseFilePath(ReadOnlySpan{char})"/>
public static ParsedFilePath? ParseFilePath( string path )
=> ParseFilePath( path.AsSpan() );

/// <summary>
/// Parses a game filesystem path and extracts information and hashes the path provided.
/// </summary>
/// <param name="path">A game filesystem path</param>
/// <returns>A <see cref="ParsedFilePath"/> which contains extracted info from the path, along with the hashes used to access the file index</returns>
public static ParsedFilePath? ParseFilePath( string path )
public static ParsedFilePath? ParseFilePath( ReadOnlySpan<char> path )
{
if( string.IsNullOrWhiteSpace( path ) )
if( path.IsWhiteSpace() )
return null;

// validate path slightly
if( path[ ^1 ] == '/' )
return null;

path = path.ToLowerInvariant().Trim();

var pathParts = path.Split( '/' );
var category = pathParts.First();
// Game paths can not be longer than MAX_PATH.
if( path.Length >= 260 )
return null;

var hash = GetFileHash( path );
var hash2 = Crc32.Get( path );
// Has to have at least one folder.
var directorySplit = path.LastIndexOf( '/' );
if( directorySplit < 0 )
return null;

Span<char> lowerPath = stackalloc char[260];
var length = path.ToLowerInvariant( lowerPath );
lowerPath[length] = '\0';
lowerPath = lowerPath[ ..length ];
lowerPath = lowerPath.Trim();

var repo = pathParts[ 1 ];
var hash = GetFileHash( lowerPath[..directorySplit], lowerPath[(directorySplit + 1)..] );
var hash2 = Crc32.Get( lowerPath );

var pathParts = lowerPath.Split( '/' );
ReadOnlySpan<char> category = pathParts.MoveNext() ? lowerPath[pathParts.Current] : [];
ReadOnlySpan<char> repo = pathParts.MoveNext() ? lowerPath[pathParts.Current] : [];
// todo: supports up to ex9, so we've got another ~11 years before this breaks
if( repo[ 0 ] != 'e' || repo[ 1 ] != 'x' || !char.IsDigit( repo[ 2 ] ) )
{
Expand All @@ -153,11 +169,11 @@ public GameData( string dataPath, ILogger logger, LuminaOptions? options = null!

return new ParsedFilePath
{
Category = category,
Category = category.ToString(),
IndexHash = hash,
Index2Hash = hash2,
Repository = repo,
Path = path
Repository = repo.ToString(),
Path = lowerPath.ToString(),
};
}

Expand Down Expand Up @@ -254,12 +270,27 @@ public T GetFileFromDisk< T >( string path, string? origPath = null ) where T :
return null;
}

/// <inheritdoc cref="FileExists(ReadOnlySpan{char})"/>
public bool FileExists( string path )
{
var parsedPath = ParseFilePath( path );
if( parsedPath == null )
return false;

if( Repositories.TryGetValue( parsedPath.Repository, out var repo ) )
{
return repo.FileExists( parsedPath.Category, parsedPath );
}

return false;
}

/// <summary>
/// Check if a file exists anywhere by checking whether the hash exists in any index
/// </summary>
/// <param name="path">The full path of the file</param>
/// <returns>True if the file exists</returns>
public bool FileExists( string path )
public bool FileExists( ReadOnlySpan<char> path )
{
var parsedPath = ParseFilePath( path );
if( parsedPath == null )
Expand All @@ -273,18 +304,33 @@ public bool FileExists( string path )
return false;
}

/// <inheritdoc cref="GetFileHash(ReadOnlySpan{char})"/>
public static UInt64 GetFileHash( string path )
=> GetFileHash( path.AsSpan() );

/// <summary>
/// Returns the index variant of a file hash
/// </summary>
/// <param name="path">The full path of the file</param>
/// <returns>A U64 containing a split hash of the folder and file CRC32s</returns>
public static UInt64 GetFileHash( string path )
public static UInt64 GetFileHash( ReadOnlySpan<char> path )
{
var pathParts = path.Split( '/' );
var filename = pathParts[ ^1 ];
var folder = path.Substring( 0, path.LastIndexOf( '/' ) );
var directorySplit = path.LastIndexOf( '/' );
if( directorySplit < 0 )
return Crc32.Get( path );

return GetFileHash( path[..directorySplit] ) << 32 | Crc32.Get( path[(directorySplit + 1)..] );
}

return (UInt64) Crc32.Get( folder ) << 32 | Crc32.Get( filename );
/// <summary>
/// Returns the index variant of a file hash
/// </summary>
/// <param name="folder">The full path of the directory the file is in without trailing '/'.</param>
/// <param name="filename">The file name with extension.</param>
/// <returns>A U64 containing a split hash of the folder and file CRC32s</returns>
public static UInt64 GetFileHash( ReadOnlySpan<char> folder, ReadOnlySpan<char> filename )
{
return (UInt64)Crc32.Get( folder ) << 32 | Crc32.Get( filename );
}

/// <summary>Loads an <see cref="ExcelSheet{T}"/>. Returns <see langword="null"/> if the sheet does not exist, has an invalid column hash or unsupported variant, or was requested with an unsupported language.</summary>
Expand Down
19 changes: 17 additions & 2 deletions src/Lumina/Misc/Crc32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,24 @@ static Crc32()
/// <returns>The CRC32 of the input data</returns>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public static uint Get( string value, uint crc = CrcInitialSeed )
=> Get( value.AsSpan(), crc );

/// <summary>
/// Calculate the CRC32 of the given string.
/// </summary>
/// <param name="value">The value to hash</param>
/// <param name="crc">The initial seed/value</param>
/// <returns>The CRC32 of the input data</returns>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public static uint Get( ReadOnlySpan<char> value, uint crc = CrcInitialSeed )
{
var data = Encoding.UTF8.GetBytes( value );
return Get( data, 0, data.Length, crc );
Span< byte > bytes = stackalloc byte[260];
int length;
while( !Encoding.UTF8.TryGetBytes( value, bytes, out length ) )
bytes = new byte[bytes.Length * 4];
bytes[length] = 0;
bytes = bytes[ ..length ];
return Get( bytes, 0, length, crc );
}

/// <summary>
Expand Down
Loading