From cd8c8e6dd9c9a0df3e18c26dd091b26f7446541c Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 10 Jul 2024 15:11:49 +0200 Subject: [PATCH] Release 9.2.3 --- common/lib/src/util/backend.dart | 10 +- common/lib/src/util/log.dart | 2 +- common/lib/src/util/path.dart | 3 - common/lib/src/util/process.dart | 114 ++++++++++-------- gui/lib/l10n/reboot_en.arb | 5 +- gui/lib/src/controller/game_controller.dart | 3 +- .../src/controller/hosting_controller.dart | 3 + .../src/messenger/implementation/profile.dart | 12 ++ .../src/page/implementation/host_page.dart | 13 +- gui/lib/src/widget/game_start_button.dart | 2 +- gui/pubspec.yaml | 10 +- 11 files changed, 114 insertions(+), 63 deletions(-) diff --git a/common/lib/src/util/backend.dart b/common/lib/src/util/backend.dart index 587961d..2468f31 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -15,10 +15,16 @@ final Semaphore _semaphore = Semaphore(); String? _lastIp; String? _lastPort; -Future startEmbeddedBackend(bool detached) async => startProcess( +Future startEmbeddedBackend(bool detached) async { + final process = await startProcess( executable: backendStartExecutable, window: detached, -); + ); + process.stdOutput.listen((message) => log("[BACKEND] Message: $message")); + process.stdError.listen((error) => log("[BACKEND] Error: $error")); + process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); + return process; +} Future startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort); diff --git a/common/lib/src/util/log.dart b/common/lib/src/util/log.dart index 9b5f9ef..d44bb16 100644 --- a/common/lib/src/util/log.dart +++ b/common/lib/src/util/log.dart @@ -7,7 +7,7 @@ final File launcherLogFile = _createLoggingFile(); final Semaphore _semaphore = Semaphore(1); File _createLoggingFile() { - final file = File("${logsDirectory.path}\\launcher.log"); + final file = File("${installationDirectory.path}\\launcher.log"); file.parent.createSync(recursive: true); if(file.existsSync()) { file.deleteSync(); diff --git a/common/lib/src/util/path.dart b/common/lib/src/util/path.dart index 1f9da61..ff1f90a 100644 --- a/common/lib/src/util/path.dart +++ b/common/lib/src/util/path.dart @@ -14,9 +14,6 @@ Directory get assetsDirectory { return installationDirectory; } -Directory get logsDirectory => - Directory("${installationDirectory.path}\\logs"); - Directory get settingsDirectory => Directory("${installationDirectory.path}\\settings"); diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index 1983581..fa406db 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -1,6 +1,7 @@ // ignore_for_file: non_constant_identifier_names import 'dart:async'; +import 'dart:collection'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; @@ -9,6 +10,7 @@ import 'dart:math'; import 'package:ffi/ffi.dart'; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; +import 'package:reboot_common/src/util/log.dart'; import 'package:sync/semaphore.dart'; import 'package:win32/win32.dart'; @@ -105,53 +107,45 @@ Future startElevatedProcess({required String executable, required String a } Future startProcess({required File executable, List? args, bool useTempBatch = true, bool window = false, String? name, Map? environment}) async { + log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)"); final argsOrEmpty = args ?? []; + final workingDirectory = _getWorkingDirectory(executable); if(useTempBatch) { final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process"); - final tempScriptFile = File("${tempScriptDirectory.path}/process.bat"); + final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat"); final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}'; await tempScriptFile.writeAsString(command, flush: true); final process = await Process.start( tempScriptFile.path, [], - workingDirectory: executable.parent.path, + workingDirectory: workingDirectory, environment: environment, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, runInShell: window ); - return _withLogger(name, executable, process, window); + return _ExtendedProcess(process, true); } final process = await Process.start( executable.path, args ?? [], - workingDirectory: executable.parent.path, + workingDirectory: workingDirectory, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, runInShell: window ); - return _withLogger(name, executable, process, window); + return _ExtendedProcess(process, true); } -_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) { - final extendedProcess = _ExtendedProcess(process, true); - final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log"); - loggingFile.parent.createSync(recursive: true); - if(loggingFile.existsSync()) { - loggingFile.deleteSync(); +String? _getWorkingDirectory(File executable) { + try { + log("[PROCESS] Calculating working directory for $executable"); + final workingDirectory = executable.parent.resolveSymbolicLinksSync(); + log("[PROCESS] Using working directory: $workingDirectory"); + return workingDirectory; + }catch(error) { + log("[PROCESS] Cannot infer working directory: $error"); + return null; } - - final semaphore = Semaphore(1); - void logEvent(String event) async { - await semaphore.acquire(); - await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true); - semaphore.release(); - } - extendedProcess.stdOutput.listen(logEvent); - extendedProcess.stdError.listen(logEvent); - if(!window) { - extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n")); - } - return extendedProcess; } final _NtResumeProcess = _ntdll.lookupFunction watchProcess(int pid) async { return await completer.future; } -// TODO: Template -List createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) { +List createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) { + log("[PROCESS] Generating reboot args"); if(password.isEmpty) { username = '${_parseUsername(username, host)}@projectreboot.dev'; } password = password.isNotEmpty ? password : "Rebooted"; - final args = [ - "-epicapp=Fortnite", - "-epicenv=Prod", - "-epiclocale=en-us", - "-epicportal", - "-skippatchcheck", - "-nobe", - "-fromfl=eac", - "-fltoken=3db3ba5dcbd2e16703f3978d", - "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=$username", - "-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}", - "-AUTH_TYPE=epic" - ]; + final args = LinkedHashMap( + equals: (a, b) => a.toUpperCase() == b.toUpperCase(), + hashCode: (a) => a.toUpperCase().hashCode + ); + args.addAll({ + "-epicapp": "Fortnite", + "-epicenv": "Prod", + "-epiclocale": "en-us", + "-epicportal": "", + "-skippatchcheck": "", + "-nobe": "", + "-fromfl": "eac", + "-fltoken": "3db3ba5dcbd2e16703f3978d", + "-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", + "-AUTH_LOGIN": username, + "-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted", + "-AUTH_TYPE": "epic" + }); - if(log) { - args.add("-log"); + if(logging) { + args["-log"] = ""; } if(host) { - args.addAll([ - "-nosplash", - "-nosound" - ]); + args["-nosplash"] = ""; + args["-nosound"] = ""; if(hostType == GameServerType.headless){ - args.add("-nullrhi"); + args["-nullrhi"] = ""; } } - if(additionalArgs.isNotEmpty){ - args.addAll(additionalArgs.split(" ")); + log("[PROCESS] Default args: $args"); + log("[PROCESS] Adding custom args: $additionalArgs"); + for(final additionalArg in additionalArgs.split(" ")) { + log("[PROCESS] Processing custom arg: $additionalArg"); + final separatorIndex = additionalArg.indexOf("="); + final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex); + log("[PROCESS] Custom arg key: $argName"); + final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1); + log("[PROCESS] Custom arg value: $argValue"); + args[argName] = argValue; + log("[PROCESS] Updated args: $args"); } - return args; + log("[PROCESS] Final args result: $args"); + return args.entries + .map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}") + .toList(); } void handleGameOutput({ @@ -257,16 +265,22 @@ void handleGameOutput({ required void Function() onBuildCorrupted, }) { if (line.contains(kShutdownLine)) { + log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line"); onShutdown(); }else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ + log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line"); onBuildCorrupted(); }else if(kCannotConnectErrors.any((element) => line.contains(element))){ + log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line"); onTokenError(); }else if(kLoggedInLines.every((entry) => line.contains(entry))) { + log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line"); onLoggedIn(); }else if(line.contains(kGameFinishedLine) && host) { + log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line"); onMatchEnd(); }else if(line.contains(kDisplayInitializedLine) && host) { + log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line"); onDisplayAttached(); } } diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index a8a9e22..c4c93d9 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -79,7 +79,7 @@ "settingsClientDescription": "Configure the internal files used by the launcher for Fortnite", "settingsClientOptionsName": "Options", "settingsClientOptionsDescription": "Configure additional options for Fortnite", - "settingsClientConsoleName": "Unreal engine console", + "settingsClientConsoleName": "Unreal engine patcher", "settingsClientConsoleDescription": "Unlocks the Unreal Engine Console", "settingsClientConsoleKeyName": "Unreal engine console key", "settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console", @@ -88,7 +88,7 @@ "settingsClientMemoryName": "Memory patcher", "settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak", "settingsClientArgsName": "Custom launch arguments", - "settingsClientArgsDescription": "Additional arguments to use when launching the game", + "settingsClientArgsDescription": "Additional arguments to use when launching Fortnite", "settingsClientArgsPlaceholder": "Arguments...", "settingsServerName": "Internal files", "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", @@ -156,6 +156,7 @@ "launchingGameClientAndServer": "Launching the game client and server...", "startGameServer": "Start a game server", "usernameOrEmail": "Username/Email", + "invalidEmail": "Invalid email", "usernameOrEmailPlaceholder": "Type your username or email", "password": "Password", "passwordPlaceholder": "Type your password, if you want to use one", diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index 09f3585..5b42b7d 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -41,8 +41,7 @@ class GameController extends GetxController { password = TextEditingController(text: _storage?.read("password") ?? ""); password.addListener(() => _storage?.write("password", password.text)); customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? ""); - customLaunchArgs.addListener(() => - _storage?.write("custom_launch_args", customLaunchArgs.text)); + customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text)); started = RxBool(false); instance = Rxn(); consoleKey = Rx(_readConsoleKey()); diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index 64b8ac0..65df3e8 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -28,6 +28,7 @@ class HostingController extends GetxController { late final RxBool published; late final Rxn instance; late final Rxn> servers; + late final TextEditingController customLaunchArgs; late final Semaphore _semaphore; HostingController() { @@ -62,6 +63,8 @@ class HostingController extends GetxController { servers.value = event; published.value = event.any((element) => element.id == uuid); }); + customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? ""); + customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text)); _semaphore = Semaphore(); } diff --git a/gui/lib/src/messenger/implementation/profile.dart b/gui/lib/src/messenger/implementation/profile.dart index fc09bd2..f892f0a 100644 --- a/gui/lib/src/messenger/implementation/profile.dart +++ b/gui/lib/src/messenger/implementation/profile.dart @@ -1,3 +1,4 @@ +import 'package:email_validator/email_validator.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show Icons; import 'package:get/get.dart'; @@ -23,6 +24,17 @@ Future showProfileForm(BuildContext context) async{ label: translations.usernameOrEmail, child: TextFormBox( placeholder: translations.usernameOrEmailPlaceholder, + validator: (text) { + if(_gameController.password.text.isEmpty) { + return null; + } + + if(EmailValidator.validate(_gameController.username.text)) { + return null; + } + + return translations.invalidEmail; + }, controller: _gameController.username, autovalidateMode: AutovalidateMode.always, enableSuggestions: true, diff --git a/gui/lib/src/page/implementation/host_page.dart b/gui/lib/src/page/implementation/host_page.dart index cc14d21..d2144b4 100644 --- a/gui/lib/src/page/implementation/host_page.dart +++ b/gui/lib/src/page/implementation/host_page.dart @@ -199,6 +199,17 @@ class _HostingPageState extends RebootPageState { title: Text(translations.settingsServerOptionsName), subtitle: Text(translations.settingsServerOptionsSubtitle), children: [ + SettingTile( + icon: Icon( + FluentIcons.options_24_regular + ), + title: Text(translations.settingsClientArgsName), + subtitle: Text(translations.settingsClientArgsDescription), + content: TextFormBox( + placeholder: translations.settingsClientArgsPlaceholder, + controller: _hostingController.customLaunchArgs, + ) + ), SettingTile( icon: Icon( FluentIcons.window_console_20_regular @@ -253,7 +264,7 @@ class _HostingPageState extends RebootPageState { FilteringTextInputFormatter.digitsOnly ] ) - ), + ) ], ); diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 5c63485..74df51f 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -251,7 +251,7 @@ class _LaunchButtonState extends State { host, hostType, false, - "" + host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text ); log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}"); final gameProcess = await startProcess( diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 394fc5d..c858e2d 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Graphical User Interface for Project Reboot -version: "9.2.2" +version: "9.2.3" publish_to: 'none' @@ -74,6 +74,9 @@ dependencies: package_info_plus: ^8.0.0 version: ^3.0.2 + # Validate profile + email_validator: ^3.0.0 + dependency_overrides: xml: ^6.3.0 http: ^0.13.5 @@ -98,4 +101,9 @@ flutter: - assets/backend/profiles/ - assets/backend/public/ - assets/backend/responses/ + - assets/backend/responses/Athena/ + - assets/backend/responses/Athena/BattlePass/ + - assets/backend/responses/Athena/Discovery/ + - assets/backend/responses/Campaign/ + - assets/backend/responses/CloudDir/ - assets/build/ \ No newline at end of file