Skip to content

Commit b9d596c

Browse files
authored
Game Hash DB Builder (#2527)
* Work on building the game hashes DB from indexed data * Missing project file * Latest changes to add Polly to the Steam/GOG clients * Cleanup the verbs a bit * Update the build command to hash the result * Move some files around that are in the wrong place, make a impl library for FileHashes * Implement a file hash service. Can download the hash database and update it. * WIP using the hash database in the game indexing * Fixing some tests failing from DI * WIP, still trying to fix the tests * Fix the tests I broke by using a struct 🤦 * Fix the endian issues * Bit of cleanup and some comments * Bit more cleanup * Limit how often we spam the Github server for releases * Clean up a lock we likely didn't need * Switch to a manually created manifest.json file * Handle PR feedback * Don't always use the global chunk cache, only when needed * Revert the change to the synchronizer, we're going to need to fix this in another way * Fix the nuget references * And fix the abstractions namespace as well * Try and fix the issues in CI * Put the file hashes in their own folder so we don't clobber everything.
1 parent fadd08b commit b9d596c

File tree

63 files changed

+1717
-228
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+1717
-228
lines changed

Abstractions/NexusMods.Abstractions.Steam/ISteamSession.cs

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ namespace NexusMods.Abstractions.Steam;
99
/// </summary>
1010
public interface ISteamSession
1111
{
12+
/// <summary>
13+
/// Connect to the Steam session, performing any necessary setup.
14+
/// </summary>
15+
public Task Connect(CancellationToken token);
16+
1217
/// <summary>
1318
/// Get the product info for the specified app ID
1419
/// </summary>

Directory.Packages.props

+3-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
1717
<PackageVersion Include="Nerdbank.FullDuplexStream" Version="1.1.12" />
1818
<PackageVersion Include="Nerdbank.Streams" Version="2.11.79" />
19-
<PackageVersion Include="NexusMods.MnemonicDB" Version="0.9.98" />
20-
<PackageVersion Include="NexusMods.MnemonicDB.Abstractions" Version="0.9.98" />
19+
<PackageVersion Include="NexusMods.MnemonicDB" Version="0.9.99" />
20+
<PackageVersion Include="NexusMods.MnemonicDB.Abstractions" Version="0.9.99" />
2121
<PackageVersion Include="NexusMods.Hashing.xxHash3.Paths" Version="3.0.3" />
2222
<PackageVersion Include="NexusMods.Hashing.xxHash3" Version="3.0.3" />
2323
<PackageVersion Include="NexusMods.Paths" Version="0.15.0" />
@@ -138,7 +138,7 @@
138138
<PackageVersion Include="Humanizer" Version="2.14.1" />
139139
<PackageVersion Include="ini-parser-netstandard" Version="2.5.2" />
140140
<PackageVersion Include="Mutagen.Bethesda.Skyrim" Version="0.44.0" />
141-
<PackageVersion Include="NexusMods.MnemonicDB.SourceGenerator" Version="0.9.98" />
141+
<PackageVersion Include="NexusMods.MnemonicDB.SourceGenerator" Version="0.9.99" />
142142
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.14" />
143143
<PackageVersion Include="OneOf" Version="3.0.271" />
144144
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />

NexusMods.App.sln

+14
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.GOG"
292292
EndProject
293293
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Logging", "src\Abstractions\NexusMods.Abstractions.Logging\NexusMods.Abstractions.Logging.csproj", "{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}"
294294
EndProject
295+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Games.FileHashes", "src\Abstractions\NexusMods.Abstractions.Games.FileHashes\NexusMods.Abstractions.Games.FileHashes.csproj", "{E2DB1DF4-9934-4119-BA90-196FDDB0904A}"
296+
EndProject
297+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.FileHashes", "src\NexusMods.Games.FileHashes\NexusMods.Games.FileHashes.csproj", "{71D8CF63-A287-45AB-B251-676A54576C1D}"
298+
EndProject
295299
Global
296300
GlobalSection(SolutionConfigurationPlatforms) = preSolution
297301
Debug|Any CPU = Debug|Any CPU
@@ -770,6 +774,14 @@ Global
770774
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
771775
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
772776
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE}.Release|Any CPU.Build.0 = Release|Any CPU
777+
{E2DB1DF4-9934-4119-BA90-196FDDB0904A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
778+
{E2DB1DF4-9934-4119-BA90-196FDDB0904A}.Debug|Any CPU.Build.0 = Debug|Any CPU
779+
{E2DB1DF4-9934-4119-BA90-196FDDB0904A}.Release|Any CPU.ActiveCfg = Release|Any CPU
780+
{E2DB1DF4-9934-4119-BA90-196FDDB0904A}.Release|Any CPU.Build.0 = Release|Any CPU
781+
{71D8CF63-A287-45AB-B251-676A54576C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
782+
{71D8CF63-A287-45AB-B251-676A54576C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
783+
{71D8CF63-A287-45AB-B251-676A54576C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
784+
{71D8CF63-A287-45AB-B251-676A54576C1D}.Release|Any CPU.Build.0 = Release|Any CPU
773785
EndGlobalSection
774786
GlobalSection(SolutionProperties) = preSolution
775787
HideSolutionNode = FALSE
@@ -906,6 +918,8 @@ Global
906918
{F7FD18A7-2F00-4EB6-84FC-15C57326FDEB} = {D7E9D8F5-8AC8-4ADA-B219-C549084AD84C}
907919
{03AC4F34-E69A-41E3-9F00-EE5A558D01B9} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
908920
{9DE1C2AC-927A-4BC6-B2A1-7016902F8BAE} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
921+
{E2DB1DF4-9934-4119-BA90-196FDDB0904A} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
922+
{71D8CF63-A287-45AB-B251-676A54576C1D} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
909923
EndGlobalSection
910924
GlobalSection(ExtensibilityGlobals) = postSolution
911925
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}

src/Abstractions/NexusMods.Abstractions.Cli/RendererExtensions.cs

+49
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ public static async ValueTask Text(this IRenderer renderer, string text)
7373
await renderer.RenderAsync(Renderable.Text(text));
7474
}
7575

76+
/// <summary>
77+
/// Renders the given text to the renderer and follows it up with a newline
78+
/// </summary>
79+
/// <param name="renderer"></param>
80+
/// <param name="text"></param>
81+
public static async ValueTask TextLine(this IRenderer renderer, string text)
82+
{
83+
await renderer.RenderAsync(Renderable.Text(text + "\n"));
84+
}
85+
7686
/// <summary>
7787
/// Starts a progress bar box in the renderer that will be stopped when the returned disposable is disposed
7888
/// </summary>
@@ -103,6 +113,17 @@ public static async ValueTask Text(this IRenderer renderer, string template, par
103113
await renderer.RenderAsync(Renderable.Text(template, args.Select(a => a.ToString()!).ToArray()));
104114
}
105115

116+
/// <summary>
117+
/// Renders the text to the renderer with the given arguments and template, followed by a newline
118+
/// </summary>
119+
/// <param name="renderer"></param>
120+
/// <param name="text"></param>
121+
public static async ValueTask TextLine(this IRenderer renderer, string template, params object[] args)
122+
{
123+
// Todo: implement custom conversion and formatting for the arguments
124+
await renderer.RenderAsync(Renderable.Text(template + "\n", args.Select(a => a.ToString()!).ToArray()));
125+
}
126+
106127
/// <summary>
107128
/// Renders the text to the renderer with the given arguments and template
108129
/// </summary>
@@ -138,6 +159,18 @@ public static async Task Error(this IRenderer renderer, Exception ex, string tem
138159
await renderer.Text(template, args);
139160
await renderer.Text("Error: {0}", ex);
140161
}
162+
163+
/// <summary>
164+
/// Prints an error message to the renderer, and returns an error code
165+
/// </summary>
166+
/// <param name="renderer"></param>
167+
/// <param name="ex"></param>
168+
/// <param name="template"></param>
169+
/// <param name="args"></param>
170+
public static async Task Error(this IRenderer renderer, string template, params object[] args)
171+
{
172+
await renderer.Text(template, args);
173+
}
141174

142175
/// <summary>
143176
/// Creates a new progress task with the given text, the task will be deleted when the returned disposable is disposed
@@ -163,4 +196,20 @@ public static async IAsyncEnumerable<T> WithProgress<T>(this T[] items, IRendere
163196
yield return item;
164197
}
165198
}
199+
200+
/// <summary>
201+
/// Wraps the enumeration in a progress task that will update the progress bar as the items are enumerated
202+
/// </summary>
203+
public static async IAsyncEnumerable<T> WithProgress<T>(this IEnumerable<T> items, IRenderer renderer, string text)
204+
{
205+
var array = items.ToArray();
206+
var increment = 1.0 / array.Length;
207+
await using var task = await renderer.StartProgressTask(text);
208+
209+
foreach (var item in array)
210+
{
211+
await task.IncrementProgress(increment);
212+
yield return item;
213+
}
214+
}
166215
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Hashes;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes;
7+
8+
/// <summary>
9+
/// An attribute for a CRC32 hash.
10+
/// </summary>
11+
public class Crc32Attribute(string ns, string name) : ScalarAttribute<Crc32, UInt32, UInt32Serializer>(ns, name)
12+
{
13+
protected override uint ToLowLevel(Crc32 value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override Crc32 FromLowLevel(uint value, AttributeResolver resolver)
19+
{
20+
return Crc32.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.GOG.Values;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes.Gog;
7+
8+
/// <summary>
9+
/// An attribute for storing GOG build IDs.
10+
/// </summary>
11+
public class BuildIdAttribute(string ns, string name) : ScalarAttribute<BuildId, ulong, UInt64Serializer>(ns, name)
12+
{
13+
protected override ulong ToLowLevel(BuildId value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override BuildId FromLowLevel(ulong value, AttributeResolver resolver)
19+
{
20+
return BuildId.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.GOG.Values;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes.Gog;
7+
8+
/// <summary>
9+
/// An attribute for storing GOG product IDs.
10+
/// </summary>
11+
public class ProductIdAttribute(string ns, string name) : ScalarAttribute<ProductId, ulong, UInt64Serializer>(ns, name)
12+
{
13+
protected override ulong ToLowLevel(ProductId value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override ProductId FromLowLevel(ulong value, AttributeResolver resolver)
19+
{
20+
return ProductId.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Hashes;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes;
7+
8+
/// <summary>
9+
/// An attribute for storing MD5 hashes.
10+
/// </summary>
11+
public class Md5Attribute(string ns, string name) : ScalarAttribute<Md5, UInt128, UInt128Serializer>(ns, name)
12+
{
13+
protected override UInt128 ToLowLevel(Md5 value)
14+
{
15+
return value.ToUInt128();
16+
}
17+
18+
protected override Md5 FromLowLevel(UInt128 value, AttributeResolver resolver)
19+
{
20+
return Md5.FromUInt128(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Hashes;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes;
7+
8+
/// <summary>
9+
/// An attribute for a SHA1 hash
10+
/// </summary>
11+
public class Sha1Attribute(string ns, string name) : ScalarAttribute<Sha1, Memory<byte>, BlobSerializer>(ns, name)
12+
{
13+
protected override Memory<byte> ToLowLevel(Sha1 value)
14+
{
15+
return value.ToArray();
16+
}
17+
18+
protected override Sha1 FromLowLevel(Memory<byte> value, AttributeResolver resolver)
19+
{
20+
return Sha1.From(value.Span);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Steam.Values;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes.Steam;
7+
8+
/// <summary>
9+
/// An attribute for a Steam App ID.
10+
/// </summary>
11+
public class AppIdAttribute(string ns, string name) : ScalarAttribute<AppId, uint, UInt32Serializer>(ns, name)
12+
{
13+
protected override uint ToLowLevel(AppId value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override AppId FromLowLevel(uint value, AttributeResolver resolver)
19+
{
20+
return AppId.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Steam.Values;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes.Steam;
7+
8+
/// <summary>
9+
/// A <see cref="DepotId"/> attribute.
10+
/// </summary>
11+
public class DepotIdAttribute(string ns, string name) : ScalarAttribute<DepotId, uint, UInt32Serializer>(ns, name)
12+
{
13+
protected override uint ToLowLevel(DepotId value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override DepotId FromLowLevel(uint value, AttributeResolver resolver)
19+
{
20+
return DepotId.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using NexusMods.Abstractions.Steam.Values;
2+
using NexusMods.MnemonicDB.Abstractions;
3+
using NexusMods.MnemonicDB.Abstractions.Attributes;
4+
using NexusMods.MnemonicDB.Abstractions.ValueSerializers;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Attributes.Steam;
7+
8+
/// <summary>
9+
/// A manifest ID attribute.
10+
/// </summary>
11+
public class ManifestIdAttribute(string ns, string name) : ScalarAttribute<ManifestId, ulong, UInt64Serializer>(ns, name)
12+
{
13+
protected override ulong ToLowLevel(ManifestId value)
14+
{
15+
return value.Value;
16+
}
17+
18+
protected override ManifestId FromLowLevel(ulong value, AttributeResolver resolver)
19+
{
20+
return ManifestId.From(value);
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using NexusMods.MnemonicDB.Abstractions;
2+
3+
namespace NexusMods.Abstractions.Games.FileHashes;
4+
5+
/// <summary>
6+
/// Interface for the file hashes service, which provides a way to download and update the file hashes database
7+
/// </summary>
8+
public interface IFileHashesService
9+
{
10+
/// <summary>
11+
/// Force an update of the file hashes database
12+
/// </summary>
13+
public Task ForceUpdate();
14+
15+
/// <summary>
16+
/// Get the file hashes database, downloading it if necessary
17+
/// </summary>
18+
public ValueTask<IDb> GetFileHashesDb();
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using NexusMods.Abstractions.Games.FileHashes.Attributes.Gog;
2+
using NexusMods.MnemonicDB.Abstractions.Attributes;
3+
using NexusMods.MnemonicDB.Abstractions.Models;
4+
using OperatingSystem = NexusMods.Abstractions.Games.FileHashes.Values.OperatingSystem;
5+
6+
namespace NexusMods.Abstractions.Games.FileHashes.Models;
7+
8+
/// <summary>
9+
/// Metadata for a GOG build.
10+
/// </summary>
11+
public partial class GogBuild : IModelDefinition
12+
{
13+
private const string Namespace = "NexusMods.Abstractions.Games.FileHashes.GogBuild";
14+
15+
/// <summary>
16+
/// The GOG build ID.
17+
/// </summary>
18+
public static readonly BuildIdAttribute BuildId = new(Namespace, nameof(BuildId)) { IsIndexed = true };
19+
20+
/// <summary>
21+
/// The GOG product ID.
22+
/// </summary>
23+
public static readonly ProductIdAttribute ProductId = new(Namespace, nameof(ProductId)) { IsIndexed = true };
24+
25+
/// <summary>
26+
/// The Operating System the build is for.
27+
/// </summary>
28+
public static readonly EnumByteAttribute<OperatingSystem> OperatingSystem = new(Namespace, nameof(OperatingSystem));
29+
30+
/// <summary>
31+
/// The version string of the GOG build.
32+
/// </summary>
33+
public static readonly StringAttribute VersionName = new(Namespace, nameof(Version));
34+
35+
/// <summary>
36+
/// Various tags for the build
37+
/// </summary>
38+
public static readonly StringsAttribute Tags = new(Namespace, nameof(Tags));
39+
40+
/// <summary>
41+
/// True if the build is public, false if it is private.
42+
/// </summary>
43+
public static readonly BooleanAttribute Public = new(Namespace, nameof(Public));
44+
45+
/// <summary>
46+
/// The files in the GOG build.
47+
/// </summary>
48+
public static readonly ReferencesAttribute<PathHashRelation> Files = new(Namespace, nameof(Files));
49+
}

0 commit comments

Comments
 (0)