11using System . Collections . Concurrent ;
2+ using System . Collections . Frozen ;
23using System . Diagnostics ;
34using System . Runtime . InteropServices ;
45using System . Text ;
@@ -61,10 +62,10 @@ public class ALoadoutSynchronizer : ILoadoutSynchronizer
6162 private readonly ISynchronizerService _synchronizerService ;
6263 private readonly IServiceProvider _serviceProvider ;
6364 private readonly ILoadoutManager _loadoutManager ;
65+ private readonly IGameLocationsService _gameLocationsService ;
6466
6567 private readonly StringPool _fileNamePool = new ( ) ;
6668
67-
6869 /// <summary>
6970 /// Connection.
7071 /// </summary>
@@ -90,6 +91,7 @@ protected ALoadoutSynchronizer(
9091 _synchronizerService = serviceProvider . GetRequiredService < ISynchronizerService > ( ) ;
9192 _jobMonitor = serviceProvider . GetRequiredService < IJobMonitor > ( ) ;
9293 _loadoutManager = serviceProvider . GetRequiredService < ILoadoutManager > ( ) ;
94+ _gameLocationsService = serviceProvider . GetRequiredService < IGameLocationsService > ( ) ;
9395
9496 _fileHashService = fileHashService ;
9597
@@ -440,7 +442,7 @@ private static bool FileIsEnabled(LoadoutItem.ReadOnly arg)
440442 /// <inheritdoc />
441443 public async Task < Dictionary < GamePath , SyncNode > > BuildSyncTree ( Loadout . ReadOnly loadout )
442444 {
443- var metadata = await ReindexState ( loadout . InstallationInstance , ignoreModifiedDates : false , Connection ) ;
445+ var metadata = await ReindexState ( loadout . InstallationInstance ) ;
444446
445447 var currentItems = GetDiskStateForGame ( metadata ) ;
446448 var prevItems = ( ( ILoadoutSynchronizer ) this ) . GetPreviouslyAppliedDiskState ( metadata ) ;
@@ -1161,11 +1163,11 @@ private bool ActionIngestFromDisk(Dictionary<GamePath, SyncNode> syncTree, Loado
11611163 return loadout ;
11621164 }
11631165
1164- public async Task < GameInstallMetadata . ReadOnly > RescanFiles ( GameInstallation gameInstallation , bool ignoreModifiedDates )
1166+ public async Task < GameInstallMetadata . ReadOnly > RescanFiles ( GameInstallation gameInstallation )
11651167 {
11661168 // Make sure the file hashes are up to date
11671169 await _fileHashService . GetFileHashesDb ( ) ;
1168- return await ReindexState ( gameInstallation , ignoreModifiedDates , Connection ) ;
1170+ return await ReindexState ( gameInstallation ) ;
11691171 }
11701172
11711173 /// <summary>
@@ -1464,196 +1466,110 @@ await Parallel.ForEachAsync(files, async (item, _) =>
14641466 await tx . Commit ( ) ;
14651467 }
14661468
1467- public Task < GameInstallMetadata . ReadOnly > ReindexState ( GameInstallation installation , bool ignoreModifiedDates )
1468- {
1469- return ReindexState ( installation , ignoreModifiedDates , Connection ) ;
1470- }
1471-
14721469 /// <summary>
14731470 /// Reindex the state of the game, running a transaction if changes are found
14741471 /// </summary>
1475- private async Task < GameInstallMetadata . ReadOnly > ReindexState ( GameInstallation installation , bool ignoreModifiedDates , IConnection connection )
1472+ public async Task < GameInstallMetadata . ReadOnly > ReindexState ( GameInstallation installation )
14761473 {
14771474 using var _ = await _lock . LockAsync ( ) ;
1478- var originalMetadata = installation . GetMetadata ( connection ) ;
1479- using var tx = connection . BeginTransaction ( ) ;
1475+ var originalMetadata = installation . GetMetadata ( Connection ) ;
1476+ using var tx = Connection . BeginTransaction ( ) ;
14801477
14811478 // Index the state
1482- var changed = await ReindexState ( installation , ignoreModifiedDates , connection , tx ) ;
1483-
1479+ var changed = await ReindexState ( installation , tx ) ;
1480+
14841481 if ( ! originalMetadata . Contains ( GameInstallMetadata . InitialDiskStateTransaction ) )
14851482 {
14861483 // No initial state, so set this transaction as the initial state
14871484 changed = true ;
14881485 tx . Add ( originalMetadata . Id , GameInstallMetadata . InitialDiskStateTransaction , EntityId . From ( TxId . Tmp . Value ) ) ;
14891486 }
1490-
1487+
14911488 if ( changed )
14921489 {
14931490 tx . Add ( installation . GameMetadataId , GameInstallMetadata . LastScannedDiskStateTransactionId , EntityId . From ( TxId . Tmp . Value ) ) ;
14941491 await tx . Commit ( ) ;
14951492 }
1496-
1497- return GameInstallMetadata . Load ( connection . Db , installation . GameMetadataId ) ;
1498- }
1499-
1500- /// <summary>
1501- /// Reindex the state of the game
1502- /// </summary>
1503- public async Task < bool > ReindexState ( GameInstallation installation , bool ignoreModifiedDates , IConnection connection , ITransaction tx )
1504- {
1505- var hashDb = await _fileHashService . GetFileHashesDb ( ) ;
15061493
1507- var gameInstallMetadata = GameInstallMetadata . Load ( connection . Db , installation . GameMetadataId ) ;
1494+ return GameInstallMetadata . Load ( Connection . Db , installation . GameMetadataId ) ;
1495+ }
15081496
1509- var previousDiskStateEntities = gameInstallMetadata . DiskStateEntries ;
1510- var previousDiskState = new Dictionary < GamePath , DiskStateEntry . ReadOnly > ( capacity : previousDiskStateEntities . Count ) ;
1497+ private FrozenDictionary < GamePath , DiskStateEntry . ReadOnly > GetDiskState ( GameInstallMetadata . ReadOnly gameInstallMetadata )
1498+ {
1499+ var entities = gameInstallMetadata . DiskStateEntries ;
1500+ var result = new Dictionary < GamePath , DiskStateEntry . ReadOnly > ( capacity : entities . Count ) ;
15111501
1512- foreach ( var previousDiskStateEntity in previousDiskStateEntities )
1502+ foreach ( var entity in entities )
15131503 {
1514- GamePath path = previousDiskStateEntity . Path ;
1515-
1516- ref var diskStateEntity = ref CollectionsMarshal . GetValueRefOrAddDefault ( previousDiskState , path , out var hasExistingDiskStateEntity ) ;
1517- if ( hasExistingDiskStateEntity )
1504+ GamePath gamePath = entity . Path ;
1505+ ref var entry = ref CollectionsMarshal . GetValueRefOrAddDefault ( result , gamePath , out var isDuplicate ) ;
1506+ if ( isDuplicate )
15181507 {
1519- Logger . LogWarning ( "Duplicate path in disk state: `{Path}`" , path ) ;
1508+ Logger . LogWarning ( "Duplicate path in disk state: `{Path}`" , gamePath ) ;
15201509 }
15211510
1522- diskStateEntity = previousDiskStateEntity ;
1511+ entry = entity ;
15231512 }
15241513
1525- var hasDiskStateChanged = false ;
1514+ return result . ToFrozenDictionary ( ) ;
1515+ }
15261516
1527- var seenPaths = new HashSet < GamePath > ( ) ;
1528- var seenPathsLock = new Lock ( ) ;
1517+ /// <summary>
1518+ /// Reindex the state of the game
1519+ /// </summary>
1520+ public async Task < bool > ReindexState ( GameInstallation installation , ITransaction tx )
1521+ {
1522+ var gameInstallMetadata = GameInstallMetadata . Load ( Connection . Db , installation . GameMetadataId ) ;
1523+ var previousState = GetDiskState ( gameInstallMetadata ) ;
1524+
1525+ var indexGameResult = await _gameLocationsService . IndexGame (
1526+ installation : installation ,
1527+ previousDiskState : previousState ,
1528+ filter : GamePathFilter ,
1529+ cancellationToken : CancellationToken . None
1530+ ) ;
15291531
1530- foreach ( var locationPair in installation . LocationsRegister . GetTopLevelLocations ( ) )
1532+ foreach ( var ( gamePath , result ) in indexGameResult . NewFiles )
15311533 {
1532- var ( _, locationPath ) = locationPair ;
1533- if ( ! locationPath . DirectoryExists ( ) ) continue ;
1534-
1535- await Parallel . ForEachAsync ( locationPath . EnumerateFiles ( ) , async ( file , token ) =>
1534+ _ = new DiskStateEntry . New ( tx , tx . TempId ( DiskStateEntry . EntryPartition ) )
15361535 {
1537- try
1538- {
1539- var gamePath = installation . LocationsRegister . ToGamePath ( file ) ;
1540- if ( ShouldIgnorePathWhenIndexing ( gamePath ) ) return ;
1541-
1542- bool isNewPath ;
1543- lock ( seenPathsLock )
1544- {
1545- isNewPath = seenPaths . Add ( gamePath ) ;
1546- }
1547-
1548- if ( ! isNewPath )
1549- {
1550- Logger . LogDebug ( "Skipping already indexed file at `{Path}`" , file ) ;
1551- return ;
1552- }
1553-
1554- if ( previousDiskState . TryGetValue ( gamePath , out var previousDiskStateEntry ) )
1555- {
1556- var fileInfo = file . FileInfo ;
1557- var writeTimeUtc = new DateTimeOffset ( fileInfo . LastWriteTimeUtc ) ;
1558-
1559- // If the files don't match, update the entry
1560- if ( writeTimeUtc != previousDiskStateEntry . LastModified || fileInfo . Size != previousDiskStateEntry . Size || ignoreModifiedDates )
1561- {
1562- var newHash = await MaybeHashFile ( hashDb , gamePath , file ,
1563- fileInfo , token
1564- ) ;
1565- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Size , fileInfo . Size ) ;
1566- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Hash , newHash ) ;
1567- tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . LastModified , writeTimeUtc ) ;
1568- hasDiskStateChanged = true ;
1569- }
1570- }
1571- else
1572- {
1573- var newHash = await MaybeHashFile ( hashDb , gamePath , file ,
1574- file . FileInfo , token
1575- ) ;
1576-
1577- _ = new DiskStateEntry . New ( tx , tx . TempId ( DiskStateEntry . EntryPartition ) )
1578- {
1579- Path = gamePath . ToGamePathParentTuple ( gameInstallMetadata . Id ) ,
1580- Hash = newHash ,
1581- Size = file . FileInfo . Size ,
1582- LastModified = file . FileInfo . LastWriteTimeUtc ,
1583- GameId = gameInstallMetadata . Id ,
1584- } ;
1585-
1586- hasDiskStateChanged = true ;
1587- }
1588- }
1589- catch ( Exception ex )
1590- {
1591- throw ex ;
1592- }
1593- } ) ;
1536+ Path = gamePath . ToGamePathParentTuple ( gameInstallMetadata . Id ) ,
1537+ Hash = result . Hash ,
1538+ Size = result . Size ,
1539+ LastModified = result . LastModified ,
1540+ GameId = gameInstallMetadata . Id ,
1541+ } ;
15941542 }
15951543
1596- // NOTE(erri120): remove files from the disk state that don't exist on disk anymore
1597- foreach ( var entry in previousDiskState . Values )
1544+ foreach ( var ( gamePath , result ) in indexGameResult . ModifiedFiles )
15981545 {
1599- if ( seenPaths . Contains ( entry . Path ) ) continue ;
1600- tx . Delete ( entry . Id , recursive : false ) ;
1601- hasDiskStateChanged = true ;
1602- }
1546+ var didFind = previousState . TryGetValue ( gamePath , out var previousDiskStateEntry ) ;
1547+ Debug . Assert ( didFind , "modified file should be in previous state" ) ;
16031548
1604- if ( hasDiskStateChanged ) tx . Add ( gameInstallMetadata . Id , GameInstallMetadata . LastScannedDiskStateTransaction , EntityId . From ( TxId . Tmp . Value ) ) ;
1605- return hasDiskStateChanged ;
1606- }
1607-
1608- private async ValueTask < Hash > MaybeHashFile ( IDb hashDb , GamePath gamePath , AbsolutePath file , IFileEntry fileInfo , CancellationToken token )
1609- {
1610- Hash ? diskMinimalHash = null ;
1549+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Size , result . Size ) ;
1550+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . Hash , result . Hash ) ;
1551+ tx . Add ( previousDiskStateEntry . Id , DiskStateEntry . LastModified , result . LastModified ) ;
1552+ }
16111553
1612- var foundHash = Hash . Zero ;
1613- var needFullHash = true ;
1614-
1615- // Look for all known files that match the path
1616- foreach ( var matchingPath in PathHashRelation . FindByPath ( hashDb , gamePath . Path ) )
1554+ foreach ( var gamePath in indexGameResult . RemovedFiles )
16171555 {
1618- // Make sure the size matches
1619- var hash = matchingPath . Hash ;
1620- if ( hash . Size . Value != fileInfo . Size )
1621- continue ;
1622-
1623- // If the minimal hash matches, then we can use the xxHash3 hash
1624- await using ( var fileStream = file . Read ( ) )
1625- {
1626- diskMinimalHash ??= await MultiHasher . MinimalHash ( fileStream , cancellationToken : token ) ;
1627- }
1628-
1629- if ( hash . MinimalHash == diskMinimalHash )
1630- {
1631- // We previously found a hash that matches the minimal hash, make sure the xxHash3 matches, otherwise we
1632- // have a hash collision
1633- if ( foundHash != Hash . Zero && foundHash != hash . XxHash3 )
1634- {
1635- // We have a hash collision, so we need to do a full hash
1636- needFullHash = true ;
1637- break ;
1638- }
1556+ var didFind = previousState . TryGetValue ( gamePath , out var previousDiskStateEntry ) ;
1557+ Debug . Assert ( didFind , "modified file should be in previous state" ) ;
16391558
1640- // Store the hash
1641- foundHash = hash . XxHash3 ;
1642- needFullHash = false ;
1643- }
1559+ tx . Delete ( previousDiskStateEntry . Id , recursive : false ) ;
16441560 }
1645-
1646- if ( ! needFullHash )
1647- return foundHash ;
16481561
1649- Logger . LogDebug ( "Didn't find matching hash data for file `{Path}` or found multiple matches, falling back to doing a full hash" , file ) ;
1650- return await file . XxHash3Async ( token : token ) ;
1562+ var hasChanged = indexGameResult . NewFiles . Count != 0 || indexGameResult . ModifiedFiles . Count != 0 || indexGameResult . RemovedFiles . Count != 0 ;
1563+ if ( ! hasChanged ) return false ;
1564+
1565+ tx . Add ( gameInstallMetadata . Id , GameInstallMetadata . LastScannedDiskStateTransaction , EntityId . From ( TxId . Tmp . Value ) ) ;
1566+ return true ;
16511567 }
16521568
16531569 public async Task ActivateLoadout ( LoadoutId loadoutId )
16541570 {
16551571 var loadout = Loadout . Load ( Connection . Db , loadoutId ) ;
1656- var reindexed = await ReindexState ( loadout . InstallationInstance , ignoreModifiedDates : false , Connection ) ;
1572+ var reindexed = await ReindexState ( loadout . InstallationInstance ) ;
16571573
16581574 var tree = BuildSyncTree ( DiskStateToPathPartPair ( reindexed . DiskStateEntries ) , DiskStateToPathPartPair ( reindexed . DiskStateEntries ) , loadout ) ;
16591575 ProcessSyncTree ( tree ) ;
@@ -1694,7 +1610,7 @@ private LoadoutGameFilesGroup.New CreateLoadoutGameFilesGroup(LoadoutId loadout,
16941610 /// Files ignored by this method will not be included in the sync tree. Prefer not including
16951611 /// the path in the first place instead of using this method.
16961612 /// </remarks>
1697- protected virtual bool ShouldIgnorePathWhenIndexing ( GamePath path ) => false ;
1613+ protected virtual IGamePathFilter GamePathFilter { get ; } = Synchronizers . GamePathFilters . Empty ;
16981614
16991615 /// <summary>
17001616 /// Gets a set of files intrinsic to this game. Such as mod order files, preference files, etc.
@@ -1709,7 +1625,7 @@ public virtual Dictionary<GamePath, IIntrinsicFile> IntrinsicFiles(Loadout.ReadO
17091625 public async Task ResetToOriginalGameState ( GameInstallation installation , LocatorId [ ] locatorIds )
17101626 {
17111627 var gameState = _fileHashService . GetGameFiles ( ( installation . Store , locatorIds ) ) ;
1712- var metaData = await ReindexState ( installation , ignoreModifiedDates : false , Connection ) ;
1628+ var metaData = await ReindexState ( installation ) ;
17131629
17141630 List < PathPartPair > diskState = [ ] ;
17151631
0 commit comments