Skip to content

Commit

Permalink
Merge branch 'main' into update-mnemonicdb
Browse files Browse the repository at this point in the history
  • Loading branch information
halgari committed Sep 30, 2024
2 parents 1397ed3 + d2d38c3 commit ebb1c73
Show file tree
Hide file tree
Showing 68 changed files with 1,568 additions and 207 deletions.
57 changes: 56 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
# Changelog

## [v0.6.0](https://github.com/Nexus-Mods/NexusMods.App/releases/tag/v0.6.0) - 2024-08-29
## [v0.6.1](https://github.com/Nexus-Mods/NexusMods.App/releases/tag/v0.6.1) - 2024-09-24

# Caution: To update the app, you must completely uninstall the old version, including all mods. [Learn More.](https://nexus-mods.github.io/NexusMods.App/users/faq/#why-do-i-have-to-uninstall-everything-to-update-the-app)

This release adds a very basic implementation of downloading Collections, updates the UI to the new tree view and includes some enhancements when interacting with Windows applications via Linux.

### New UI for My Mods and Library
The My Mods and Library pages have been completely reworked to use the new tree view. Mods are now grouped by the mod page on Nexus Mods, meaning if download several files from the same page they will be grouped together. A "Switch View" option has been added to the toolbar to toggle these groupings on or off. We are continuing to work towards to designs shown in the [previous changelog](./docs/changelog-assets/1b28e2fad5b5a6431a72c286d1bcd3fd.webp).

![An image showing mods in the Library nested by mod page (left) or ungrouped (right)](./docs/changelog-assets/823627a8ccb068dc1559d62cd3326ebe.webp)

### EXPERIMENTAL - Collections
**WARNING: The feature is unfinished and not considered stable. It will not accurately install complex collections and is currently only functional for Premium users.**

We've included a very early implementation of the Collections feature in this release. It's incomplete and will not install collections as the user has set them up in Vortex. Currently, only mods from Nexus Mods can be installed - anything from external websites or bundled with the collection will not install as expected.

![A collection for Cyberpunk 2077 installed into a loadout.](./docs/changelog-assets/8a591449d6a8cddc5d2bad7d1fd5c849.webp)

Collections will appear as a separate list of mods in the left menu. Users can view all mods in the loadout from the new "Installed Mods" option at the top of the left menu.

To start out, this will only be available to Premium users, but we are working on the free user journey separately which requires considerably more UI elements to be created. This will be available in a future release.


### Cyberpunk 2077 Enhancements
As a further enhancement to support for Cyberpunk 2077, we will now detect if the REDmod DLC is missing and prompt the user to install it if required.

![The diagnostic message for REDmod shown in the Health Check.](./docs/changelog-assets/531dc13e8116620f8ded4a8a98b281da.webp)

We've also fixed the issue which prevented REDmod from deploying automatically on Linux. This work also sets up a framework for running Windows apps and tools on a Linux system using [Protontricks](https://github.com/Matoking/protontricks) ([#1989](https://github.com/Nexus-Mods/NexusMods.App/pull/1989)).

### Known Issues
- When batch selecting mods in My Mods and using the remove button the app will occasionally fail to remove mods that are not currently visible in the UI due to scrolling.
- Trying to install a collection with an unsupported type of mod (e.g. Bundled or External) will fail with no error message. This is not supported in the current build.
- Trying to install a collection as a non-Premium user will fail with no error message. This is not supported in the current build.
- Once a collection is added to the app, it cannot be removed from the left menu.
- Collections allow users to modify the included mods but do not allow you to reset them to a the original state.
- The first row of the My Mods or Library tables will sometimes be misaligned with the headers. Scrolling or adjusting any column width will correct this.
- The "Switch View" option does not persist.

### Other Features
- The name of the active loadout will now appear in the top bar ([#1953](https://github.com/Nexus-Mods/NexusMods.App/pull/1953)).
- The app now has a minimum window size of `360x360` to prevent it being resized to unusable dimensions ([#1947](https://github.com/Nexus-Mods/NexusMods.App/pull/1947)).

### Bugfixes
- Stardew Valley: Fixed enabled mods showing up as disabled in the diagnostics ([#1923](https://github.com/Nexus-Mods/NexusMods.App/pull/1953)).
- Linux: Fixed the game not launching when running through Steam ([#1917](https://github.com/Nexus-Mods/NexusMods.App/pull/1917)).

### Technical Changes
- Added a system for storing and displaying images in the app.

### External Contributors
- [@Patriot99](https://github.com/Patriot99): [#1896](https://github.com/Nexus-Mods/NexusMods.App/pull/1896)
- [@LoulouNoLegend](https://github.com/LoulouNoLegend): [#1997](https://github.com/Nexus-Mods/NexusMods.App/pull/1997), [#1998](https://github.com/Nexus-Mods/NexusMods.App/pull/1998), [#1999](https://github.com/Nexus-Mods/NexusMods.App/pull/1999)


## [v0.6.0](https://github.com/Nexus-Mods/NexusMods.App/releases/tag/v0.6.0) - 2024-09-03

**Caution: To update the app, you must completely uninstall the old version, including all mods. [Learn More.](https://nexus-mods.github.io/NexusMods.App/users/faq/#why-do-i-have-to-uninstall-everything-to-update-the-app)**


This release enhances support for Cyberpunk 2077, adds multiple Loadouts per game and implements the back-end changes to support our new "Loadout items" data model.

Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageVersion Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageVersion Include="FlatSharp.Compiler" Version="7.6.0" />
<PackageVersion Include="FlatSharp.Runtime" Version="7.6.0" />
<PackageVersion Include="GameFinder.Launcher.Heroic" Version="4.3.0" />
<PackageVersion Include="LinqGen" Version="0.3.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.8.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" />
Expand Down Expand Up @@ -120,7 +121,7 @@
<PackageVersion Include="BitFaster.Caching" Version="2.5.0" />
<PackageVersion Include="CliWrap" Version="3.6.6" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
<PackageVersion Include="GameFinder" Version="4.2.4" />
<PackageVersion Include="GameFinder" Version="4.3.0" />
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.2" />
<PackageVersion Include="Mutagen.Bethesda.Skyrim" Version="0.44.0" />
Expand Down
7 changes: 7 additions & 0 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Abstractions.Medi
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.Larian", "src\Games\NexusMods.Games.Larian\NexusMods.Games.Larian.csproj", "{2A35EBB5-1CA6-4F5D-8CE8-352146C82C28}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.Larian.Tests", "tests\Games\NexusMods.Games.Larian.Tests\NexusMods.Games.Larian.Tests.csproj", "{425F7A13-99A2-4231-B0C1-C56EB819C174}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -668,6 +670,10 @@ Global
{2A35EBB5-1CA6-4F5D-8CE8-352146C82C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A35EBB5-1CA6-4F5D-8CE8-352146C82C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A35EBB5-1CA6-4F5D-8CE8-352146C82C28}.Release|Any CPU.Build.0 = Release|Any CPU
{425F7A13-99A2-4231-B0C1-C56EB819C174}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{425F7A13-99A2-4231-B0C1-C56EB819C174}.Debug|Any CPU.Build.0 = Debug|Any CPU
{425F7A13-99A2-4231-B0C1-C56EB819C174}.Release|Any CPU.ActiveCfg = Release|Any CPU
{425F7A13-99A2-4231-B0C1-C56EB819C174}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -787,6 +793,7 @@ Global
{8C817874-7A88-450E-B216-851A1B03684C} = {52AF9D62-7D5B-4AD0-BA12-86F2AA67428B}
{5CB6D02C-07D0-4C0D-BF5C-4E2E958A0612} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{2A35EBB5-1CA6-4F5D-8CE8-352146C82C28} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{425F7A13-99A2-4231-B0C1-C56EB819C174} = {05B06AC1-7F2B-492F-983E-5BC63CDBF20D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using NexusMods.Abstractions.Games.GameCapabilities;
using NexusMods.Paths;

namespace NexusMods.Abstractions.GameLocators.GameCapabilities;
Expand Down Expand Up @@ -56,11 +55,6 @@ public static void AddCommonLocations(IReadOnlyDictionary<LocationId, AbsolutePa
{
// Locations has
DestinationGamePath = new GamePath(location.Key, ""),
KnownSourceFolderNames = Array.Empty<string>(),
KnownValidSubfolders = Array.Empty<string>(),
KnownValidFileExtensions = Array.Empty<Extension>(),
FileExtensionsToDiscard = Array.Empty<Extension>(),
SubPathsToDiscard = Array.Empty<RelativePath>()
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.GameLocators.GameCapabilities;
using NexusMods.Paths;
using NexusMods.Paths;

namespace NexusMods.Abstractions.Games.GameCapabilities;
namespace NexusMods.Abstractions.GameLocators.GameCapabilities;

/// <summary>
/// Represents a target path for installing simple mods archives
Expand All @@ -21,32 +19,32 @@ public class InstallFolderTarget : IModInstallDestination
/// <summary>
/// List of known recognizable aliases that can be directly mapped to the <see cref="DestinationGamePath"/>.
/// </summary>
public IEnumerable<string> KnownSourceFolderNames { get; init; } = Enumerable.Empty<string>();
public IEnumerable<RelativePath> KnownSourceFolderNames { get; init; } = [];

/// <summary>
/// List of known recognizable first level subfolders of the target <see cref="DestinationGamePath"/>.
/// NOTE: Only include folders that are only likely to appear at this level of the folder hierarchy.
/// </summary>
public IEnumerable<string> KnownValidSubfolders { get; init; } = Enumerable.Empty<string>();
public IEnumerable<RelativePath> Names { get; init; } = [];

/// <summary>
/// List of known recognizable file extensions for direct children of the target <see cref="DestinationGamePath"/>.
/// NOTE: Only include file extensions that are only likely to appear at this level of the folder hierarchy.
/// </summary>
public IEnumerable<Extension> KnownValidFileExtensions { get; init; } = Enumerable.Empty<Extension>();
public IEnumerable<Extension> KnownValidFileExtensions { get; init; } = [];

/// <summary>
/// List of subPaths of the target <see cref="DestinationGamePath"/> that should be discarded.
/// </summary>
public IEnumerable<RelativePath> SubPathsToDiscard { get; init; } = Enumerable.Empty<RelativePath>();
public IEnumerable<RelativePath> SubPathsToDiscard { get; init; } = [];

/// <summary>
/// List of file extensions to discard when installing to this target.
/// </summary>
public IEnumerable<Extension> FileExtensionsToDiscard { get; init; } = Enumerable.Empty<Extension>();
public IEnumerable<Extension> FileExtensionsToDiscard { get; init; } = [];

/// <summary>
/// Collection of Targets that are nested paths relative to <see cref="DestinationGamePath"/>.
/// </summary>
public IEnumerable<InstallFolderTarget> SubTargets { get; init; } = Enumerable.Empty<InstallFolderTarget>();
public IEnumerable<InstallFolderTarget> SubTargets { get; init; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ public record GOGLocatorResultMetadata : IGameLocatorResultMetadata
/// </summary>
public required long Id { get; init; }
}

[PublicAPI]
public record HeroicGOGLocatorResultMetadata : GOGLocatorResultMetadata;
23 changes: 20 additions & 3 deletions src/Abstractions/NexusMods.Abstractions.Games/RunGameTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Logging;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.Games.DTO;
using NexusMods.Abstractions.Games.Stores.GOG;
using NexusMods.Abstractions.Games.Stores.Steam;
using NexusMods.Abstractions.Loadouts;
using NexusMods.CrossPlatform.Process;
Expand Down Expand Up @@ -60,10 +61,18 @@ public async Task Execute(Loadout.ReadOnly loadout, CancellationToken cancellati
var program = await GetGamePath(loadout);
var primaryFile = _game.GetPrimaryFile(loadout.InstallationInstance.Store).CombineChecked(loadout.InstallationInstance);

if (OSInformation.Shared.IsLinux && program.Equals(primaryFile) && loadout.InstallationInstance.LocatorResultMetadata is SteamLocatorResultMetadata steamLocatorResultMetadata)
if (OSInformation.Shared.IsLinux && program.Equals(primaryFile))
{
await RunThroughSteam(steamLocatorResultMetadata.AppId, cancellationToken);
return;
var locator = loadout.InstallationInstance.LocatorResultMetadata;
switch (locator)
{
case SteamLocatorResultMetadata steamLocatorResultMetadata:
await RunThroughSteam(steamLocatorResultMetadata.AppId, cancellationToken);
return;
case HeroicGOGLocatorResultMetadata heroicGOGLocatorResultMetadata:
await RunThroughHeroic("gog", heroicGOGLocatorResultMetadata.Id, cancellationToken);
return;
}
}

var names = new HashSet<string>
Expand Down Expand Up @@ -177,6 +186,14 @@ private async Task RunThroughSteam(uint appId, CancellationToken cancellationTok
await reaper.WaitForExitAsync(cancellationToken);
}

private async Task RunThroughHeroic(string type, long appId, CancellationToken cancellationToken)
{
Debug.Assert(OSInformation.Shared.IsLinux);

// TODO: track process
await _osInterop.OpenUrl(new Uri($"heroic://launch/{type}/{appId.ToString(CultureInfo.InvariantCulture)}"), fireAndForget: true, cancellationToken: cancellationToken);
}

private async ValueTask<Process?> WaitForProcessToStart(
string processName,
TimeSpan timeout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ public SyncTree BuildSyncTree(DiskState currentState, DiskState previousTree, IE
return file;
})
.Where(f => !f.TryGetAsDeletedFile(out _))
.Where(f => !IsIgnoredPath(f.TargetPath))
.OfTypeLoadoutFile();

return BuildSyncTree(currentState, previousTree, grouped);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.IndexSegments;
using NexusMods.MnemonicDB.Abstractions.Models;
using Splat.ModeDetection;

Expand All @@ -19,13 +20,34 @@ public readonly struct GraphQLResolver(ITransaction Tx, ReadOnlyModel Model)
/// <summary>
/// Create a new resolver using the given primary key attribute and value.
/// </summary>
public static GraphQLResolver Create<THighLevel, TLowLevel>(IDb referenceDb, ITransaction tx, ScalarAttribute<THighLevel, TLowLevel> primaryKeyAttribute, THighLevel primaryKeyValue) where THighLevel : notnull
public static GraphQLResolver Create<THighLevel, TLowLevel>(IDb db, ITransaction tx, ScalarAttribute<THighLevel, TLowLevel> primaryKeyAttribute, THighLevel primaryKeyValue) where THighLevel : notnull
{
var existing = referenceDb.Datoms(primaryKeyAttribute, primaryKeyValue);
var existing = db.Datoms(primaryKeyAttribute, primaryKeyValue);
var exists = existing.Count > 0;
var id = existing.Count == 0 ? tx.TempId() : existing[0].E;
if (!exists)
tx.Add(id, primaryKeyAttribute, primaryKeyValue);
return new GraphQLResolver(tx, new ReadOnlyModel(db, id));
}

/// <summary>
/// Create a resolver that depends on two primary key attributes and values.
/// </summary>
public static GraphQLResolver Create<THighLevel1, TLowLevel1, THighLevel2, TLowLevel2>(IDb referenceDb, ITransaction tx,
(ScalarAttribute<THighLevel1, TLowLevel1> A, THighLevel1 V) pair1,
(ScalarAttribute<THighLevel2, TLowLevel2> A, THighLevel2 V) pair2)
where THighLevel1 : notnull
where THighLevel2 : notnull
{
var existing = referenceDb.Datoms(pair1, pair2);
var exists = existing.Count > 0;
var id = existing.Count == 0 ? tx.TempId() : existing[0];
if (!exists)
{
tx.Add(id, pair1.A, pair1.V);
tx.Add(id, pair2.A, pair2.V);
}

return new GraphQLResolver(tx, new ReadOnlyModel(referenceDb, id));
}

Expand Down Expand Up @@ -81,7 +103,7 @@ public void Add<TOther>(ReferenceAttribute<TOther> attribute, EntityId id)
return;
}

if (attribute.Get(Model).Equals(id))
if (attribute.TryGet(Model, out var foundId) && foundId.Equals(id))
return;

// Else add the value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ public partial class CollectionMetadata : IModelDefinition
/// The collections' image.
/// </summary>
public static readonly MemoryAttribute TileImage = new(Namespace, nameof(TileImage));

/// <summary>
/// The collections' image.
/// </summary>
public static readonly MemoryAttribute BackgroundImage = new(Namespace, nameof(BackgroundImage));
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,18 @@ public partial class CollectionRevisionMetadata : IModelDefinition
/// <summary>
/// The locally unique revision number (aka "version") of a collection. Only unique within one collection.
/// </summary>
public static readonly RevisionNumberAttribute RevisionNumber = new(Namespace, nameof(RevisionNumber));
public static readonly RevisionNumberAttribute RevisionNumber = new(Namespace, nameof(RevisionNumber)) { IsIndexed = true };

/// <summary>
/// The collection this revision belongs to.
/// </summary>
public static readonly ReferenceAttribute<CollectionMetadata> Collection = new(Namespace, nameof(Collection));

/// <summary>
/// All the mod files in this revision.
/// </summary>
public static readonly BackReferenceAttribute<CollectionRevisionModFile> Files = new(CollectionRevisionModFile.CollectionRevision);

/// <summary>
/// The number of downloads this revision has.
/// </summary>
Expand All @@ -47,9 +52,4 @@ public partial class CollectionRevisionMetadata : IModelDefinition
/// The total number of ratings this revision has.
/// </summary>
public static readonly ULongAttribute TotalRatings = new(Namespace, nameof(TotalRatings));

/// <summary>
/// The total number of mods in this revision.
/// </summary>
public static readonly ULongAttribute ModCount = new(Namespace, nameof(ModCount));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using NexusMods.Abstractions.MnemonicDB.Attributes;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.Models;

namespace NexusMods.Abstractions.NexusModsLibrary.Models;

public partial class CollectionRevisionModFile : IModelDefinition
{
private const string Namespace = "NexusMods.Library.CollectionRevisionModFile";

/// <summary>
/// The Nexus, globally unique id identifying a specific file of a collection revision.
/// </summary>
public static readonly ULongAttribute FileId = new(Namespace, nameof(FileId)) { IsIndexed = true };

/// <summary>
/// If the file is optional or not
/// </summary>
public static readonly BooleanAttribute IsOptional = new(Namespace, nameof(IsOptional));

/// <summary>
/// The associated NexusModsFileMetadata that contains the other metadata of the file.
/// </summary>
public static readonly ReferenceAttribute<NexusModsFileMetadata> NexusModFile = new(Namespace, nameof(NexusModFile));

/// <summary>
/// The associated CollectionRevision
/// </summary>
public static readonly ReferenceAttribute<CollectionRevisionMetadata> CollectionRevision = new(Namespace, nameof(CollectionRevision));
}
Loading

0 comments on commit ebb1c73

Please sign in to comment.