Skip to content

Commit 24355e6

Browse files
authored
Merge pull request #3203 from Nexus-Mods/main
Backport fixes from main
2 parents 5bc3fd6 + 95d6d94 commit 24355e6

File tree

18 files changed

+228
-109
lines changed

18 files changed

+228
-109
lines changed

Directory.Packages.props

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.4.0" />
1717
<PackageVersion Include="Nerdbank.FullDuplexStream" Version="1.1.12" />
1818
<PackageVersion Include="Nerdbank.Streams" Version="2.11.79" />
19-
<PackageVersion Include="NexusMods.Cascade" Version="0.11.0" />
20-
<PackageVersion Include="NexusMods.Cascade.SourceGenerator" Version="0.11.0" />
21-
<PackageVersion Include="NexusMods.MnemonicDB" Version="0.12.0" />
22-
<PackageVersion Include="NexusMods.MnemonicDB.Abstractions" Version="0.12.0" />
19+
<PackageVersion Include="NexusMods.Cascade" Version="0.12.0" />
20+
<PackageVersion Include="NexusMods.Cascade.SourceGenerator" Version="0.12.0" />
21+
<PackageVersion Include="NexusMods.MnemonicDB" Version="0.13.0" />
22+
<PackageVersion Include="NexusMods.MnemonicDB.Abstractions" Version="0.13.0" />
2323
<PackageVersion Include="NexusMods.Hashing.xxHash3.Paths" Version="3.0.3" />
2424
<PackageVersion Include="NexusMods.Hashing.xxHash3" Version="3.0.3" />
2525
<PackageVersion Include="NexusMods.Archives.Nx" Version="0.6.4" />
@@ -149,7 +149,7 @@
149149
<PackageVersion Include="Humanizer" Version="2.14.1" />
150150
<PackageVersion Include="ini-parser-netstandard" Version="2.5.2" />
151151
<PackageVersion Include="Mutagen.Bethesda.Skyrim" Version="0.44.0" />
152-
<PackageVersion Include="NexusMods.MnemonicDB.SourceGenerator" Version="0.12.0" />
152+
<PackageVersion Include="NexusMods.MnemonicDB.SourceGenerator" Version="0.13.0" />
153153
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.14" />
154154
<PackageVersion Include="OneOf" Version="3.0.271" />
155155
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />

src/NexusMods.Abstractions.Loadouts.Synchronizers/ALoadoutSynchronizer.cs

Lines changed: 76 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ private static bool FileIsEnabled(LoadoutItem.ReadOnly arg)
353353
/// <inheritdoc />
354354
public async Task<Dictionary<GamePath, SyncNode>> BuildSyncTree(Loadout.ReadOnly loadout)
355355
{
356-
var metadata = await ReindexState(loadout.InstallationInstance, Connection);
356+
var metadata = await ReindexState(loadout.InstallationInstance, false, Connection);
357357
var previouslyApplied = loadout.Installation.GetLastAppliedDiskState();
358358
return BuildSyncTree(DiskStateToPathPartPair(metadata.DiskStateEntries), DiskStateToPathPartPair(previouslyApplied), loadout);
359359
}
@@ -908,11 +908,11 @@ private bool ActionIngestFromDisk(Dictionary<GamePath, SyncNode> syncTree, Loado
908908
return await RunActions(tree, loadout);
909909
}
910910

911-
public async Task<GameInstallMetadata.ReadOnly> RescanFiles(GameInstallation gameInstallation)
911+
public async Task<GameInstallMetadata.ReadOnly> RescanFiles(GameInstallation gameInstallation, bool ignoreModifiedDates)
912912
{
913913
// Make sure the file hashes are up to date
914914
await _fileHashService.GetFileHashesDb();
915-
return await ReindexState(gameInstallation, Connection);
915+
return await ReindexState(gameInstallation, ignoreModifiedDates, Connection);
916916
}
917917

918918
/// <summary>
@@ -1201,14 +1201,14 @@ await Parallel.ForEachAsync(files, async (item, _) =>
12011201
/// <summary>
12021202
/// Reindex the state of the game, running a transaction if changes are found
12031203
/// </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)
12051205
{
12061206
using var _ = await _lock.LockAsync();
12071207
var originalMetadata = installation.GetMetadata(connection);
12081208
using var tx = connection.BeginTransaction();
12091209

12101210
// Index the state
1211-
var changed = await ReindexState(installation, connection, tx);
1211+
var changed = await ReindexState(installation, ignoreModifiedDates, connection, tx);
12121212

12131213
if (!originalMetadata.Contains(GameInstallMetadata.InitialDiskStateTransaction))
12141214
{
@@ -1219,6 +1219,7 @@ await Parallel.ForEachAsync(files, async (item, _) =>
12191219

12201220
if (changed)
12211221
{
1222+
tx.Add(installation.GameMetadataId, GameInstallMetadata.LastScannedDiskStateTransactionId, EntityId.From(TxId.Tmp.Value));
12221223
await tx.Commit();
12231224
}
12241225

@@ -1228,7 +1229,7 @@ await Parallel.ForEachAsync(files, async (item, _) =>
12281229
/// <summary>
12291230
/// Reindex the state of the game
12301231
/// </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)
12321233
{
12331234
var hashDb = await _fileHashService.GetFileHashesDb();
12341235

@@ -1262,50 +1263,61 @@ public async Task<bool> ReindexState(GameInstallation installation, IConnection
12621263

12631264
await Parallel.ForEachAsync(locationPath.EnumerateFiles(), async (file, token) =>
12641265
{
1265-
var gamePath = installation.LocationsRegister.ToGamePath(file);
1266-
if (ShouldIgnorePathWhenIndexing(gamePath)) return;
1267-
1268-
bool isNewPath;
1269-
lock (seenPathsLock)
1266+
try
12701267
{
1271-
isNewPath = seenPaths.Add(gamePath);
1272-
}
1268+
var gamePath = installation.LocationsRegister.ToGamePath(file);
1269+
if (ShouldIgnorePathWhenIndexing(gamePath)) return;
12731270

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+
}
12791276

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);
12841287

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
12871301
{
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+
12921315
hasDiskStateChanged = true;
12931316
}
12941317
}
1295-
else
1318+
catch (Exception ex)
12961319
{
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;
13091321
}
13101322
});
13111323
}
@@ -1325,6 +1337,9 @@ await Parallel.ForEachAsync(locationPath.EnumerateFiles(), async (file, token) =
13251337
private async ValueTask<Hash> MaybeHashFile(IDb hashDb, GamePath gamePath, AbsolutePath file, IFileEntry fileInfo, CancellationToken token)
13261338
{
13271339
Hash? diskMinimalHash = null;
1340+
1341+
var foundHash = Hash.Zero;
1342+
var needFullHash = true;
13281343

13291344
// Look for all known files that match the path
13301345
foreach (var matchingPath in PathHashRelation.FindByPath(hashDb, gamePath.Path))
@@ -1338,10 +1353,26 @@ private async ValueTask<Hash> MaybeHashFile(IDb hashDb, GamePath gamePath, Absol
13381353
diskMinimalHash ??= await MultiHasher.MinimalHash(file, token);
13391354

13401355
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+
}
13421370
}
1371+
1372+
if (!needFullHash)
1373+
return foundHash;
13431374

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);
13451376
return await file.XxHash3Async(token: token);
13461377
}
13471378

@@ -1460,7 +1491,7 @@ public Optional<LoadoutId> GetCurrentlyActiveLoadout(GameInstallation installati
14601491
public async Task ActivateLoadout(LoadoutId loadoutId)
14611492
{
14621493
var loadout = Loadout.Load(Connection.Db, loadoutId);
1463-
var reindexed = await ReindexState(loadout.InstallationInstance, Connection);
1494+
var reindexed = await ReindexState(loadout.InstallationInstance, false, Connection);
14641495

14651496
var tree = BuildSyncTree(DiskStateToPathPartPair(reindexed.DiskStateEntries), DiskStateToPathPartPair(reindexed.DiskStateEntries), loadout);
14661497
ProcessSyncTree(tree);
@@ -1664,7 +1695,7 @@ public async Task DeleteLoadout(LoadoutId loadoutId, GarbageCollectorRunMode gcR
16641695
public async Task ResetToOriginalGameState(GameInstallation installation, LocatorId[] locatorIds)
16651696
{
16661697
var gameState = _fileHashService.GetGameFiles((installation.Store, locatorIds));
1667-
var metaData = await ReindexState(installation, Connection);
1698+
var metaData = await ReindexState(installation, false, Connection);
16681699

16691700
List<PathPartPair> diskState = [];
16701701

src/NexusMods.Abstractions.Loadouts.Synchronizers/ILoadoutSynchronizer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public interface ILoadoutSynchronizer
4949
/// Rescan the files in the folders this game requires. This is used to bring the local cache up to date with the
5050
/// whatever is on disk.
5151
/// </summary>
52-
Task<GameInstallMetadata.ReadOnly> RescanFiles(GameInstallation gameInstallation);
52+
Task<GameInstallMetadata.ReadOnly> RescanFiles(GameInstallation gameInstallation, bool ignoreModifiedDate = false);
5353

5454
#endregion
5555

src/NexusMods.Abstractions.Loadouts.Synchronizers/Rules/ActionMapping.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static Actions MapActions(SignatureShorthand shorthand)
7373
AAx_XXx_i => DeleteFromDisk,
7474
AAx_xxx_I => BackupFile | DeleteFromDisk,
7575
AAx_XXx_I => DeleteFromDisk,
76-
AAA_xxx_i => BackupFile,
76+
AAA_xxx_i => DoNothing,
7777
AAA_XXX_i => DoNothing,
7878
AAA_xxx_I => DoNothing,
7979
AAA_XXX_I => DoNothing,

src/NexusMods.Abstractions.NexusWebApi/ILoginManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public interface ILoginManager
5959
/// <summary>
6060
/// Returns the user's information
6161
/// </summary>
62-
Task<UserInfo?> GetUserInfoAsync(CancellationToken token = default);
62+
ValueTask<UserInfo?> GetUserInfoAsync(CancellationToken token = default);
6363

6464
/// <summary>
6565
/// Verifies whether the user is logged in or not

src/NexusMods.App/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ public static int Main(string[] args)
139139
}
140140
finally
141141
{
142-
host.StopAsync().Wait(timeout: TimeSpan.FromSeconds(5));
142+
// Wait for 15 seconds for the host to stop, otherwise kill the process
143+
if (!host.StopAsync().Wait(timeout: TimeSpan.FromSeconds(15)))
144+
Environment.Exit(0);
143145
}
144146
}
145147

src/NexusMods.Collections/CollectionDownloader.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using NexusMods.Abstractions.NexusWebApi;
1616
using NexusMods.Abstractions.NexusWebApi.Types;
1717
using NexusMods.Abstractions.NexusWebApi.Types.V2;
18+
using NexusMods.Abstractions.Settings;
1819
using NexusMods.Abstractions.Telemetry;
1920
using NexusMods.CrossPlatform.Process;
2021
using NexusMods.Extensions.BCL;
@@ -182,7 +183,10 @@ public async ValueTask Download(CollectionDownloadExternal.ReadOnly download, Ca
182183
/// </summary>
183184
public async ValueTask Download(CollectionDownloadNexusMods.ReadOnly download, CancellationToken cancellationToken)
184185
{
185-
if (_loginManager.IsPremium)
186+
var userInfo = await _loginManager.GetUserInfoAsync(cancellationToken);
187+
if (userInfo is null) return;
188+
189+
if (userInfo.UserRole is UserRole.Premium)
186190
{
187191
await using var tempPath = _temporaryFileManager.CreateFile();
188192
var job = await _nexusModsLibrary.CreateDownloadJob(tempPath, download.FileMetadata, cancellationToken: cancellationToken);
@@ -253,7 +257,6 @@ public async ValueTask DownloadItems(
253257
CollectionRevisionMetadata.ReadOnly revisionMetadata,
254258
ItemType itemType,
255259
IDb db,
256-
int maxDegreeOfParallelism = -1,
257260
CancellationToken cancellationToken = default)
258261
{
259262
var job = new DownloadCollectionJob
@@ -263,7 +266,7 @@ public async ValueTask DownloadItems(
263266
RevisionMetadata = revisionMetadata,
264267
Db = db,
265268
ItemType = itemType,
266-
MaxDegreeOfParallelism = maxDegreeOfParallelism,
269+
MaxDegreeOfParallelism = _serviceProvider.GetRequiredService<ISettingsManager>().Get<DownloadSettings>().MaxParallelDownloads,
267270
};
268271

269272
await _jobMonitor.Begin<DownloadCollectionJob, R3.Unit>(job);

src/NexusMods.Collections/DownloadCollectionJob.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class DownloadCollectionJob : IJobDefinitionWithStart<DownloadCollectionJ
1212
public required CollectionDownloader.ItemType ItemType { get; init; }
1313
public required CollectionDownloader Downloader { get; init; }
1414
public required IDb Db { get; init; }
15-
public int MaxDegreeOfParallelism { get; init; } = -1;
15+
public required int MaxDegreeOfParallelism { get; init; }
1616

1717
public async ValueTask<R3.Unit> StartAsync(IJobContext<DownloadCollectionJob> context)
1818
{
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using NexusMods.Abstractions.Settings;
2+
3+
namespace NexusMods.Collections;
4+
5+
public class DownloadSettings : ISettings
6+
{
7+
public int MaxParallelDownloads { get; set; } = Environment.ProcessorCount;
8+
9+
public static ISettingsBuilder Configure(ISettingsBuilder settingsBuilder)
10+
{
11+
return settingsBuilder.AddToUI<DownloadSettings>(builder => builder
12+
.AddPropertyToUI(x => x.MaxParallelDownloads, propertyBuilder => propertyBuilder
13+
.AddToSection(Sections.General)
14+
.WithDisplayName("Max Parallel Downloads")
15+
.WithDescription("Set the maximum number of downloads that can happen in parallel when downloading collections")
16+
.UseSingleValueMultipleChoiceContainer(
17+
valueComparer: EqualityComparer<int>.Default,
18+
allowedValues: Enumerable.Range(start: 1, Environment.ProcessorCount).ToArray(),
19+
valueToDisplayString: static i => i.ToString()
20+
)
21+
)
22+
);
23+
}
24+
}

src/NexusMods.Collections/Services.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using NexusMods.Abstractions.Collections;
33
using NexusMods.Abstractions.Loadouts;
44
using NexusMods.Abstractions.NexusModsLibrary;
5+
using NexusMods.Abstractions.Settings;
56

67
namespace NexusMods.Collections;
78

@@ -16,6 +17,7 @@ public static IServiceCollection AddNexusModsCollections(this IServiceCollection
1617
.AddNexusCollectionItemLoadoutGroupModel()
1718
.AddNexusCollectionReplicatedLoadoutGroupModel()
1819
.AddCollectionVerbs()
19-
.AddSingleton<CollectionDownloader>();
20+
.AddSingleton<CollectionDownloader>()
21+
.AddSettings<DownloadSettings>();
2022
}
2123
}

src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationLinux.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ public ProtocolRegistrationLinux(
4747
/// <inheritdoc/>
4848
public async Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default)
4949
{
50-
if (ApplicationConstants.InstallationMethod != InstallationMethod.PackageManager)
50+
var canWriteDesktopFile = ApplicationConstants.InstallationMethod is InstallationMethod.AppImage or InstallationMethod.Manually;
51+
var canRegisterAsDefault = ApplicationConstants.InstallationMethod is not InstallationMethod.Flatpak and not InstallationMethod.PackageManager;
52+
53+
if (canWriteDesktopFile)
5154
{
5255
var applicationsDirectory = _fileSystem.GetKnownPath(KnownPath.XDG_DATA_HOME).Combine("applications");
5356
_logger.LogInformation("Using applications directory `{Path}`", applicationsDirectory);
@@ -73,7 +76,7 @@ public async Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = t
7376
}
7477
}
7578

76-
if (setAsDefaultHandler)
79+
if (setAsDefaultHandler && canRegisterAsDefault)
7780
{
7881
try
7982
{

0 commit comments

Comments
 (0)