@@ -353,7 +353,7 @@ private static bool FileIsEnabled(LoadoutItem.ReadOnly arg)
353
353
/// <inheritdoc />
354
354
public async Task < Dictionary < GamePath , SyncNode > > BuildSyncTree ( Loadout . ReadOnly loadout )
355
355
{
356
- var metadata = await ReindexState ( loadout . InstallationInstance , Connection ) ;
356
+ var metadata = await ReindexState ( loadout . InstallationInstance , false , Connection ) ;
357
357
var previouslyApplied = loadout . Installation . GetLastAppliedDiskState ( ) ;
358
358
return BuildSyncTree ( DiskStateToPathPartPair ( metadata . DiskStateEntries ) , DiskStateToPathPartPair ( previouslyApplied ) , loadout ) ;
359
359
}
@@ -908,11 +908,11 @@ private bool ActionIngestFromDisk(Dictionary<GamePath, SyncNode> syncTree, Loado
908
908
return await RunActions ( tree , loadout ) ;
909
909
}
910
910
911
- public async Task < GameInstallMetadata . ReadOnly > RescanFiles ( GameInstallation gameInstallation )
911
+ public async Task < GameInstallMetadata . ReadOnly > RescanFiles ( GameInstallation gameInstallation , bool ignoreModifiedDates )
912
912
{
913
913
// Make sure the file hashes are up to date
914
914
await _fileHashService . GetFileHashesDb ( ) ;
915
- return await ReindexState ( gameInstallation , Connection ) ;
915
+ return await ReindexState ( gameInstallation , ignoreModifiedDates , Connection ) ;
916
916
}
917
917
918
918
/// <summary>
@@ -1201,14 +1201,14 @@ await Parallel.ForEachAsync(files, async (item, _) =>
1201
1201
/// <summary>
1202
1202
/// Reindex the state of the game, running a transaction if changes are found
1203
1203
/// </summary>
1204
- private async Task < GameInstallMetadata . ReadOnly > ReindexState ( GameInstallation installation , IConnection connection )
1204
+ private async Task < GameInstallMetadata . ReadOnly > ReindexState ( GameInstallation installation , bool ignoreModifiedDates , IConnection connection )
1205
1205
{
1206
1206
using var _ = await _lock . LockAsync ( ) ;
1207
1207
var originalMetadata = installation . GetMetadata ( connection ) ;
1208
1208
using var tx = connection . BeginTransaction ( ) ;
1209
1209
1210
1210
// Index the state
1211
- var changed = await ReindexState ( installation , connection , tx ) ;
1211
+ var changed = await ReindexState ( installation , ignoreModifiedDates , connection , tx ) ;
1212
1212
1213
1213
if ( ! originalMetadata . Contains ( GameInstallMetadata . InitialDiskStateTransaction ) )
1214
1214
{
@@ -1219,6 +1219,7 @@ await Parallel.ForEachAsync(files, async (item, _) =>
1219
1219
1220
1220
if ( changed )
1221
1221
{
1222
+ tx . Add ( installation . GameMetadataId , GameInstallMetadata . LastScannedDiskStateTransactionId , EntityId . From ( TxId . Tmp . Value ) ) ;
1222
1223
await tx . Commit ( ) ;
1223
1224
}
1224
1225
@@ -1228,7 +1229,7 @@ await Parallel.ForEachAsync(files, async (item, _) =>
1228
1229
/// <summary>
1229
1230
/// Reindex the state of the game
1230
1231
/// </summary>
1231
- public async Task < bool > ReindexState ( GameInstallation installation , IConnection connection , ITransaction tx )
1232
+ public async Task < bool > ReindexState ( GameInstallation installation , bool ignoreModifiedDates , IConnection connection , ITransaction tx )
1232
1233
{
1233
1234
var hashDb = await _fileHashService . GetFileHashesDb ( ) ;
1234
1235
@@ -1262,50 +1263,61 @@ public async Task<bool> ReindexState(GameInstallation installation, IConnection
1262
1263
1263
1264
await Parallel . ForEachAsync ( locationPath . EnumerateFiles ( ) , async ( file , token ) =>
1264
1265
{
1265
- var gamePath = installation . LocationsRegister . ToGamePath ( file ) ;
1266
- if ( ShouldIgnorePathWhenIndexing ( gamePath ) ) return ;
1267
-
1268
- bool isNewPath ;
1269
- lock ( seenPathsLock )
1266
+ try
1270
1267
{
1271
- isNewPath = seenPaths . Add ( gamePath ) ;
1272
- }
1268
+ var gamePath = installation . LocationsRegister . ToGamePath ( file ) ;
1269
+ if ( ShouldIgnorePathWhenIndexing ( gamePath ) ) return ;
1273
1270
1274
- if ( ! isNewPath )
1275
- {
1276
- Logger . LogDebug ( "Skipping already indexed file at `{Path}`" , file ) ;
1277
- return ;
1278
- }
1271
+ bool isNewPath ;
1272
+ lock ( seenPathsLock )
1273
+ {
1274
+ isNewPath = seenPaths . Add ( gamePath ) ;
1275
+ }
1279
1276
1280
- if ( previousDiskState . TryGetValue ( gamePath , out var previousDiskStateEntry ) )
1281
- {
1282
- var fileInfo = file . FileInfo ;
1283
- var writeTimeUtc = new DateTimeOffset ( fileInfo . LastWriteTimeUtc ) ;
1277
+ if ( ! isNewPath )
1278
+ {
1279
+ Logger . LogDebug ( "Skipping already indexed file at `{Path}`" , file ) ;
1280
+ return ;
1281
+ }
1282
+
1283
+ if ( previousDiskState . TryGetValue ( gamePath , out var previousDiskStateEntry ) )
1284
+ {
1285
+ var fileInfo = file . FileInfo ;
1286
+ var writeTimeUtc = new DateTimeOffset ( fileInfo . LastWriteTimeUtc ) ;
1284
1287
1285
- // If the files don't match, update the entry
1286
- if ( writeTimeUtc != previousDiskStateEntry . LastModified || fileInfo . Size != previousDiskStateEntry . Size )
1288
+ // If the files don't match, update the entry
1289
+ if ( writeTimeUtc != previousDiskStateEntry . LastModified || fileInfo . Size != previousDiskStateEntry . Size || ignoreModifiedDates )
1290
+ {
1291
+ var newHash = await MaybeHashFile ( hashDb , gamePath , file ,
1292
+ fileInfo , token
1293
+ ) ;
1294
+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Size , fileInfo . Size ) ;
1295
+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Hash , newHash ) ;
1296
+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . LastModified , writeTimeUtc ) ;
1297
+ hasDiskStateChanged = true ;
1298
+ }
1299
+ }
1300
+ else
1287
1301
{
1288
- var newHash = await MaybeHashFile ( hashDb , gamePath , file , fileInfo , token ) ;
1289
- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Size , fileInfo . Size ) ;
1290
- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Hash , newHash ) ;
1291
- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . LastModified , writeTimeUtc ) ;
1302
+ var newHash = await MaybeHashFile ( hashDb , gamePath , file ,
1303
+ file . FileInfo , token
1304
+ ) ;
1305
+
1306
+ _ = new DiskStateEntry . New ( tx , tx . TempId ( DiskStateEntry . EntryPartition ) )
1307
+ {
1308
+ Path = gamePath . ToGamePathParentTuple ( gameInstallMetadata . Id ) ,
1309
+ Hash = newHash ,
1310
+ Size = file . FileInfo . Size ,
1311
+ LastModified = file . FileInfo . LastWriteTimeUtc ,
1312
+ GameId = gameInstallMetadata . Id ,
1313
+ } ;
1314
+
1292
1315
hasDiskStateChanged = true ;
1293
1316
}
1294
1317
}
1295
- else
1318
+ catch ( Exception ex )
1296
1319
{
1297
- var newHash = await MaybeHashFile ( hashDb , gamePath , file , file . FileInfo , token ) ;
1298
-
1299
- _ = new DiskStateEntry . New ( tx , tx . TempId ( DiskStateEntry . EntryPartition ) )
1300
- {
1301
- Path = gamePath . ToGamePathParentTuple ( gameInstallMetadata . Id ) ,
1302
- Hash = newHash ,
1303
- Size = file . FileInfo . Size ,
1304
- LastModified = file . FileInfo . LastWriteTimeUtc ,
1305
- GameId = gameInstallMetadata . Id ,
1306
- } ;
1307
-
1308
- hasDiskStateChanged = true ;
1320
+ throw ex ;
1309
1321
}
1310
1322
} ) ;
1311
1323
}
@@ -1325,6 +1337,9 @@ await Parallel.ForEachAsync(locationPath.EnumerateFiles(), async (file, token) =
1325
1337
private async ValueTask < Hash > MaybeHashFile ( IDb hashDb , GamePath gamePath , AbsolutePath file , IFileEntry fileInfo , CancellationToken token )
1326
1338
{
1327
1339
Hash ? diskMinimalHash = null ;
1340
+
1341
+ var foundHash = Hash . Zero ;
1342
+ var needFullHash = true ;
1328
1343
1329
1344
// Look for all known files that match the path
1330
1345
foreach ( var matchingPath in PathHashRelation . FindByPath ( hashDb , gamePath . Path ) )
@@ -1338,10 +1353,26 @@ private async ValueTask<Hash> MaybeHashFile(IDb hashDb, GamePath gamePath, Absol
1338
1353
diskMinimalHash ??= await MultiHasher . MinimalHash ( file , token ) ;
1339
1354
1340
1355
if ( hash . MinimalHash == diskMinimalHash )
1341
- return hash . XxHash3 ;
1356
+ {
1357
+ // We previously found a hash that matches the minimal hash, make sure the xxHash3 matches, otherwise we
1358
+ // have a hash collision
1359
+ if ( foundHash != Hash . Zero && foundHash != hash . XxHash3 )
1360
+ {
1361
+ // We have a hash collision, so we need to do a full hash
1362
+ needFullHash = true ;
1363
+ break ;
1364
+ }
1365
+
1366
+ // Store the hash
1367
+ foundHash = hash . XxHash3 ;
1368
+ needFullHash = false ;
1369
+ }
1342
1370
}
1371
+
1372
+ if ( ! needFullHash )
1373
+ return foundHash ;
1343
1374
1344
- Logger . LogDebug ( "Didn't find matching hash data for file `{Path}`, falling back to doing a full hash" , file ) ;
1375
+ Logger . LogDebug ( "Didn't find matching hash data for file `{Path}` or found multiple matches , falling back to doing a full hash" , file ) ;
1345
1376
return await file . XxHash3Async ( token : token ) ;
1346
1377
}
1347
1378
@@ -1460,7 +1491,7 @@ public Optional<LoadoutId> GetCurrentlyActiveLoadout(GameInstallation installati
1460
1491
public async Task ActivateLoadout ( LoadoutId loadoutId )
1461
1492
{
1462
1493
var loadout = Loadout . Load ( Connection . Db , loadoutId ) ;
1463
- var reindexed = await ReindexState ( loadout . InstallationInstance , Connection ) ;
1494
+ var reindexed = await ReindexState ( loadout . InstallationInstance , false , Connection ) ;
1464
1495
1465
1496
var tree = BuildSyncTree ( DiskStateToPathPartPair ( reindexed . DiskStateEntries ) , DiskStateToPathPartPair ( reindexed . DiskStateEntries ) , loadout ) ;
1466
1497
ProcessSyncTree ( tree ) ;
@@ -1664,7 +1695,7 @@ public async Task DeleteLoadout(LoadoutId loadoutId, GarbageCollectorRunMode gcR
1664
1695
public async Task ResetToOriginalGameState ( GameInstallation installation , LocatorId [ ] locatorIds )
1665
1696
{
1666
1697
var gameState = _fileHashService . GetGameFiles ( ( installation . Store , locatorIds ) ) ;
1667
- var metaData = await ReindexState ( installation , Connection ) ;
1698
+ var metaData = await ReindexState ( installation , false , Connection ) ;
1668
1699
1669
1700
List < PathPartPair > diskState = [ ] ;
1670
1701
0 commit comments