8
8
using NexusMods . Abstractions . GameLocators ;
9
9
using NexusMods . Abstractions . Games . FileHashes ;
10
10
using NexusMods . Abstractions . Games . FileHashes . Models ;
11
+ using NexusMods . Abstractions . GOG . Values ;
11
12
using NexusMods . Abstractions . Jobs ;
12
13
using NexusMods . Abstractions . NexusWebApi . Types . V2 ;
13
14
using NexusMods . Abstractions . Settings ;
14
15
using NexusMods . Abstractions . Steam . Values ;
15
16
using NexusMods . Games . FileHashes . DTOs ;
16
17
using NexusMods . Hashing . xxHash3 ;
17
- using NexusMods . MnemonicDB ;
18
+ using NexusMods . HyperDuck ;
18
19
using NexusMods . MnemonicDB . Abstractions ;
19
20
using NexusMods . MnemonicDB . Storage ;
20
21
using NexusMods . MnemonicDB . Storage . RocksDbBackend ;
21
22
using NexusMods . Paths ;
22
23
using NexusMods . Sdk ;
23
24
using NexusMods . Sdk . IO ;
24
25
using BuildId = NexusMods . Abstractions . GOG . Values . BuildId ;
26
+ using Connection = NexusMods . MnemonicDB . Connection ;
25
27
26
28
namespace NexusMods . Games . FileHashes ;
27
29
28
30
internal sealed class FileHashesService : IFileHashesService , IDisposable
29
31
{
32
+ private const string DefaultLanguage = "en-US" ;
33
+
30
34
private readonly ScopedAsyncLock _lock = new ( ) ;
31
35
private readonly FileHashesServiceSettings _settings ;
32
36
private readonly IFileSystem _fileSystem ;
@@ -42,6 +46,8 @@ internal sealed class FileHashesService : IFileHashesService, IDisposable
42
46
private ConnectedDb ? _currentDb ;
43
47
44
48
private readonly ILogger < FileHashesService > _logger ;
49
+ private readonly IQueryEngine _queryEngine ;
50
+ private IQueryMixin _queryMixin ;
45
51
46
52
private record ConnectedDb ( IDb Db , DatomStore Store , Backend Backend , DatabaseInfo DatabaseInfo ) ;
47
53
@@ -54,6 +60,8 @@ public FileHashesService(ILogger<FileHashesService> logger, ISettingsManager set
54
60
_settings = settingsManager . Get < FileHashesServiceSettings > ( ) ;
55
61
_databases = new Dictionary < AbsolutePath , ConnectedDb > ( ) ;
56
62
_provider = provider ;
63
+ _queryEngine = provider . GetRequiredService < IQueryEngine > ( ) ;
64
+ _queryMixin = _queryEngine . DuckDb ;
57
65
58
66
_hashDatabaseLocation = _settings . HashDatabaseLocation . ToPath ( _fileSystem ) ;
59
67
_hashDatabaseLocation . CreateDirectory ( ) ;
@@ -76,7 +84,7 @@ private ConnectedDb OpenDb(DatabaseInfo databaseInfo)
76
84
} ;
77
85
78
86
var store = new DatomStore ( _provider . GetRequiredService < ILogger < DatomStore > > ( ) , settings , backend ) ;
79
- var connection = new Connection ( _provider . GetRequiredService < ILogger < Connection > > ( ) , store , _provider , [ ] , readOnlyMode : true ) ;
87
+ var connection = new Connection ( _provider . GetRequiredService < ILogger < Connection > > ( ) , store , _provider , [ ] , readOnlyMode : true , prefix : "hashes" , queryEngine : _queryEngine ) ;
80
88
var connectedDb = new ConnectedDb ( connection . Db , store , backend , databaseInfo ) ;
81
89
82
90
_databases [ databaseInfo . Path ] = connectedDb ;
@@ -333,17 +341,48 @@ public IEnumerable<GameFileRecord> GetGameFiles(LocatorIdsWithGameStore locatorI
333
341
334
342
if ( gameStore == GameStore . GOG )
335
343
{
344
+ HashSet < GogBuild . ReadOnly > gogBuilds = [ ] ;
345
+ HashSet < ProductId > gogProducts = [ ] ;
346
+ Dictionary < EntityId , GogManifest . ReadOnly > gogManifests = [ ] ;
347
+
348
+ // So first we find all the valid build Ids, and then assume that everything else is a product Id
336
349
foreach ( var id in locatorIds )
337
350
{
338
351
if ( ! ulong . TryParse ( id . Value , out var parsedId ) )
339
352
continue ;
340
353
341
354
var gogId = BuildId . From ( parsedId ) ;
342
355
343
- if ( ! GogBuild . FindByBuildId ( Current , gogId ) . TryGetFirst ( out var firstBuild ) )
356
+ if ( GogBuild . FindByBuildId ( Current , gogId ) . TryGetFirst ( out var firstBuild ) )
357
+ {
358
+ gogBuilds . Add ( firstBuild ) ;
344
359
continue ;
360
+ }
361
+
362
+ var productId = ProductId . From ( parsedId ) ;
363
+ gogProducts . Add ( productId ) ;
364
+ }
365
+
366
+ // Now we emit all the files from the build products, and then also from any secondary products
367
+ foreach ( var build in gogBuilds )
368
+ {
369
+ foreach ( var depot in build . Depots )
370
+ {
371
+ // We only care about the productId of the build, and the productIds of the secondary products
372
+ if ( ! ( depot . ProductId == build . ProductId || gogProducts . Contains ( depot . ProductId ) ) )
373
+ continue ;
374
+
375
+ // If there is a language setting for the files, they have to be the same as the default language
376
+ if ( ! ( depot . Languages . Count == 0 || depot . Languages . Contains ( DefaultLanguage ) ) )
377
+ continue ;
378
+
379
+ gogManifests [ depot . Manifest . Id ] = depot . Manifest ;
380
+ }
381
+ }
345
382
346
- foreach ( var file in firstBuild . Files )
383
+ foreach ( var ( _ , manifest ) in gogManifests )
384
+ {
385
+ foreach ( var file in manifest . Files )
347
386
{
348
387
yield return new GameFileRecord
349
388
{
@@ -354,6 +393,7 @@ public IEnumerable<GameFileRecord> GetGameFiles(LocatorIdsWithGameStore locatorI
354
393
} ;
355
394
}
356
395
}
396
+
357
397
}
358
398
else if ( gameStore == GameStore . Steam )
359
399
{
@@ -560,7 +600,7 @@ public LocatorId[] GetLocatorIdsForVersionDefinition(GameStore gameStore, Versio
560
600
{
561
601
if ( gameStore == GameStore . GOG )
562
602
{
563
- return versionDefinition . GogBuilds . Select ( build => LocatorId . From ( build . BuildId . ToString ( ) ) ) . ToArray ( ) ;
603
+ return versionDefinition . GogBuilds . Select ( build => LocatorId . From ( build . BuildId ! . Value . ToString ( ) ) ) . ToArray ( ) ;
564
604
}
565
605
566
606
if ( gameStore == GameStore . Steam )
@@ -598,6 +638,42 @@ public Optional<VersionData> SuggestVersionData(GameInstallation gameInstallatio
598
638
. FirstOrOptional ( _ => true ) ;
599
639
}
600
640
641
+ public LocatorId [ ] GetLocatorIdsForGame ( GameInstallation gameInstallation )
642
+ {
643
+ if ( gameInstallation . Store == GameStore . Steam )
644
+ {
645
+ var ids = _queryMixin . Query < DepotId > ( "SELECT * FROM file_hashes.resolve_steam_depots({gameInstallation.GameMetadataId});" )
646
+ . Select ( id => LocatorId . From ( id . Value . ToString ( ) ) )
647
+ . ToArray ( ) ;
648
+ return ids ;
649
+ }
650
+ else if ( gameInstallation . Store == GameStore . GOG )
651
+ {
652
+ if ( ! _queryMixin . Query < ( BuildId , ProductId , List < ProductId > ) > ( "SELECT BuildId, BuildProductId, ProductIds FROM file_hashes.resolve_gog_build({gameInstallation.GameMetadataId})" )
653
+ . TryGetFirst ( out var found ) )
654
+ return [ ] ;
655
+
656
+ var ids = new List < LocatorId > ( ) ;
657
+
658
+ ids . Add ( LocatorId . From ( found . Item1 . Value . ToString ( ) ) ) ;
659
+
660
+ // We want to add the Build Id and then all the product Ids that are not the same as the Build's product
661
+ foreach ( var productId in found . Item3 )
662
+ {
663
+ if ( productId == found . Item2 )
664
+ continue ;
665
+
666
+ ids . Add ( LocatorId . From ( productId . Value . ToString ( ) ) ) ;
667
+ }
668
+
669
+ return ids . ToArray ( ) ;
670
+ }
671
+ else
672
+ {
673
+ throw new NotSupportedException ( "No way to get locator IDs for: " + gameInstallation . Store ) ;
674
+ }
675
+ }
676
+
601
677
/// <inheritdoc/>
602
678
public void Dispose ( )
603
679
{
0 commit comments