diff --git a/CHANGELOG.md b/CHANGELOG.md index a82faf13..09002955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ### Changelog +#### Version - 2.4.0.0 - ??? +* Wabbajack is now based on .NET 5.0 (does not require a runtime download by users) +* Origin is now supported as a game source +* Basic (mostly untested) support for Dragon Age : Origins + #### Version - 2.3.6.2 - 12/31/2020 * HOTFIX: Also apply the IPS4 changes to LL Meta lookups diff --git a/Wabbajack.Common/GameMetaData.cs b/Wabbajack.Common/GameMetaData.cs index 20dcf5e8..1fc7f909 100644 --- a/Wabbajack.Common/GameMetaData.cs +++ b/Wabbajack.Common/GameMetaData.cs @@ -41,7 +41,8 @@ namespace Wabbajack.Common StardewValley, KingdomComeDeliverance, MechWarrior5Mercenaries, - NoMansSky + NoMansSky, + DragonAgeOrigins } public static class GameExtensions @@ -70,6 +71,11 @@ namespace Wabbajack.Common // to get gog ids: https://www.gogdb.org public List? GOGIDs { get; internal set; } + + // to get these ids, split the numbers from the letters in file names found in + // C:\ProgramData\Origin\LocalContent\{game name)\*.mfst + // So for DA:O this is "DR208591800.mfst" -> "DR:208591800" + public List OriginIDs { get; set; } = new(); public List EpicGameStoreIDs { get; internal set; } = new List(); @@ -548,7 +554,7 @@ namespace Wabbajack.Common Game = Game.NoMansSky, NexusName = "nomanssky", NexusGameId = 1634, - MO2Name = "Mo Man's Sky", + MO2Name = "No Man's Sky", SteamIDs = new List{275850}, GOGIDs = new List{1446213994}, RequiredFiles = new List @@ -557,6 +563,22 @@ namespace Wabbajack.Common }, MainExecutable = @"Binaries\NMS.exe" } + }, + { + Game.DragonAgeOrigins, new GameMetaData + { + Game = Game.DragonAgeOrigins, + NexusName = "dragonage", + NexusGameId = 140, + MO2Name = "Dragon Age: Origins", // Probably wrong + SteamIDs = new List{17450}, + OriginIDs = new List{"DR:208591800"}, + RequiredFiles = new List + { + @"bin_ship\daorigins.exe" + }, + MainExecutable = @"bin_ship\daorigins.exe" + } } }; diff --git a/Wabbajack.Common/StoreHandlers/BethNetHandler.cs b/Wabbajack.Common/StoreHandlers/BethNetHandler.cs index 0221b899..7c94b91a 100644 --- a/Wabbajack.Common/StoreHandlers/BethNetHandler.cs +++ b/Wabbajack.Common/StoreHandlers/BethNetHandler.cs @@ -12,6 +12,9 @@ namespace Wabbajack.Common.StoreHandlers { public override Game Game { get; internal set; } public override StoreType Type { get; internal set; } = StoreType.BethNet; + + public AbsolutePath InstallPath; + } public class BethNetHandler : AStoreHandler diff --git a/Wabbajack.Common/StoreHandlers/OriginHandler.cs b/Wabbajack.Common/StoreHandlers/OriginHandler.cs new file mode 100644 index 00000000..e5ebc3f7 --- /dev/null +++ b/Wabbajack.Common/StoreHandlers/OriginHandler.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using Microsoft.Win32; + +namespace Wabbajack.Common.StoreHandlers +{ + public class OriginHandler : AStoreHandler + { + private AbsolutePath OriginDataPath = (AbsolutePath)@"C:\ProgramData\Origin\LocalContent"; + private Extension MFSTExtension = new Extension(".mfst"); + private HashSet KnownMFSTs = new(); + + public override StoreType Type { get; internal set; } = StoreType.Origin; + public override bool Init() + { + try + { + if (!OriginDataPath.Exists) + return false; + + KnownMFSTs = OriginDataPath.EnumerateFiles() + .Where(f => f.Extension == MFSTExtension) + .Select(f => f.FileNameWithoutExtension.ToString()) + .ToHashSet(); + + Utils.Log($"Found MFSTs from Origin: {string.Join(", ", KnownMFSTs)}"); + + return true; + + } + catch (Exception) + { + return false; + } + } + + public override bool LoadAllGames() + { + try + { + foreach (var game in GameRegistry.Games) + { + var mfst = game.Value.OriginIDs.FirstOrDefault(g => KnownMFSTs.Contains(g.Replace(":", ""))); + if (mfst == null) + continue; + + var ogame = new OriginGame(mfst, game.Key, game.Value); + ogame.GetAndCacheManifestResponse(); + Games.Add(ogame); + } + + return true; + } + catch (Exception ex) + { + Utils.Log(ex.ToString()); + return false; + } + } + } + + public sealed class OriginGame : AStoreGame + { + private string _mfst; + private GameMetaData _metaData; + + public OriginGame(string mfst, Game game, GameMetaData metaData) + { + _mfst = mfst; + Game = game; + _metaData = metaData; + } + public override Game Game { get; internal set; } + public override StoreType Type { get; internal set; } = StoreType.Origin; + + public override AbsolutePath Path + { + get + { + return GetGamePath(); + + } + internal set + { + throw new NotImplementedException(); + } + } + + private AbsolutePath GetGamePath() + { + var manifestData = GetAndCacheManifestResponse().FromJsonString(); + var platform = manifestData!.publishing!.softwareList!.software!.FirstOrDefault(a => a.softwarePlatform == "PCWIN"); + + var installPath = GetPathFromPlatformPath(platform!.fulfillmentAttributes!.installCheckOverride!); + return installPath; + + } + + internal AbsolutePath GetPathFromPlatformPath(string path, RegistryView platformView) + { + if (!path.StartsWith("[")) + { + return (AbsolutePath)path; + } + + var matchPath = Regex.Match(path, @"\[(.*?)\\(.*)\\(.*)\](.*)"); + if (!matchPath.Success) + { + Utils.Log("Unknown path format " + path); + return default; + } + + var root = matchPath.Groups[1].Value; + + RegistryKey rootKey = root switch + { + "HKEY_LOCAL_MACHINE" => RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, platformView), + "HKEY_CURRENT_USER" => RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, platformView), + _ => throw new Exception("Unknown registry root entry " + root) + }; + + var subPath = matchPath.Groups[2].Value.Trim('\\'); + var key = matchPath.Groups[3].Value; + var executable = matchPath.Groups[4].Value.Trim('\\'); + var subKey = rootKey.OpenSubKey(subPath); + if (subKey == null) + { + return default; + } + + var keyValue = rootKey!.OpenSubKey(subPath)!.GetValue(key); + if (keyValue == null) + { + return default; + } + + return (AbsolutePath)keyValue!.ToString()!; + } + + internal AbsolutePath GetPathFromPlatformPath(string path) + { + var resultPath = GetPathFromPlatformPath(path, RegistryView.Registry64); + if (resultPath == default) + { + resultPath = GetPathFromPlatformPath(path, RegistryView.Registry32); + } + + return resultPath; + } + + private AbsolutePath ManifestCacheLocation => + Consts.LocalAppDataPath.Combine("OriginManifestCache", _mfst.Replace(":", "")); + + internal string GetAndCacheManifestResponse() + { + if (ManifestCacheLocation.Exists) + { + return ManifestCacheLocation.ReadAllText(); + } + + Utils.Log($"Getting Origin Manifest info for {_mfst}"); + var client = new HttpClient(); + var data = client.GetStringAsync($"https://api1.origin.com/ecommerce2/public/{_mfst}/en_US").Result; + ManifestCacheLocation.Parent.CreateDirectory(); + ManifestCacheLocation.WriteAllTextAsync(data).Wait(); + return data; + } + + public class GameLocalDataResponse + { + public class LocalizableAttributes + { + public string? longDescription; + public string? displayName; + } + + public class Publishing + { + public class Software + { + public class FulfillmentAttributes + { + public string? executePathOverride; + public string? installationDirectory; + public string? installCheckOverride; + } + + public string? softwareId; + public string? softwarePlatform; + public FulfillmentAttributes? fulfillmentAttributes; + } + + public class SoftwareList + { + public List? software; + } + + public SoftwareList? softwareList; + } + + public string? offerId; + public string? offerType; + public Publishing? publishing; + public LocalizableAttributes? localizableAttributes; + } + } +} diff --git a/Wabbajack.Common/StoreHandlers/StoreHandler.cs b/Wabbajack.Common/StoreHandlers/StoreHandler.cs index 68d3653c..96fe27c5 100644 --- a/Wabbajack.Common/StoreHandlers/StoreHandler.cs +++ b/Wabbajack.Common/StoreHandlers/StoreHandler.cs @@ -10,7 +10,8 @@ namespace Wabbajack.Common.StoreHandlers STEAM, GOG, BethNet, - EpicGameStore + EpicGameStore, + Origin } public class StoreHandler @@ -29,6 +30,9 @@ namespace Wabbajack.Common.StoreHandlers private static readonly Lazy _epicGameStoreHandler = new Lazy(() => new EpicGameStoreHandler()); public EpicGameStoreHandler EpicGameStoreHandler = _epicGameStoreHandler.Value; + + private static readonly Lazy _originHandler = new Lazy(() => new OriginHandler()); + public OriginHandler OriginHandler = _originHandler.Value; public List StoreGames; @@ -83,6 +87,18 @@ namespace Wabbajack.Common.StoreHandlers { Utils.Error(new StoreException("Could not Init the EpicGameStoreHandler, check previous error messages!")); } + + if (OriginHandler.Init()) + { + if (OriginHandler.LoadAllGames()) + StoreGames.AddRange(OriginHandler.Games); + else + Utils.Error(new StoreException("Could not load all Games from the OriginHandler, check previous error messages!")); + } + else + { + Utils.Error(new StoreException("Could not Init the OriginHandler, check previous error messages!")); + } } public AbsolutePath? TryGetGamePath(Game game) diff --git a/Wabbajack.Test/GameStoreTests.cs b/Wabbajack.Test/GameStoreTests.cs new file mode 100644 index 00000000..eef17393 --- /dev/null +++ b/Wabbajack.Test/GameStoreTests.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Wabbajack.Common; +using Xunit; +using Xunit.Abstractions; + +namespace Wabbajack.Test +{ + public class GameStoreTests : ATestBase + { + public GameStoreTests(ITestOutputHelper output) : base(output) + { + } + + + /// + /// Comment out this [Fact] when not testing by hand. It's too hard to have all games installed at all times + /// so we only run this test as neede + /// + /// + //[Fact] + public async Task OriginGameStoreTest() + { + Assert.True(Game.DragonAgeOrigins.MetaData().TryGetGameLocation(out var loc)); + Assert.NotEqual(default, loc); + Assert.Equal((AbsolutePath)@"c:\Games\Dragon Age", loc); + Assert.True(Game.DragonAgeOrigins.MetaData().IsInstalled); + } + } +}