From f30d736d08bb469c43984057d79d9e8d55c3dc88 Mon Sep 17 00:00:00 2001 From: InvoxiPlayGames Date: Fri, 21 Jul 2023 08:38:22 +0100 Subject: [PATCH] better default account stuff, epicovt support, offline mode and dry run option --- .github/workflows/build.yml | 27 +++++ EASAccount.cs | 57 ----------- EASLogin.cs | 76 -------------- EpicAccount.cs | 90 +++++++++++++++++ EASClasses.cs => EpicClasses.cs | 23 ++++- EpicEcom.cs | 50 ++++++++++ EpicLogin.cs | 81 +++++++++++++++ EricLauncher.csproj | 2 + FortniteUpdateCheck.cs | 7 +- Program.cs | 171 +++++++++++++++++++++++++------- README.md | 36 ++++--- 11 files changed, 427 insertions(+), 193 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 EASAccount.cs delete mode 100644 EASLogin.cs create mode 100644 EpicAccount.cs rename EASClasses.cs => EpicClasses.cs (65%) create mode 100644 EpicEcom.cs create mode 100644 EpicLogin.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f22cdf1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build + +on: + push: + branches: [ "master" ] + +jobs: + build: + strategy: + matrix: + rid: [win-x64, osx-arm64, linux-x64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Build + run: dotnet build -c Release -r ${{ matrix.rid }} + - name: Publish + run: dotnet publish -c Release -r ${{ matrix.rid }} --no-build + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: EricLauncher-${{ matrix.rid }} + path: bin/Release/net7.0/${{ matrix.rid }}/publish/ diff --git a/EASAccount.cs b/EASAccount.cs deleted file mode 100644 index 09e9dc5..0000000 --- a/EASAccount.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Threading.Tasks; - -namespace EricLauncher -{ - class EASAccount - { - private const string EXCHANGE_API_URL = "https://account-public-service-prod.ol.epicgames.com/account/api/oauth/exchange"; - - public string? AccountId; - public string? DisplayName; - public string AccessToken; - public DateTime AccessExpiry; - public string RefreshToken; - public DateTime RefreshExpiry; - - private HttpClient HTTPClient; - - public EASAccount(EASLoginResponse login) - { - AccessToken = login.access_token!; - RefreshToken = login.refresh_token!; - AccessExpiry = login.expires_at!; - RefreshExpiry = login.refresh_expires_at!; - AccountId = login.account_id!; - DisplayName = login.displayName; - - HTTPClient = new(); - HTTPClient.DefaultRequestHeaders.Accept.Clear(); - HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - HTTPClient.DefaultRequestHeaders.Add("Authorization", login.token_type + " " + login.access_token); - } - - public async Task GetExchangeCode() - { - EASExchangeResponse? resp = await HTTPClient.GetFromJsonAsync(EXCHANGE_API_URL); - return resp!.code!; - } - - public StoredAccountInfo MakeStoredAccountInfo() - { - StoredAccountInfo info = new StoredAccountInfo(); - info.AccountId = AccountId; - info.RefreshExpiry = RefreshExpiry; - info.RefreshToken = RefreshToken; - info.AccessExpiry = AccessExpiry; - info.AccessToken = AccessToken; - info.DisplayName = DisplayName; - return info; - } - } -} diff --git a/EASLogin.cs b/EASLogin.cs deleted file mode 100644 index 3dbd6fd..0000000 --- a/EASLogin.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Buffers.Text; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Text; -using System.Threading.Tasks; - -namespace EricLauncher -{ - class EASLogin - { - public const string LAUNCHER_CLIENT = "34a02cf8f4414e29b15921876da36f9a"; - public const string LAUNCHER_SECRET = "daafbccc737745039dffe53d94fc76cf"; - - public const string FORTNITE_CLIENT = "ec684b8c687f479fadea3cb2ad83f5c6"; - public const string FORTNITE_SECRET = "e1f31c211f28413186262d37a13fc84d"; - - private const string API_URL = "https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token"; - - private string ClientId; - private string ClientSecret; - private HttpClient HTTPClient; - - public EASLogin(string client_id = LAUNCHER_CLIENT, string client_secret = LAUNCHER_SECRET) - { - ClientId = client_id; - ClientSecret = client_secret; - HTTPClient = new(); - HTTPClient.DefaultRequestHeaders.Accept.Clear(); - HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - HTTPClient.DefaultRequestHeaders.Add("Authorization", "basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ClientId}:{ClientSecret}"))); - } - - public async Task GetClientCredentials() - { - HttpResponseMessage resp = await HTTPClient.PostAsync(API_URL, new StringContent("grant_type=client_credentials&token_type=eg1", Encoding.UTF8, "application/x-www-form-urlencoded")); - if (!resp.IsSuccessStatusCode) - { - EASError? error_response = await resp.Content.ReadFromJsonAsync(); - throw new Exception($"Client credential fetch failed: '{error_response!.errorMessage}' ({error_response!.errorCode})"); - } - EASLoginResponse? login_response = await resp.Content.ReadFromJsonAsync(); - return login_response!.access_token; - } - - public async Task LoginWithRefreshToken(string refresh_token) - { - HttpResponseMessage resp = await HTTPClient.PostAsync(API_URL, new StringContent($"grant_type=refresh_token&refresh_token={refresh_token}&token_type=eg1", Encoding.UTF8, "application/x-www-form-urlencoded")); - if (!resp.IsSuccessStatusCode) - { - EASError? error_response = await resp.Content.ReadFromJsonAsync(); - throw new Exception($"Refresh token login failed: '{error_response!.errorMessage}' ({error_response!.errorCode})"); - } - EASLoginResponse? login_response = await resp.Content.ReadFromJsonAsync(); - if (login_response == null) // maybe we should throw an exception here - return null; - return new EASAccount(login_response); - } - - public async Task LoginWithAuthorizationCode(string authorization_code) - { - HttpResponseMessage resp = await HTTPClient.PostAsync(API_URL, new StringContent($"grant_type=authorization_code&code={authorization_code}&token_type=eg1", Encoding.UTF8, "application/x-www-form-urlencoded")); - if (!resp.IsSuccessStatusCode) - { - EASError? error_response = await resp.Content.ReadFromJsonAsync(); - throw new Exception($"Authorization code login failed: '{error_response!.errorMessage}' ({error_response!.errorCode})"); - } - EASLoginResponse? login_response = await resp.Content.ReadFromJsonAsync(); - if (login_response == null) // maybe we should throw an exception here - return null; - return new EASAccount(login_response); - } - } -} diff --git a/EpicAccount.cs b/EpicAccount.cs new file mode 100644 index 0000000..308dda3 --- /dev/null +++ b/EpicAccount.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; + +namespace EricLauncher +{ + class EpicAccount + { + private const string EXCHANGE_API_URL = "/account/api/oauth/exchange"; + private const string VERIFY_API_URL = "/account/api/oauth/verify"; + + public string? AccountId; + public string? DisplayName; + public string AccessToken; + public DateTime AccessExpiry; + public string RefreshToken; + public DateTime RefreshExpiry; + + private HttpClient HTTPClient; + + public EpicAccount(EpicLoginResponse login) + { + AccessToken = login.access_token!; + RefreshToken = login.refresh_token!; + AccessExpiry = login.expires_at!; + RefreshExpiry = login.refresh_expires_at!; + AccountId = login.account_id!; + DisplayName = login.displayName; + + HTTPClient = new(); + HTTPClient.BaseAddress = new Uri(EpicLogin.ACCOUNTS_API_BASE); + HTTPClient.DefaultRequestHeaders.Accept.Clear(); + HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + HTTPClient.DefaultRequestHeaders.Add("Authorization", login.token_type + " " + login.access_token); + } + + public EpicAccount(StoredAccountInfo info) + { + if (info.AccessToken == null || info.RefreshToken == null) + throw new Exception("Stored account info doesn't have access or refresh token"); + + if (info.AccountId != null) + AccountId = info.AccountId; + if (info.DisplayName != null) + DisplayName = info.DisplayName; + AccessToken = info.AccessToken; + RefreshToken = info.RefreshToken; + AccessExpiry = info.AccessExpiry; + RefreshExpiry = info.RefreshExpiry; + + HTTPClient = new(); + HTTPClient.BaseAddress = new Uri(EpicLogin.ACCOUNTS_API_BASE); + HTTPClient.DefaultRequestHeaders.Accept.Clear(); + HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + HTTPClient.DefaultRequestHeaders.Add("Authorization", "bearer " + info.AccessToken); + } + + public async Task GetExchangeCode() + { + EpicExchangeResponse? resp = await HTTPClient.GetFromJsonAsync(EXCHANGE_API_URL); + return resp!.code!; + } + + public async Task VerifyToken() + { + HttpResponseMessage resp = await HTTPClient.GetAsync(VERIFY_API_URL); + if (!resp.IsSuccessStatusCode) + return false; + EpicVerifyResponse? verify_response = await resp.Content.ReadFromJsonAsync(); + // if the token is expiring soon, why bother amirite + return verify_response!.expires_in > 60; + } + + public StoredAccountInfo MakeStoredAccountInfo() + { + StoredAccountInfo info = new StoredAccountInfo(); + info.AccountId = AccountId; + info.RefreshExpiry = RefreshExpiry; + info.RefreshToken = RefreshToken; + info.AccessExpiry = AccessExpiry; + info.AccessToken = AccessToken; + info.DisplayName = DisplayName; + return info; + } + } +} diff --git a/EASClasses.cs b/EpicClasses.cs similarity index 65% rename from EASClasses.cs rename to EpicClasses.cs index bc475cd..91277da 100644 --- a/EASClasses.cs +++ b/EpicClasses.cs @@ -6,7 +6,7 @@ namespace EricLauncher { - public class EASLoginResponse + public class EpicLoginResponse { public string? access_token { get; set; } public int expires_in { get; set; } @@ -27,14 +27,14 @@ public class EASLoginResponse public string? application_id { get; set; } } - public class EASExchangeResponse + public class EpicExchangeResponse { public int expiresInSeconds { get; set; } public string? code { get; set; } public string? creatingClientId { get; set; } } - public class EASError + public class EpicError { public string? errorCode { get; set; } public string? errorMessage { get; set; } @@ -46,4 +46,21 @@ public class EASError public string? error { get; set; } } + public class EpicVerifyResponse + { + public string? token { get; set; } + public string? session_id { get; set; } + public string? token_type { get; set; } + public string? client_id { get; set; } + public bool internal_client { get; set; } + public string? client_service { get; set; } + public string? account_id { get; set; } + public int expires_in { get; set; } + public DateTime expires_at { get; set; } + public string? auth_method { get; set; } + public string? display_name { get; set; } + public string? app { get; set; } + public string? in_app_id { get; set; } + public string? device_id { get; set; } + } } diff --git a/EpicEcom.cs b/EpicEcom.cs new file mode 100644 index 0000000..b8bb6ae --- /dev/null +++ b/EpicEcom.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; + +namespace EricLauncher +{ + class EpicEcomToken + { + public string? token { get; set; } + } + + class EpicEcom + { + public const string API_BASE = "https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com"; + + private const string OWT_API_TEMPLATE = "/ecommerceintegration/api/public/platforms/EPIC/identities/{0}/ownershipToken"; + + private EpicAccount Account; + private HttpClient HTTPClient; + + public EpicEcom(EpicAccount account) + { + Account = account; + + HTTPClient = new(); + HTTPClient.BaseAddress = new Uri(API_BASE); + HTTPClient.DefaultRequestHeaders.Accept.Clear(); + HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + HTTPClient.DefaultRequestHeaders.Add("Authorization", "bearer " + account.AccessToken!); + } + + public async Task GetOwnershipToken(string catalog_namespace, string catalog_item_id) + { + string formatted_url = string.Format(OWT_API_TEMPLATE, Account.AccountId!); + HttpResponseMessage resp = await HTTPClient.PostAsync(formatted_url, new StringContent($"nsCatalogItemId={catalog_namespace}:{catalog_item_id}", Encoding.UTF8, "application/x-www-form-urlencoded")); + if (!resp.IsSuccessStatusCode) + { + EpicError? error_response = await resp.Content.ReadFromJsonAsync(); + Console.WriteLine($"Failed to fetch ownership token: '{error_response!.errorMessage}' ({error_response!.errorCode})"); + return null; + } + EpicEcomToken? ecom_response = await resp.Content.ReadFromJsonAsync(); + return ecom_response!.token; + } + } +} diff --git a/EpicLogin.cs b/EpicLogin.cs new file mode 100644 index 0000000..af62f80 --- /dev/null +++ b/EpicLogin.cs @@ -0,0 +1,81 @@ +using System; +using System.Buffers.Text; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Threading.Tasks; + +namespace EricLauncher +{ + class EpicLogin + { + public const string LAUNCHER_CLIENT = "34a02cf8f4414e29b15921876da36f9a"; + public const string LAUNCHER_SECRET = "daafbccc737745039dffe53d94fc76cf"; + public const string ACCOUNTS_API_BASE = "https://account-public-service-prod.ol.epicgames.com"; + + private const string TOKEN_API_URL = "/account/api/oauth/token"; + + private string ClientId; + private string ClientSecret; + private HttpClient HTTPClient; + + public EpicLogin(string client_id = LAUNCHER_CLIENT, string client_secret = LAUNCHER_SECRET) + { + ClientId = client_id; + ClientSecret = client_secret; + HTTPClient = new(); + HTTPClient.BaseAddress = new Uri(ACCOUNTS_API_BASE); + HTTPClient.DefaultRequestHeaders.Accept.Clear(); + HTTPClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + HTTPClient.DefaultRequestHeaders.Add("Authorization", "basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ClientId}:{ClientSecret}"))); + } + + private async Task DoOAuthLogin(string grant_type, string? grant_name = null, string? grant_code = null, string? extra_arguments = null, bool use_eg1 = true) + { + if (grant_name != null && grant_code == null) + throw new Exception($"Grant name was given but with no grant value."); + + // build the data to send to the oauth token endpoint + string form_encoded_data = $"grant_type={grant_type}"; + if (extra_arguments != null) + form_encoded_data += $"&{extra_arguments}"; + if (grant_code != null) // if no grant name is provided use the name of the type + form_encoded_data += $"&{(grant_name != null ? grant_name : grant_type)}={grant_code}"; + if (use_eg1) + form_encoded_data += $"&token_type=eg1"; + + HttpResponseMessage resp = await HTTPClient.PostAsync(TOKEN_API_URL, new StringContent(form_encoded_data, Encoding.UTF8, "application/x-www-form-urlencoded")); + if (!resp.IsSuccessStatusCode) + { + EpicError? error_response = await resp.Content.ReadFromJsonAsync(); + throw new Exception($"OAuth login of grant type {grant_type} failed: '{error_response!.errorMessage}' ({error_response!.errorCode})"); + } + EpicLoginResponse? login_response = await resp.Content.ReadFromJsonAsync(); + return login_response; + } + + public async Task GetClientCredentials() + { + EpicLoginResponse? login_response = await DoOAuthLogin("client_credentials"); + return login_response!.access_token; + } + + public async Task LoginWithRefreshToken(string refresh_token) + { + EpicLoginResponse? login_response = await DoOAuthLogin("refresh_token", null, refresh_token); + if (login_response == null) // maybe we should throw an exception here + return null; + return new EpicAccount(login_response); + } + + public async Task LoginWithAuthorizationCode(string authorization_code) + { + EpicLoginResponse? login_response = await DoOAuthLogin("authorization_code", "code", authorization_code); + if (login_response == null) // maybe we should throw an exception here + return null; + return new EpicAccount(login_response); + } + } +} diff --git a/EricLauncher.csproj b/EricLauncher.csproj index f02677b..2d22d40 100644 --- a/EricLauncher.csproj +++ b/EricLauncher.csproj @@ -3,6 +3,8 @@ Exe net7.0 + true + false enable enable diff --git a/FortniteUpdateCheck.cs b/FortniteUpdateCheck.cs index 382831f..0bc6bd6 100644 --- a/FortniteUpdateCheck.cs +++ b/FortniteUpdateCheck.cs @@ -26,10 +26,13 @@ public class FortniteCloudContent class FortniteUpdateCheck { + public const string FORTNITE_PC_CLIENT = "ec684b8c687f479fadea3cb2ad83f5c6"; + public const string FORTNITE_PC_SECRET = "e1f31c211f28413186262d37a13fc84d"; + public static async Task IsUpToDate(string fortnite_version, string platform) { - EASLogin eas = new EASLogin(EASLogin.FORTNITE_CLIENT, EASLogin.FORTNITE_SECRET); - string? client_credentials = await eas.GetClientCredentials(); + EpicLogin epic = new EpicLogin(FORTNITE_PC_CLIENT, FORTNITE_PC_SECRET); + string? client_credentials = await epic.GetClientCredentials(); HttpClient client = new(); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); diff --git a/Program.cs b/Program.cs index 6fb1d1d..735580a 100644 --- a/Program.cs +++ b/Program.cs @@ -7,20 +7,22 @@ namespace EricLauncher { internal class Program { - static string BaseAppDataFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData) + $"/EricLauncher"; - static string RedirectURL = "https://www.epicgames.com/id/api/redirect?clientId=" + EASLogin.LAUNCHER_CLIENT + "&responseType=code"; + static string BaseAppDataFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData) + "/EricLauncher"; + static string BaseOVTFolder = BaseAppDataFolder + "/OVT"; + static string RedirectURL = "https://www.epicgames.com/id/api/redirect?clientId=" + EpicLogin.LAUNCHER_CLIENT + "&responseType=code"; static void PrintUsage() { Console.WriteLine("Usage: EricLauncher.exe [executable path] (options)"); Console.WriteLine(); Console.WriteLine("Options:"); - Console.WriteLine(" --accountId [accountId] - use a specific Epic Games account to sign in."); - Console.WriteLine(" omitting this option will use the default account"); - Console.WriteLine(" --noManifest - don't check the local Epic Games Launcher install folder for the manifest."); - Console.WriteLine(" this WILL break certain games from launching, e.g. Fortnite"); - Console.WriteLine(" --stayOpen - keeps EricLauncher open in the background until the game is closed"); - Console.WriteLine(" useful for launching through other launchers, e.g. Steam"); + Console.WriteLine(" --accountId [id] - use a specific Epic Games account ID to sign in."); + Console.WriteLine(" --noManifest - don't check the local Epic Games Launcher install folder for the manifest."); + Console.WriteLine(" --stayOpen - keeps EricLauncher open in the background until the game is closed."); + Console.WriteLine(" --dryRun - goes through the Epic Games login flow, but does not launch the game."); + Console.WriteLine(" --offline - skips the Epic Games login flow, to launch the game in offline mode."); + Console.WriteLine(" --manifest [file] - specify a specific manifest file to use."); + Console.WriteLine(); } static async Task Main(string[] args) @@ -30,28 +32,49 @@ static async Task Main(string[] args) return; } bool needs_code_login = false; - EASLogin login = new EASLogin(); - EASAccount? account = null; + EpicLogin login = new EpicLogin(); + EpicAccount? account = null; // parse the cli arguments string? account_id = null; + string? manifest_path = null; + bool set_default = false; bool no_manifest = false; bool stay_open = false; + bool dry_run = false; + bool offline = false; + bool skip_fortnite_update = false; if (args.Length > 1) { for (int i = 1; i < args.Length; i++) { if (args[i] == "--accountId") account_id = args[++i]; + if (args[i] == "--manifest") + manifest_path = args[++i]; + if (args[i] == "--setDefault") + set_default = true; if (args[i] == "--noManifest") no_manifest = true; if (args[i] == "--stayOpen") stay_open = true; + if (args[i] == "--dryRun") + dry_run = true; + if (args[i] == "--offline") + offline = true; + if (args[i] == "--noCheckFn") + skip_fortnite_update = true; } } + // always run as a dry run if we're on Linux or FreeBSD + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || + RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) + dry_run = true; + // if we're launching fortnite, do an update check - if (Path.GetFileName(args[0]).ToLower() == "fortnitelauncher.exe") + if (!skip_fortnite_update && !offline && + Path.GetFileName(args[0]).ToLower() == "fortnitelauncher.exe") { Console.WriteLine("Checking for Fortnite updates..."); // traverse back to the cloud content json @@ -66,6 +89,7 @@ static async Task Main(string[] args) { Console.WriteLine("Fortnite is not the latest version!"); Console.WriteLine("Please open the Epic Games Launcher to start updating the game."); + Thread.Sleep(2500); return; } } catch @@ -77,9 +101,17 @@ static async Task Main(string[] args) // check if we have an account saved already StoredAccountInfo? storedInfo; + bool is_default = account_id == null || set_default; if (account_id != null) { Console.WriteLine($"Using account {account_id}"); storedInfo = GetAccountInfo(account_id); + // check if the account id specified is the current default account + if (!set_default) + { + StoredAccountInfo? default_account = GetAccountInfo(); + if (default_account?.AccountId == storedInfo?.AccountId) + is_default = true; + } } else { Console.WriteLine("Using default account"); @@ -91,21 +123,41 @@ static async Task Main(string[] args) } else { if (storedInfo.DisplayName != null) - Console.WriteLine($"Logging in as {storedInfo.DisplayName} ({storedInfo.AccountId}) with refresh token..."); + Console.Write($"Logging in as {storedInfo.DisplayName} ({storedInfo.AccountId})..."); else - Console.WriteLine($"Logging in as {storedInfo.AccountId} with refresh token..."); + Console.Write($"Logging in as {storedInfo.AccountId}..."); - try + account = new(storedInfo); + if (!offline) { - account = await login.LoginWithRefreshToken(storedInfo.RefreshToken!); - } catch { } - if (account == null) + // check the expiry date, if the access token has expired then just refresh straight away, otherwise verify our access token + bool verified = account.AccessExpiry >= DateTime.UtcNow ? await account.VerifyToken() : false; + if (!verified) + { + Console.Write("refreshing..."); + account = null; + try + { + account = await login.LoginWithRefreshToken(storedInfo.RefreshToken!); + Console.WriteLine("success!"); + } + catch { } + if (account == null) + { + Console.WriteLine("failed."); + Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + Console.WriteLine("@ WARNING: EPIC GAMES REFRESH TOKEN HAS CHANGED! @"); + Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); + Console.WriteLine("IT IS POSSIBLE THAT SWEENEY IS DOING SOMETHING EPIC!"); + needs_code_login = true; + } + } else + { + Console.WriteLine("success!"); + } + } else { - Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - Console.WriteLine("@ WARNING: EPIC GAMES REFRESH TOKEN HAS CHANGED! @"); - Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); - Console.WriteLine("IT IS POSSIBLE THAT SWEENEY IS DOING SOMETHING EPIC!"); - needs_code_login = true; + Console.WriteLine("offline."); } } @@ -137,6 +189,8 @@ static async Task Main(string[] args) if (account_id != null && account!.AccountId != account_id) { Console.WriteLine($"Logged in, but the account ID ({account.AccountId}) isn't the same as the one provided at the command line ({account_id})."); + // save the account info later just to save time + StoreAccountInfo(account!.MakeStoredAccountInfo(), false); return; } @@ -149,13 +203,17 @@ static async Task Main(string[] args) // save our refresh token for later usage if (!Directory.Exists(BaseAppDataFolder)) Directory.CreateDirectory(BaseAppDataFolder); - StoreAccountInfo(account!.MakeStoredAccountInfo(), account_id == null); + StoreAccountInfo(account!.MakeStoredAccountInfo(), is_default); // fetch the game's manifest from the installed epic games launcher EGLManifest? manifest = null; - if (!no_manifest) + if (!no_manifest && manifest_path == null) { - manifest = GetManifest(Path.GetFileName(args[0])); + manifest = GetEGLManifest(Path.GetFileName(args[0])); + } else if (!no_manifest && manifest_path != null) + { + string jsonstring = File.ReadAllText(manifest_path); + manifest = JsonSerializer.Deserialize(jsonstring); } if (manifest == null) { @@ -165,34 +223,41 @@ static async Task Main(string[] args) } // launch the game - string exchange = await account.GetExchangeCode(); + string exchange = ""; + if (!offline) + exchange = await account.GetExchangeCode(); Console.WriteLine("Launching game..."); - Process game = LaunchGame(args[0], exchange, account, manifest); - if (stay_open) + Process game = await LaunchGame(args[0], exchange, account, manifest, dry_run, offline); + if (stay_open && !dry_run) { game.WaitForExit(); Console.WriteLine($"Game exited with code {game.ExitCode}"); } } - static Process LaunchGame(string filename, string? exchange, EASAccount? account, EGLManifest? manifest) + static async Task LaunchGame(string filename, string? exchange, EpicAccount? account, EGLManifest? manifest, bool dry_run, bool skip_ovt) { Process p = new Process(); p.StartInfo.FileName = filename; p.StartInfo.WorkingDirectory = Path.GetDirectoryName(filename); - p.StartInfo.ArgumentList.Add($"-AUTH_LOGIN=unused"); - p.StartInfo.ArgumentList.Add($"-AUTH_TYPE=exchangecode"); - if (exchange != null) - p.StartInfo.ArgumentList.Add($"-AUTH_PASSWORD={exchange}"); p.StartInfo.ArgumentList.Add($"-epicenv=Prod"); p.StartInfo.ArgumentList.Add($"-epiclocale=en-US"); p.StartInfo.ArgumentList.Add($"-EpicPortal"); + p.StartInfo.ArgumentList.Add($"-AUTH_LOGIN=unused"); + + if (exchange != null) + { + p.StartInfo.ArgumentList.Add($"-AUTH_TYPE=exchangecode"); + p.StartInfo.ArgumentList.Add($"-AUTH_PASSWORD={exchange}"); + } + if (account != null) { p.StartInfo.ArgumentList.Add($"-epicuserid={account.AccountId}"); if (account.DisplayName != null) p.StartInfo.ArgumentList.Add($"-epicusername=\"{account.DisplayName}\""); } + if (manifest != null) { p.StartInfo.ArgumentList.Add($"-epicsandboxid={manifest.MainGameCatalogNamespace}"); @@ -204,11 +269,44 @@ static Process LaunchGame(string filename, string? exchange, EASAccount? account p.StartInfo.ArgumentList.Add(arg); } } - p.Start(); + + if (account != null && manifest != null && !skip_ovt && + manifest.OwnershipToken == "true") + { + string? epicovt_path = await GetOwnershipTokenPath(account, manifest); + if (epicovt_path != null) + p.StartInfo.ArgumentList.Add($"-epicovt=\"{epicovt_path}\""); + } + + if (!dry_run) + p.Start(); + else + { + string full_command = filename + " "; + foreach (string arg in p.StartInfo.ArgumentList) + { + full_command += arg + " "; + } + Console.WriteLine("Launch: " + full_command); + } + return p; } - static EGLManifest? GetManifest(string executable_name) + static async Task GetOwnershipTokenPath(EpicAccount account, EGLManifest manifest) + { + Directory.CreateDirectory(BaseOVTFolder); + string ovt_path = $"{BaseOVTFolder}/{manifest.MainGameAppName!}.ovt"; + EpicEcom ecom = new(account); + string? epicovt = await ecom.GetOwnershipToken(manifest.CatalogNamespace!, manifest.CatalogItemId!); + if (epicovt != null) + { + File.WriteAllText(ovt_path, epicovt); + return ovt_path; + } else return null; + } + + static EGLManifest? GetEGLManifest(string executable_name) { IEnumerable files; string manifestfolder = "/Epic/EpicGamesLauncher/Data/Manifests"; @@ -229,7 +327,8 @@ static Process LaunchGame(string filename, string? exchange, EASAccount? account { string jsonstring = File.ReadAllText(file); EGLManifest? manifest = JsonSerializer.Deserialize(jsonstring); - if (manifest != null && manifest.LaunchExecutable != null && Path.GetFileName(manifest.LaunchExecutable).ToLower() == executable_name.ToLower()) + if (manifest != null && manifest.LaunchExecutable != null && + Path.GetFileName(manifest.LaunchExecutable).ToLower() == executable_name.ToLower()) { return manifest; } diff --git a/README.md b/README.md index ed24973..2116a4f 100644 --- a/README.md +++ b/README.md @@ -4,47 +4,45 @@ A very bare-bones launcher for Epic Games Store games **that have already been i This application was written for personal usage and isn't 100% user oriented. If you're looking for something more stable, tested, reliable, featureful and/or cross-platform, check out [Legendary (CLI)](https://github.com/derrod/legendary) or [Heroic Games Launcher (GUI)](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher). -**This is provided without any warranty, I am not responsible for your account getting banned, your hard drive exploding, getting sniped at 2nd in a Battle Royale, or thermonuclear war.** +**This is provided without any warranty, I am not responsible for your account getting banned, your save data being lost, your hard drive exploding, getting sniped at 2nd in a Battle Royale, or thermonuclear war.** ## Features - Logging in to Epic Games accounts. - Multi-account support. (specify an `--accountId` at the command line.) - Checking for Fortnite updates. (if an update is available, the game will not launch.) +- Windows and macOS support, as well as providing launch args on Linux. - Support for at least some games. (tested with Fortnite, Borderlands 3, Death Stranding and FUSER.) - - Not every game will work right now. + - Including games that require ownership tokens. (in theory) +- Offline game launching. (only works on some games) ## Usage -This is designed to be run from the command line, but you can drag and drop a game's executable onto it and it should work. (Alternatively you could make a batch file or shortcut.) +This is designed to be run from the command line, but you can drag and drop a game's primary executable onto it and it should work. + +Alternatively you could make a batch file or shortcut, or create a shortcut in a launcher such as Steam - pointing to EricLauncher, with the game you want to launch as the launch arguments ``` Usage: EricLauncher.exe [executable path] (options) Options: - --accountId [accountId] - use a specific Epic Games account to sign in. - omitting this option will use the default account - --noManifest - don't check the local Epic Games Launcher install folder for the manifest. - this WILL break certain games from launching, e.g. Fortnite - --stayOpen - keeps EricLauncher open in the background until the game is closed - useful for launching through other launchers, e.g. Steam + --accountId [id] - use a specific Epic Games account ID to sign in. + --noManifest - don't check the local Epic Games Launcher install folder for the manifest. + --stayOpen - keeps EricLauncher open in the background until the game is closed. + --dryRun - goes through the Epic Games login flow, but does not launch the game. + --offline - skips the Epic Games login flow, to launch the game in offline mode. + --manifest [file] - specify a specific manifest file to use. ``` The account ID parameter is only required if you are using multiple accounts. Omitting this value will use (or save) a default account. For best results, make sure the game has been launched at least once by the official Epic Games Launcher, and the provided executable path is the same one that gets launched by the official launcher. -Session files are stored in `%localappdata%\EricLauncher`. - -## Known Issues - -- Using the "accountId" parameter with the same account ID as the default account will break the default account until re-login. +Epic Games session files are stored in `%localappdata%\EricLauncher`. ## TODO -- Only refresh tokens when access token expires. - Fetching manifest online when one can't be found in the local cache. -- Fetch ownership tokens (`-epicovt`) for the games that require them. -- Offline game launching. -- Support for macOS and Linux. -- Allowing specifying an auth type at the command line. + - and also from Heroic/Legendary launchers cache. +- Support for launching games under Proton on Linux. + - ...and Crossover on macOS.