Skip to content

Commit

Permalink
Run games through Steam (#681)
Browse files Browse the repository at this point in the history
* Upgrade GameFinder to 4.0.0

* Include metadata in game installation

* Run the game through Steam

* Fix bugs

* Fix OAuth issues
  • Loading branch information
erri120 authored Sep 28, 2023
1 parent 06a0da0 commit a74a37b
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<PackageVersion Include="BitFaster.Caching" Version="2.2.0" />
<PackageVersion Include="CliWrap" Version="3.6.4" />
<PackageVersion Include="DynamicData" Version="7.14.2" />
<PackageVersion Include="GameFinder" Version="3.2.2" />
<PackageVersion Include="GameFinder" Version="4.0.0" />
<PackageVersion Include="Humanizer" Version="2.14.1" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.2" />
<PackageVersion Include="Mutagen.Bethesda.Skyrim" Version="0.41.0-pr002" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.Common.OSInterop;
using NexusMods.DataModel.Extensions;
using NexusMods.DataModel.Games;
using NexusMods.DataModel.Loadouts;
Expand All @@ -14,15 +15,15 @@ namespace NexusMods.Games.BethesdaGameStudios;
/// <typeparam name="T"></typeparam>
public abstract class RunGameWithScriptExtender<T> : RunGameTool<T> where T : AGame {
// ReSharper disable once ContextualLoggerProblem
protected RunGameWithScriptExtender(ILogger<RunGameTool<T>> logger, T game, IProcessFactory processFactory)
: base(logger, game, processFactory) { }
protected RunGameWithScriptExtender(ILogger<RunGameTool<T>> logger, T game, IProcessFactory processFactory, IOSInterop osInterop)
: base(logger, game, processFactory, osInterop) { }

protected abstract GamePath ScriptLoaderPath { get; }

protected override AbsolutePath GetGamePath(Loadout loadout, ApplyPlan applyPlan)
{
return applyPlan.Flattened.ContainsKey(ScriptLoaderPath) ?
ScriptLoaderPath.CombineChecked(loadout.Installation) :
return applyPlan.Flattened.ContainsKey(ScriptLoaderPath) ?
ScriptLoaderPath.CombineChecked(loadout.Installation) :
base.GetGamePath(loadout, applyPlan);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.Common.OSInterop;
using NexusMods.DataModel.Games;
using NexusMods.Paths;

Expand All @@ -8,7 +9,7 @@ namespace NexusMods.Games.BethesdaGameStudios;
public class SkyrimLegendaryEditionGameTool : RunGameWithScriptExtender<SkyrimLegendaryEdition>
{
// ReSharper disable once ContextualLoggerProblem
public SkyrimLegendaryEditionGameTool(ILogger<RunGameTool<SkyrimLegendaryEdition>> logger, SkyrimLegendaryEdition game, IProcessFactory processFactory)
: base(logger, game, processFactory) { }
public SkyrimLegendaryEditionGameTool(ILogger<RunGameTool<SkyrimLegendaryEdition>> logger, SkyrimLegendaryEdition game, IProcessFactory processFactory, IOSInterop osInterop)
: base(logger, game, processFactory, osInterop) { }
protected override GamePath ScriptLoaderPath => new(LocationId.Game, "skse_loader.exe");
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.Common.OSInterop;
using NexusMods.DataModel.Games;
using NexusMods.Paths;

Expand All @@ -8,7 +9,7 @@ namespace NexusMods.Games.BethesdaGameStudios;
public class SkyrimSpecialEditionGameTool : RunGameWithScriptExtender<SkyrimSpecialEdition>
{
// ReSharper disable once ContextualLoggerProblem
public SkyrimSpecialEditionGameTool(ILogger<RunGameTool<SkyrimSpecialEdition>> logger, SkyrimSpecialEdition game, IProcessFactory processFactory)
: base(logger, game, processFactory) { }
public SkyrimSpecialEditionGameTool(ILogger<RunGameTool<SkyrimSpecialEdition>> logger, SkyrimSpecialEdition game, IProcessFactory processFactory, IOSInterop osInterop)
: base(logger, game, processFactory, osInterop) { }
protected override GamePath ScriptLoaderPath => new(LocationId.Game, "skse64_loader.exe");
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using NexusMods.DataModel.Abstractions;
using NexusMods.DataModel.Abstractions.Ids;
using NexusMods.DataModel.JsonConverters;
using NexusMods.Networking.NexusWebApi.DTOs.OAuth;

namespace NexusMods.Networking.NexusWebApi.NMA;

Expand All @@ -15,6 +16,21 @@ public record JWTTokenEntity : Entity
/// </summary>
public static readonly IId StoreId = new IdVariableLength(EntityCategory.AuthData, "NexusMods.Networking.NexusWebApi.JWTTokens"u8.ToArray());

/// <summary>
/// Creates a new <see cref="JWTTokenEntity"/> from a <see cref="JwtTokenReply"/>.
/// </summary>
public static JWTTokenEntity? From(JwtTokenReply? tokenReply)
{
if (tokenReply?.AccessToken is null || tokenReply.RefreshToken is null) return null;
return new JWTTokenEntity
{
AccessToken = tokenReply.AccessToken,
RefreshToken = tokenReply.RefreshToken,
ExpiresAt = DateTimeOffset.FromUnixTimeSeconds(tokenReply.CreatedAt) + TimeSpan.FromSeconds(tokenReply.ExpiresIn),
DataStoreId = StoreId
};
}

/// <inheritdoc/>
public override EntityCategory Category => StoreId.Category;

Expand Down
34 changes: 17 additions & 17 deletions src/Networking/NexusMods.Networking.NexusWebApi.NMA/LoginManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using NexusMods.Common;
using NexusMods.Common.ProtocolRegistration;
using NexusMods.DataModel.Abstractions;
Expand All @@ -14,6 +15,7 @@ namespace NexusMods.Networking.NexusWebApi.NMA;
[PublicAPI]
public sealed class LoginManager : IDisposable
{
private readonly ILogger<LoginManager> _logger;
private readonly OAuth _oauth;
private readonly IDataStore _dataStore;
private readonly IProtocolRegistration _protocolRegistration;
Expand All @@ -40,23 +42,23 @@ public sealed class LoginManager : IDisposable
/// </summary>
public IObservable<Uri?> Avatar => UserInfo.Select(info => info?.AvatarUrl);

/// <summary/>
/// <param name="client">Nexus API client.</param>
/// <param name="msgFactory">Used to check authentication status and ensure verified.</param>
/// <param name="oauth">Helper class to deal with authentication messages.</param>
/// <param name="dataStore">Used for storing information about the current login session.</param>
/// <param name="protocolRegistration">Used to register NXM protocol.</param>
public LoginManager(Client client,
/// <summary>
/// Constructor.
/// </summary>
public LoginManager(
Client client,
IAuthenticatingMessageFactory msgFactory,
OAuth oauth,
IDataStore dataStore,
IProtocolRegistration protocolRegistration)
IProtocolRegistration protocolRegistration,
ILogger<LoginManager> logger)
{
_oauth = oauth;
_msgFactory = msgFactory;
_client = client;
_dataStore = dataStore;
_protocolRegistration = protocolRegistration;
_logger = logger;

UserInfo = _dataStore.IdChanges
// NOTE(err120): Since IDs don't change on startup, we can insert
Expand Down Expand Up @@ -99,16 +101,14 @@ public async Task LoginAsync(CancellationToken token = default)
await _protocolRegistration.RegisterSelf("nxm");

var jwtToken = await _oauth.AuthorizeRequest(token);
var createdAt = DateTimeOffset.FromUnixTimeSeconds(jwtToken.CreatedAt);
var expiresIn = TimeSpan.FromSeconds(jwtToken.ExpiresIn);
var expiresAt = createdAt + expiresIn;

_dataStore.Put(JWTTokenEntity.StoreId, new JWTTokenEntity
var newTokenEntity = JWTTokenEntity.From(jwtToken);
if (newTokenEntity is null)
{
RefreshToken = jwtToken.RefreshToken,
AccessToken = jwtToken.AccessToken,
ExpiresAt = expiresAt
});
_logger.LogError("Invalid new token!");
return;
}

_dataStore.Put(JWTTokenEntity.StoreId, newTokenEntity);
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/Networking/NexusMods.Networking.NexusWebApi.NMA/OAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public OAuth(ILogger<OAuth> logger,
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns>task with the jwt token once we receive one</returns>
public async Task<JwtTokenReply> AuthorizeRequest(CancellationToken cancellationToken)
public async Task<JwtTokenReply?> AuthorizeRequest(CancellationToken cancellationToken)
{
// see https://www.rfc-editor.org/rfc/rfc7636#section-4.1
var codeVerifier = _idGen.UUIDv4().ToBase64();
Expand Down Expand Up @@ -97,7 +97,7 @@ private IInterprocessJob CreateJob(Uri url)
/// <param name="refreshToken">the refresh token</param>
/// <param name="cancel"></param>
/// <returns>a new token reply</returns>
public async Task<JwtTokenReply> RefreshToken(string refreshToken, CancellationToken cancel)
public async Task<JwtTokenReply?> RefreshToken(string refreshToken, CancellationToken cancel)
{
var request = new Dictionary<string, string>
{
Expand All @@ -113,7 +113,7 @@ public async Task<JwtTokenReply> RefreshToken(string refreshToken, CancellationT
return JsonSerializer.Deserialize<JwtTokenReply>(responseString);
}

private async Task<JwtTokenReply> AuthorizeToken(string verifier, string code, CancellationToken cancel)
private async Task<JwtTokenReply?> AuthorizeToken(string verifier, string code, CancellationToken cancel)
{
var request = new Dictionary<string, string> {
{ "grant_type", "authorization_code" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ public OAuth2MessageFactory(
_logger.LogDebug("Refreshing expired OAuth token");

var newToken = await _auth.RefreshToken(_cachedTokenEntity.RefreshToken, cancellationToken);
_cachedTokenEntity = new JWTTokenEntity
var newTokenEntity = JWTTokenEntity.From(newToken);
if (newTokenEntity is null)
{
RefreshToken = newToken.RefreshToken,
AccessToken = newToken.AccessToken,
ExpiresAt = DateTimeOffset.UtcNow,
};
_logger.LogError("Invalid new token!");
return null;
}

_cachedTokenEntity = newTokenEntity;
_store.Put(JWTTokenEntity.StoreId, _cachedTokenEntity);
_cachedTokenEntity.DataStoreId = JWTTokenEntity.StoreId;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,38 @@ namespace NexusMods.Networking.NexusWebApi.DTOs.OAuth;
/// <summary>
/// JWT Token info as provided by the OAuth server
/// </summary>
public struct JwtTokenReply
public class JwtTokenReply
{
/// <summary>
/// the token to use for authentication
/// </summary>
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
public string? AccessToken { get; set; }

/// <summary>
/// token type, e.g. "Bearer"
/// </summary>
[JsonPropertyName("token_type")]
public string Type { get; set; }
public string? Type { get; set; }

/// <summary>
/// when the access token expires in seconds
/// </summary>
[JsonPropertyName("expires_in")]
public ulong ExpiresIn { get; set; }

/// <summary>
/// token to use to refresh once this one has expired
/// </summary>
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
public string? RefreshToken { get; set; }

/// <summary>
/// space separated list of scopes. defined by the server, currently always "public"?
/// </summary>
[JsonPropertyName("scope")]
public string Scope { get; set; }
public string? Scope { get; set; }

/// <summary>
/// unix timestamp (seconds resolution) of when the token was created
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/NexusMods.App.UI/LeftMenu/Items/LaunchButtonViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
Expand Down Expand Up @@ -52,8 +53,7 @@ public LaunchButtonViewModel(ILogger<LaunchButtonViewModel> logger, IToolManager
.DisposeWith(d);

var canExecute = _jobs.WhenAnyValue(coll => coll.Count, count => count == 0);

Command = ReactiveCommand.CreateFromTask(LaunchGame, canExecute.OnUI());
Command = ReactiveCommand.CreateFromObservable(() => Observable.StartAsync(LaunchGame, RxApp.TaskpoolScheduler), canExecute.OnUI());
});
}

Expand Down
10 changes: 4 additions & 6 deletions src/NexusMods.DataModel/Games/AGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,15 @@ private List<GameInstallation> GetInstallations()
return (_gamelocators.SelectMany(locator => locator.Find(this),
(locator, installation) =>
{
var locations = GetLocations(installation.Path.FileSystem,
installation);
var locations = GetLocations(installation.Path.FileSystem, installation);
return new GameInstallation
{
Game = this,
LocationsRegister =
new GameLocationsRegister(
new Dictionary<LocationId, AbsolutePath>(locations)),
LocationsRegister = new GameLocationsRegister(new Dictionary<LocationId, AbsolutePath>(locations)),
InstallDestinations = GetInstallDestinations(locations),
Version = installation.Version ?? GetVersion(installation),
Store = installation.Store
Store = installation.Store,
LocatorResultMetadata = installation.Metadata
};
}))
.DistinctBy(g => g.LocationsRegister[LocationId.Game])
Expand Down
13 changes: 9 additions & 4 deletions src/NexusMods.DataModel/Games/GameInstallation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ namespace NexusMods.DataModel.Games;
/// </summary>
public class GameInstallation
{
/// <summary>
/// Empty game installation, used for testing and some cases where a property must be set.
/// </summary>
public static GameInstallation Empty => new();

/// <summary>
/// The Version installed.
/// </summary>
Expand All @@ -30,14 +35,14 @@ public class GameInstallation
public IGame Game { get; init; } = null!;

/// <summary>
/// Empty game installation, used for testing and some cases where a property must be set.
/// The <see cref="GameStore"/> which was used to install the game.
/// </summary>
public static GameInstallation Empty => new();
public GameStore Store { get; init; } = GameStore.Unknown;

/// <summary>
/// The <see cref="GameStore"/> which was used to install the game.
/// Gets the metadata returned by the game locator.
/// </summary>
public GameStore Store { get; init; } = GameStore.Unknown;
public IGameLocatorResultMetadata? LocatorResultMetadata { get; init; }

/// <summary>
/// Returns the game name and version as
Expand Down
Loading

0 comments on commit a74a37b

Please sign in to comment.