From cd8c8e6dd9c9a0df3e18c26dd091b26f7446541c Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 10 Jul 2024 15:11:49 +0200 Subject: [PATCH 1/4] 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 From 1ef4e76768cd39b66fc111a96f03168f5808e4f4 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 10 Jul 2024 15:19:20 +0200 Subject: [PATCH 2/4] Small fix to display errors and warnings from backend --- gui/lib/src/messenger/implementation/server.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/lib/src/messenger/implementation/server.dart b/gui/lib/src/messenger/implementation/server.dart index 4f51123..4f14c65 100644 --- a/gui/lib/src/messenger/implementation/server.dart +++ b/gui/lib/src/messenger/implementation/server.dart @@ -307,7 +307,9 @@ extension ServerControllerDialog on BackendController { onDismissed: onDismissed, action: action ); - _infoBars.add(result); + if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) { + _infoBars.add(result); + } return result; } } \ No newline at end of file From 582270849e846df106effc9101419b42acbc0bda Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Wed, 10 Jul 2024 15:40:52 +0200 Subject: [PATCH 3/4] Released 9.2.4 --- gui/lib/src/controller/backend_controller.dart | 18 +++++++++--------- gui/pubspec.yaml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index bacbe6d..6863376 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -105,16 +105,12 @@ class BackendController extends GetxController { return; } + final serverType = type.value; final hostData = this.host.text.trim(); final portData = this.port.text.trim(); - if(type() != ServerType.local) { - started.value = true; + started.value = true; + if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) { yield ServerResult(ServerResultType.starting); - }else { - started.value = false; - if(portData != kDefaultBackendPort.toString()) { - yield ServerResult(ServerResultType.starting); - } } if (hostData.isEmpty) { @@ -136,7 +132,7 @@ class BackendController extends GetxController { return; } - if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { + if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { yield ServerResult(ServerResultType.freeingPort); final result = await freeBackendPort(); yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); @@ -146,7 +142,7 @@ class BackendController extends GetxController { } } - switch(type()){ + switch(serverType){ case ServerType.embedded: final process = await startEmbeddedBackend(detached.value); embeddedProcessPid = process.pid; @@ -173,6 +169,10 @@ class BackendController extends GetxController { } localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData")); + }else { + // If the local server is running on port 3551 there is no reverse proxy running + // We only need to check if everything is working + started.value = false; } break; diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index c858e2d..47cdaf7 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.3" +version: "9.2.4" publish_to: 'none' From 4c3fe9bc65c83f72371a7b69b47e20282a3ea8d8 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Sun, 18 Aug 2024 20:29:09 +0200 Subject: [PATCH 4/4] Released 9.2.5 --- backend/index.js | 2 +- common/lib/src/constant/game.dart | 2 +- common/lib/src/extension/path.dart | 19 +- common/lib/src/util/backend.dart | 7 +- common/lib/src/util/process.dart | 12 +- gui/assets/backend/lawinserver.exe | Bin 86146729 -> 86146709 bytes gui/lib/l10n/reboot_en.arb | 9 +- gui/lib/main.dart | 16 +- .../src/controller/backend_controller.dart | 88 ++++-- gui/lib/src/controller/dll_controller.dart | 265 ++++++++++++++++++ gui/lib/src/controller/game_controller.dart | 43 +-- .../src/controller/hosting_controller.dart | 6 +- .../src/controller/settings_controller.dart | 257 +---------------- .../src/messenger/implementation/server.dart | 36 ++- .../src/page/implementation/backend_page.dart | 5 +- .../src/page/implementation/home_page.dart | 8 +- .../src/page/implementation/host_page.dart | 37 +-- .../src/page/implementation/play_page.dart | 27 +- .../page/implementation/settings_page.dart | 13 - gui/lib/src/widget/game_start_button.dart | 32 ++- gui/pubspec.yaml | 2 +- 21 files changed, 503 insertions(+), 383 deletions(-) create mode 100644 gui/lib/src/controller/dll_controller.dart diff --git a/backend/index.js b/backend/index.js index 6cdb596..5c4b4f4 100644 --- a/backend/index.js +++ b/backend/index.js @@ -35,7 +35,7 @@ express.use(require("./structure/matchmaking.js")); express.use(require("./structure/cloudstorage.js")); express.use(require("./structure/mcp.js")); -const port = process.env.PORT || 3551; +const port = 3551; express.listen(port, () => { console.log("LawinServer started listening on port", port); diff --git a/common/lib/src/constant/game.dart b/common/lib/src/constant/game.dart index 7e7655a..4e6c283 100644 --- a/common/lib/src/constant/game.dart +++ b/common/lib/src/constant/game.dart @@ -11,7 +11,7 @@ const List kCorruptedBuildErrors = [ "Critical error", "when 0 bytes remain", "Pak chunk signature verification failed!", - "Couldn't find pak signature file" + "LogWindows:Error: Fatal error!" ]; const List kCannotConnectErrors = [ "port 3551 failed: Connection refused", diff --git a/common/lib/src/extension/path.dart b/common/lib/src/extension/path.dart index 3b8ff89..9959744 100644 --- a/common/lib/src/extension/path.dart +++ b/common/lib/src/extension/path.dart @@ -9,9 +9,22 @@ extension FortniteVersionExtension on FortniteVersion { static File? findFile(Directory directory, String name) { try{ - final result = directory.listSync(recursive: true) - .firstWhere((element) => path.basename(element.path) == name); - return File(result.path); + for(final child in directory.listSync()) { + if(child is Directory) { + if(!path.basename(child.path).startsWith("\.")) { + final result = findFile(child, name); + if(result != null) { + return result; + } + } + }else if(child is File) { + if(path.basename(child.path) == name) { + return child; + } + } + } + + return null; }catch(_){ return null; } diff --git a/common/lib/src/util/backend.dart b/common/lib/src/util/backend.dart index 2468f31..eb8f9c0 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -15,13 +15,16 @@ final Semaphore _semaphore = Semaphore(); String? _lastIp; String? _lastPort; -Future startEmbeddedBackend(bool detached) async { +Future startEmbeddedBackend(bool detached, {void Function(String)? onError}) 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.stdError.listen((error) { + log("[BACKEND] Error: $error"); + onError?.call(error); + }); process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); return process; } diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index fa406db..7f90f9b 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -102,8 +102,7 @@ Future startElevatedProcess({required String executable, required String a shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.cbSize = sizeOf(); - var shellResult = ShellExecuteEx(shellInput); - return shellResult == 1; + return ShellExecuteEx(shellInput) == 1; } Future startProcess({required File executable, List? args, bool useTempBatch = true, bool window = false, String? name, Map? environment}) async { @@ -313,7 +312,14 @@ final class _ExtendedProcess implements Process { @override - Future get exitCode => _delegate.exitCode; + Future get exitCode { + try { + return _delegate.exitCode; + }catch(_) { + return watchProcess(_delegate.pid) + .then((_) => -1); + } + } @override bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal); diff --git a/gui/assets/backend/lawinserver.exe b/gui/assets/backend/lawinserver.exe index 39581f50940edc9d0aacbba71806fa8a0c652404..22f73775862f7a11bfc414b6173add0df9e704ca 100644 GIT binary patch delta 11774 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 TnWsY2pXbyJ&x+M0BRc;N32QjH delta 11793 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 doWhenWindowReady(() async { Future> _initStorage() async { final errors = []; try { - await GetStorage("game_storage", settingsDirectory.path).initStorage; - await GetStorage("backend_storage", settingsDirectory.path).initStorage; - await GetStorage("settings_storage", settingsDirectory.path).initStorage; - await GetStorage("hosting_storage", settingsDirectory.path).initStorage; + await GetStorage(GameController.storageName, settingsDirectory.path).initStorage; + await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage; + await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage; + await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage; + await GetStorage(DllController.storageName, settingsDirectory.path).initStorage; }catch(error) { appWithNoStorage = true; errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); @@ -223,6 +225,12 @@ Future> _initStorage() async { errors.add(error); } + try { + Get.put(DllController()); + }catch(error) { + errors.add(error); + } + return errors; } diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index 6863376..48e53cf 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -2,18 +2,24 @@ import 'dart:async'; import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/util/keyboard.dart'; class BackendController extends GetxController { - late final GetStorage? storage; + static const String storageName = "backend_storage"; + static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); + + late final GetStorage? _storage; late final TextEditingController host; late final TextEditingController port; late final Rx type; late final TextEditingController gameServerAddress; late final FocusNode gameServerAddressFocusNode; + late final Rx consoleKey; late final RxBool started; late final RxBool detached; StreamSubscription? worker; @@ -22,13 +28,13 @@ class BackendController extends GetxController { HttpServer? remoteServer; BackendController() { - storage = appWithNoStorage ? null : GetStorage("backend_storage"); + _storage = appWithNoStorage ? null : GetStorage(storageName); started = RxBool(false); - type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0)); + type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0)); type.listen((value) { host.text = _readHost(); port.text = _readPort(); - storage?.write("type", value.index); + _storage?.write("type", value.index); if (!started.value) { return; } @@ -37,13 +43,13 @@ class BackendController extends GetxController { }); host = TextEditingController(text: _readHost()); host.addListener(() => - storage?.write("${type.value.name}_host", host.text)); + _storage?.write("${type.value.name}_host", host.text)); port = TextEditingController(text: _readPort()); port.addListener(() => - storage?.write("${type.value.name}_port", port.text)); - detached = RxBool(storage?.read("detached") ?? false); - detached.listen((value) => storage?.write("detached", value)); - final address = storage?.read("game_server_address"); + _storage?.write("${type.value.name}_port", port.text)); + detached = RxBool(_storage?.read("detached") ?? false); + detached.listen((value) => _storage?.write("detached", value)); + final address = _storage?.read("game_server_address"); gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address); var lastValue = gameServerAddress.text; writeMatchmakingIp(lastValue); @@ -55,7 +61,7 @@ class BackendController extends GetxController { lastValue = newValue; gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length); - storage?.write("game_server_address", newValue); + _storage?.write("game_server_address", newValue); writeMatchmakingIp(newValue); }); watchMatchmakingIp().listen((event) { @@ -64,6 +70,37 @@ class BackendController extends GetxController { } }); gameServerAddressFocusNode = FocusNode(); + consoleKey = Rx(_readConsoleKey()); + _writeConsoleKey(consoleKey.value); + consoleKey.listen((newValue) { + _storage?.write("console_key", newValue.usbHidUsage); + _writeConsoleKey(newValue); + }); + } + + PhysicalKeyboardKey _readConsoleKey() { + final consoleKeyValue = _storage?.read("console_key"); + if(consoleKeyValue == null) { + return _kDefaultConsoleKey; + } + + final consoleKeyNumber = int.tryParse(consoleKeyValue.toString()); + if(consoleKeyNumber == null) { + return _kDefaultConsoleKey; + } + + final consoleKey = PhysicalKeyboardKey(consoleKeyNumber); + if(!consoleKey.isUnrealEngineKey) { + return _kDefaultConsoleKey; + } + + return consoleKey; + } + + Future _writeConsoleKey(PhysicalKeyboardKey keyValue) async { + final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini"); + await defaultInput.parent.create(recursive: true); + await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true); } void joinLocalhost() { @@ -73,18 +110,19 @@ class BackendController extends GetxController { void reset() async { type.value = ServerType.values.elementAt(0); for (final type in ServerType.values) { - storage?.write("${type.name}_host", null); - storage?.write("${type.name}_port", null); + _storage?.write("${type.name}_host", null); + _storage?.write("${type.name}_port", null); } host.text = type.value != ServerType.remote ? kDefaultBackendHost : ""; port.text = kDefaultBackendPort.toString(); gameServerAddress.text = "127.0.0.1"; + consoleKey.value = _kDefaultConsoleKey; detached.value = false; } String _readHost() { - String? value = storage?.read("${type.value.name}_host"); + String? value = _storage?.read("${type.value.name}_host"); if (value != null && value.isNotEmpty) { return value; } @@ -97,9 +135,9 @@ class BackendController extends GetxController { } String _readPort() => - storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString(); + _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString(); - Stream start() async* { + Stream start({required void Function() onExit, required void Function(String) onError}) async* { try { if(started.value) { return; @@ -144,7 +182,18 @@ class BackendController extends GetxController { switch(serverType){ case ServerType.embedded: - final process = await startEmbeddedBackend(detached.value); + final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) { + if(started.value) { + started.value = false; + onError(errorMessage); + } + }); + watchProcess(process.pid).then((_) { + if(started.value) { + started.value = false; + onExit(); + } + }); embeddedProcessPid = process.pid; break; case ServerType.remote: @@ -237,11 +286,14 @@ class BackendController extends GetxController { } } - Stream toggle() async* { + Stream toggle({required void Function() onExit, required void Function(String) onError}) async* { if(started()) { yield* stop(); }else { - yield* start(); + yield* start( + onExit: onExit, + onError: onError + ); } } } \ No newline at end of file diff --git a/gui/lib/src/controller/dll_controller.dart b/gui/lib/src/controller/dll_controller.dart new file mode 100644 index 0000000..3e3b0e8 --- /dev/null +++ b/gui/lib/src/controller/dll_controller.dart @@ -0,0 +1,265 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart'; +import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; +import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:version/version.dart'; +import 'package:yaml/yaml.dart'; + +class DllController extends GetxController { + static const String storageName = "dll_storage"; + + late final GetStorage? _storage; + late final String originalDll; + late final TextEditingController gameServerDll; + late final TextEditingController unrealEngineConsoleDll; + late final TextEditingController backendDll; + late final TextEditingController memoryLeakDll; + late final TextEditingController gameServerPort; + late final Rx timer; + late final TextEditingController url; + late final RxBool customGameServer; + late final RxnInt timestamp; + late final Map> _operations; + late final Rx status; + InfoBarEntry? infoBarEntry; + Future? _updater; + + DllController() { + _storage = appWithNoStorage ? null : GetStorage(storageName); + gameServerDll = _createController("game_server", InjectableDll.reboot); + unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); + backendDll = _createController("backend", InjectableDll.cobalt); + memoryLeakDll = _createController("memory_leak", InjectableDll.memory); + gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort); + gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text)); + final timerIndex = _storage?.read("timer"); + timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); + timer.listen((value) => _storage?.write("timer", value.index)); + url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl); + url.addListener(() => _storage?.write("update_url", url.text)); + status = Rx(UpdateStatus.waiting); + customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); + customGameServer.listen((value) => _storage?.write("custom_game_server", value)); + timestamp = RxnInt(_storage?.read("ts")); + timestamp.listen((value) => _storage?.write("ts", value)); + _operations = {}; + } + + TextEditingController _createController(String key, InjectableDll dll) { + final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll)); + controller.addListener(() => _storage?.write(key, controller.text)); + return controller; + } + + void resetGame() { + gameServerDll.text = _getDefaultPath(InjectableDll.reboot); + unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console); + backendDll.text = _getDefaultPath(InjectableDll.cobalt); + memoryLeakDll.text = _getDefaultPath(InjectableDll.memory); + } + + void resetServer() { + gameServerPort.text = kDefaultGameServerPort; + timer.value = UpdateTimer.hour; + url.text = kRebootDownloadUrl; + status.value = UpdateStatus.waiting; + customGameServer.value = false; + timestamp.value = null; + updateGameServerDll(); + } + + Future updateGameServerDll({bool force = false, bool silent = false}) async { + if(_updater != null) { + return await _updater!; + } + + final result = _updateGameServerDll(force, silent); + _updater = result; + return await result; + } + + Future _updateGameServerDll(bool force, bool silent) async { + try { + if(customGameServer.value) { + status.value = UpdateStatus.success; + return true; + } + + final needsUpdate = await hasRebootDllUpdate( + timestamp.value, + hours: timer.value.hours, + force: force + ); + if(!needsUpdate) { + status.value = UpdateStatus.success; + return true; + } + + if(!silent) { + infoBarEntry = showRebootInfoBar( + translations.downloadingDll("reboot"), + loading: true, + duration: null + ); + } + timestamp.value = await downloadRebootDll(url.text); + status.value = UpdateStatus.success; + infoBarEntry?.close(); + if(!silent) { + infoBarEntry = showRebootInfoBar( + translations.downloadDllSuccess("reboot"), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } + return true; + }catch(message) { + infoBarEntry?.close(); + var error = message.toString(); + error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + status.value = UpdateStatus.error; + showRebootInfoBar( + translations.downloadDllError("reboot.dll", error.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error, + action: Button( + onPressed: () => updateGameServerDll( + force: true, + silent: silent + ), + child: Text(translations.downloadDllRetry), + ) + ); + return false; + }finally { + _updater = null; + } + } + + (File, bool) getInjectableData(InjectableDll dll) { + final defaultPath = canonicalize(_getDefaultPath(dll)); + switch(dll){ + case InjectableDll.reboot: + if(customGameServer.value) { + final file = File(gameServerDll.text); + if(file.existsSync()) { + return (file, true); + } + } + + return (rebootDllFile, false); + case InjectableDll.console: + final ue4ConsoleFile = File(unrealEngineConsoleDll.text); + return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); + case InjectableDll.cobalt: + final backendFile = File(backendDll.text); + return (backendFile, canonicalize(backendFile.path) != defaultPath); + case InjectableDll.memory: + final memoryLeakFile = File(memoryLeakDll.text); + return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath); + } + } + + String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll"; + + Future downloadCriticalDllInteractive(String filePath, {bool silent = false}) { + log("[DLL] Asking for $filePath(silent: $silent)"); + final old = _operations[filePath]; + if(old != null) { + log("[DLL] Download task already exists"); + return old; + } + + log("[DLL] Creating new download task..."); + final newRun = _downloadCriticalDllInteractive(filePath, silent); + _operations[filePath] = newRun; + return newRun; + } + + Future _downloadCriticalDllInteractive(String filePath, bool silent) async { + final fileName = basename(filePath).toLowerCase(); + log("[DLL] File name: $fileName"); + InfoBarEntry? entry; + try { + if (fileName == "reboot.dll") { + log("[DLL] Downloading reboot.dll..."); + return await updateGameServerDll( + silent: silent + ); + } + + if(File(filePath).existsSync()) { + log("[DLL] File already exists"); + return true; + } + + final fileNameWithoutExtension = basenameWithoutExtension(filePath); + if(!silent) { + entry = showRebootInfoBar( + translations.downloadingDll(fileNameWithoutExtension), + loading: true, + duration: null + ); + } + await downloadCriticalDll(fileName, filePath); + entry?.close(); + if(!silent) { + entry = await showRebootInfoBar( + translations.downloadDllSuccess(fileNameWithoutExtension), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } + return true; + }catch(message) { + log("[DLL] Error: $message"); + entry?.close(); + var error = message.toString(); + error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + final completer = Completer(); + await showRebootInfoBar( + translations.downloadDllError(fileName, error.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error, + onDismissed: () => completer.complete(null), + action: Button( + onPressed: () async { + await downloadCriticalDllInteractive(filePath); + completer.complete(null); + }, + child: Text(translations.downloadDllRetry), + ) + ); + await completer.future; + return false; + }finally { + _operations.remove(fileName); + } + } +} + +extension _UpdateTimerExtension on UpdateTimer { + int get hours { + switch(this) { + case UpdateTimer.never: + return -1; + case UpdateTimer.hour: + return 1; + case UpdateTimer.day: + return 24; + case UpdateTimer.week: + return 24 * 7; + } + } +} \ No newline at end of file diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index 5b42b7d..dc922d3 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -1,18 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/util/keyboard.dart'; - -import '../../main.dart'; +import 'package:reboot_launcher/main.dart'; class GameController extends GetxController { - static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); + static const String storageName = "game_storage"; late final GetStorage? _storage; late final TextEditingController username; @@ -22,10 +18,9 @@ class GameController extends GetxController { late final Rxn _selectedVersion; late final RxBool started; late final Rxn instance; - late final Rx consoleKey; GameController() { - _storage = appWithNoStorage ? null : GetStorage("game_storage"); + _storage = appWithNoStorage ? null : GetStorage(storageName); Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); final decodedVersions = decodedVersionsJson .map((entry) => FortniteVersion.fromJson(entry)) @@ -44,37 +39,6 @@ class GameController extends GetxController { customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text)); started = RxBool(false); instance = Rxn(); - consoleKey = Rx(_readConsoleKey()); - _writeConsoleKey(consoleKey.value); - consoleKey.listen((newValue) { - _storage?.write("console_key", newValue.usbHidUsage); - _writeConsoleKey(newValue); - }); - } - - PhysicalKeyboardKey _readConsoleKey() { - final consoleKeyValue = _storage?.read("console_key"); - if(consoleKeyValue == null) { - return _kDefaultConsoleKey; - } - - final consoleKeyNumber = int.tryParse(consoleKeyValue.toString()); - if(consoleKeyNumber == null) { - return _kDefaultConsoleKey; - } - - final consoleKey = PhysicalKeyboardKey(consoleKeyNumber); - if(!consoleKey.isUnrealEngineKey) { - return _kDefaultConsoleKey; - } - - return consoleKey; - } - - Future _writeConsoleKey(PhysicalKeyboardKey keyValue) async { - final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini"); - await defaultInput.parent.create(recursive: true); - await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true); } void reset() { @@ -82,6 +46,7 @@ class GameController extends GetxController { password.text = ""; customLaunchArgs.text = ""; versions.value = []; + _selectedVersion.value = null; instance.value = null; } diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index 65df3e8..f1fd440 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -12,6 +12,8 @@ import 'package:sync/semaphore.dart'; import 'package:uuid/uuid.dart'; class HostingController extends GetxController { + static const String storageName = "hosting_storage"; + late final GetStorage? _storage; late final String uuid; late final TextEditingController name; @@ -32,7 +34,7 @@ class HostingController extends GetxController { late final Semaphore _semaphore; HostingController() { - _storage = appWithNoStorage ? null : GetStorage("hosting_storage"); + _storage = appWithNoStorage ? null : GetStorage(storageName); uuid = _storage?.read("uuid") ?? const Uuid().v4(); _storage?.write("uuid", uuid); name = TextEditingController(text: _storage?.read("name")); @@ -138,10 +140,10 @@ class HostingController extends GetxController { description.text = ""; showPassword.value = false; discoverable.value = false; - started.value = false; instance.value = null; type.value = GameServerType.headless; autoRestart.value = true; + customLaunchArgs.text = ""; } FortniteServer? findServerById(String uuid) { diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index 50d31ec..862f4b2 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -15,37 +15,19 @@ import 'package:version/version.dart'; import 'package:yaml/yaml.dart'; class SettingsController extends GetxController { + static const String storageName = "settings_storage"; + late final GetStorage? _storage; - late final String originalDll; - late final TextEditingController gameServerDll; - late final TextEditingController unrealEngineConsoleDll; - late final TextEditingController backendDll; - late final TextEditingController memoryLeakDll; - late final TextEditingController gameServerPort; late final RxString language; late final Rx themeMode; - late final RxnInt timestamp; - late final Rx status; - late final Rx timer; - late final TextEditingController url; - late final RxBool customGameServer; late final RxBool firstRun; - late final Map> _operations; late double width; late double height; late double? offsetX; late double? offsetY; - InfoBarEntry? infoBarEntry; - Future? _updater; SettingsController() { - _storage = appWithNoStorage ? null : GetStorage("settings_storage"); - gameServerDll = _createController("game_server", InjectableDll.reboot); - unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); - backendDll = _createController("backend", InjectableDll.cobalt); - memoryLeakDll = _createController("memory_leak", InjectableDll.memory); - gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort); - gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text)); + _storage = appWithNoStorage ? null : GetStorage(storageName); width = _storage?.read("width") ?? kDefaultWindowWidth; height = _storage?.read("height") ?? kDefaultWindowHeight; offsetX = _storage?.read("offset_x"); @@ -54,25 +36,8 @@ class SettingsController extends GetxController { themeMode.listen((value) => _storage?.write("theme", value.index)); language = RxString(_storage?.read("language") ?? currentLocale); language.listen((value) => _storage?.write("language", value)); - timestamp = RxnInt(_storage?.read("ts")); - timestamp.listen((value) => _storage?.write("ts", value)); - final timerIndex = _storage?.read("timer"); - timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); - timer.listen((value) => _storage?.write("timer", value.index)); - url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl); - url.addListener(() => _storage?.write("update_url", url.text)); - status = Rx(UpdateStatus.waiting); - customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); - customGameServer.listen((value) => _storage?.write("custom_game_server", value)); firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true); firstRun.listen((value) => _storage?.write("first_run_tutorial", value)); - _operations = {}; - } - - TextEditingController _createController(String key, InjectableDll dll) { - final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll)); - controller.addListener(() => _storage?.write(key, controller.text)); - return controller; } void saveWindowSize(Size size) { @@ -87,32 +52,18 @@ class SettingsController extends GetxController { _storage?.write("offset_y", offsetY); } - void reset(){ - gameServerDll.text = _getDefaultPath(InjectableDll.reboot); - unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console); - backendDll.text = _getDefaultPath(InjectableDll.cobalt); - memoryLeakDll.text = _getDefaultPath(InjectableDll.memory); - gameServerPort.text = kDefaultGameServerPort; - timestamp.value = null; - timer.value = UpdateTimer.never; - url.text = kRebootDownloadUrl; - status.value = UpdateStatus.waiting; - customGameServer.value = false; - updateReboot(); - } - Future notifyLauncherUpdate() async { - if(appVersion == null) { + if (appVersion == null) { return; } final pubspec = await _getPubspecYaml(); - if(pubspec == null) { + if (pubspec == null) { return; } final latestVersion = Version.parse(pubspec["version"]); - if(latestVersion <= appVersion) { + if (latestVersion <= appVersion) { return; } @@ -125,7 +76,8 @@ class SettingsController extends GetxController { child: Text(translations.updateAvailableAction), onPressed: () { infoBar.close(); - launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); + launchUrl(Uri.parse( + "https://github.com/Auties00/reboot_launcher/releases")); }, ) ); @@ -133,201 +85,16 @@ class SettingsController extends GetxController { Future _getPubspecYaml() async { try { - final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); - if(pubspecResponse.statusCode != 200) { + final pubspecResponse = await http.get(Uri.parse( + "https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); + if (pubspecResponse.statusCode != 200) { return null; } return loadYaml(pubspecResponse.body); - }catch(error) { + } catch (error) { log("[UPDATER] Cannot check for updates: $error"); return null; } } - - Future updateReboot({bool force = false, bool silent = false}) async { - if(_updater != null) { - return await _updater!; - } - - final result = _updateReboot(force, silent); - _updater = result; - return await result; - } - - Future _updateReboot(bool force, bool silent) async { - try { - if(customGameServer.value) { - status.value = UpdateStatus.success; - return true; - } - - final needsUpdate = await hasRebootDllUpdate( - timestamp.value, - hours: timer.value.hours, - force: force - ); - if(!needsUpdate) { - status.value = UpdateStatus.success; - return true; - } - - if(!silent) { - infoBarEntry = showRebootInfoBar( - translations.downloadingDll("reboot"), - loading: true, - duration: null - ); - } - timestamp.value = await downloadRebootDll(url.text); - status.value = UpdateStatus.success; - infoBarEntry?.close(); - if(!silent) { - infoBarEntry = showRebootInfoBar( - translations.downloadDllSuccess("reboot"), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - } - return true; - }catch(message) { - infoBarEntry?.close(); - var error = message.toString(); - error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - status.value = UpdateStatus.error; - showRebootInfoBar( - translations.downloadDllError("reboot.dll", error.toString()), - duration: infoBarLongDuration, - severity: InfoBarSeverity.error, - action: Button( - onPressed: () => updateReboot( - force: true, - silent: silent - ), - child: Text(translations.downloadDllRetry), - ) - ); - return false; - }finally { - _updater = null; - } - } - - (File, bool) getInjectableData(InjectableDll dll) { - final defaultPath = canonicalize(_getDefaultPath(dll)); - switch(dll){ - case InjectableDll.reboot: - if(customGameServer.value) { - final file = File(gameServerDll.text); - if(file.existsSync()) { - return (file, true); - } - } - - return (rebootDllFile, false); - case InjectableDll.console: - final ue4ConsoleFile = File(unrealEngineConsoleDll.text); - return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); - case InjectableDll.cobalt: - final backendFile = File(backendDll.text); - return (backendFile, canonicalize(backendFile.path) != defaultPath); - case InjectableDll.memory: - final memoryLeakFile = File(memoryLeakDll.text); - return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath); - } - } - - String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll"; - - Future downloadCriticalDllInteractive(String filePath, {bool silent = false}) { - log("[DLL] Asking for $filePath(silent: $silent)"); - final old = _operations[filePath]; - if(old != null) { - log("[DLL] Download task already exists"); - return old; - } - - log("[DLL] Creating new download task..."); - final newRun = _downloadCriticalDllInteractive(filePath, silent); - _operations[filePath] = newRun; - return newRun; - } - - Future _downloadCriticalDllInteractive(String filePath, bool silent) async { - final fileName = basename(filePath).toLowerCase(); - log("[DLL] File name: $fileName"); - InfoBarEntry? entry; - try { - if (fileName == "reboot.dll") { - log("[DLL] Downloading reboot.dll..."); - return await updateReboot( - silent: silent - ); - } - - if(File(filePath).existsSync()) { - log("[DLL] File already exists"); - return true; - } - - final fileNameWithoutExtension = basenameWithoutExtension(filePath); - if(!silent) { - entry = showRebootInfoBar( - translations.downloadingDll(fileNameWithoutExtension), - loading: true, - duration: null - ); - } - await downloadCriticalDll(fileName, filePath); - entry?.close(); - if(!silent) { - entry = await showRebootInfoBar( - translations.downloadDllSuccess(fileNameWithoutExtension), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - } - return true; - }catch(message) { - log("[DLL] Error: $message"); - entry?.close(); - var error = message.toString(); - error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - final completer = Completer(); - await showRebootInfoBar( - translations.downloadDllError(fileName, error.toString()), - duration: infoBarLongDuration, - severity: InfoBarSeverity.error, - onDismissed: () => completer.complete(null), - action: Button( - onPressed: () async { - await downloadCriticalDllInteractive(filePath); - completer.complete(null); - }, - child: Text(translations.downloadDllRetry), - ) - ); - await completer.future; - return false; - }finally { - _operations.remove(fileName); - } - } -} - -extension _UpdateTimerExtension on UpdateTimer { - int get hours { - switch(this) { - case UpdateTimer.never: - return -1; - case UpdateTimer.hour: - return 1; - case UpdateTimer.day: - return 24; - case UpdateTimer.week: - return 24 * 7; - } - } } \ No newline at end of file diff --git a/gui/lib/src/messenger/implementation/server.dart b/gui/lib/src/messenger/implementation/server.dart index 4f14c65..cd5665e 100644 --- a/gui/lib/src/messenger/implementation/server.dart +++ b/gui/lib/src/messenger/implementation/server.dart @@ -15,6 +15,7 @@ import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/cryptography.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:url_launcher/url_launcher.dart'; final List _infoBars = []; @@ -27,7 +28,27 @@ extension ServerControllerDialog on BackendController { Future toggleInteractive() async { cancelInteractive(); - final stream = toggle(); + final stream = toggle( + onExit: () { + cancelInteractive(); + _showRebootInfoBar( + translations.backendProcessError, + severity: InfoBarSeverity.error + ); + }, + onError: (errorMessage) { + cancelInteractive(); + _showRebootInfoBar( + translations.backendErrorMessage, + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) + ); + } + ); final completer = Completer(); InfoBarEntry? entry; worker = stream.listen((event) { @@ -54,19 +75,6 @@ extension ServerControllerDialog on BackendController { duration: null ); case ServerResultType.startSuccess: - final embeddedProcessPid = this.embeddedProcessPid; - if(embeddedProcessPid != null) { - watchProcess(embeddedProcessPid).then((_) { - if(started.value) { - started.value = false; - _showRebootInfoBar( - translations.backendProcessError, - severity: InfoBarSeverity.error - ); - } - }); - } - return _showRebootInfoBar( type.value == ServerType.local ? translations.checkedServer : translations.startedServer, severity: InfoBarSeverity.success diff --git a/gui/lib/src/page/implementation/backend_page.dart b/gui/lib/src/page/implementation/backend_page.dart index 947a6e7..a043b3f 100644 --- a/gui/lib/src/page/implementation/backend_page.dart +++ b/gui/lib/src/page/implementation/backend_page.dart @@ -43,7 +43,6 @@ class BackendPage extends RebootPage { } class _BackendPageState extends RebootPageState { - final GameController _gameController = Get.find(); final BackendController _backendController = Get.find(); InfoBarEntry? _infoBarEntry; @@ -56,7 +55,7 @@ class _BackendPageState extends RebootPageState { } if(keyEvent.physicalKey.isUnrealEngineKey) { - _gameController.consoleKey.value = keyEvent.physicalKey; + _backendController.consoleKey.value = keyEvent.physicalKey; } _infoBarEntry?.close(); @@ -194,7 +193,7 @@ class _BackendPageState extends RebootPageState { duration: null ); }, - child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""), + child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""), ), ) ); diff --git a/gui/lib/src/page/implementation/home_page.dart b/gui/lib/src/page/implementation/home_page.dart index 8655e0d..31808d2 100644 --- a/gui/lib/src/page/implementation/home_page.dart +++ b/gui/lib/src/page/implementation/home_page.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart' show MaterialPage; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; +import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; @@ -44,6 +45,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA final BackendController _backendController = Get.find(); final HostingController _hostingController = Get.find(); final SettingsController _settingsController = Get.find(); + final DllController _dllController = Get.find(); final GlobalKey _searchKey = GlobalKey(); final FocusNode _searchFocusNode = FocusNode(); final TextEditingController _searchController = TextEditingController(); @@ -134,9 +136,9 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA } for(final injectable in InjectableDll.values) { - final (file, custom) = _settingsController.getInjectableData(injectable); + final (file, custom) = _dllController.getInjectableData(injectable); if(!custom) { - _settingsController.downloadCriticalDllInteractive( + _dllController.downloadCriticalDllInteractive( file.path, silent: true ); @@ -144,7 +146,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA } watchDlls().listen((filePath) => showDllDeletedDialog(() { - _settingsController.downloadCriticalDllInteractive(filePath); + _dllController.downloadCriticalDllInteractive(filePath); })); } diff --git a/gui/lib/src/page/implementation/host_page.dart b/gui/lib/src/page/implementation/host_page.dart index d2144b4..5a508eb 100644 --- a/gui/lib/src/page/implementation/host_page.dart +++ b/gui/lib/src/page/implementation/host_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; +import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; @@ -53,6 +54,7 @@ class _HostingPageState extends RebootPageState { final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); final SettingsController _settingsController = Get.find(); + final DllController _dllController = Get.find(); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); @@ -257,7 +259,7 @@ class _HostingPageState extends RebootPageState { contentWidth: 64, content: TextFormBox( placeholder: translations.settingsServerPortName, - controller: _settingsController.gameServerPort, + controller: _dllController.gameServerPort, keyboardType: TextInputType.number, textAlign: TextAlign.center, inputFormatters: [ @@ -284,22 +286,22 @@ class _HostingPageState extends RebootPageState { content: Obx(() => DropDownButton( onOpen: () => inDialog = true, onClose: () => inDialog = false, - leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), + leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), items: { false: translations.settingsServerTypeEmbeddedName, true: translations.settingsServerTypeCustomName }.entries.map((entry) => MenuFlyoutItem( text: Text(entry.value), onPressed: () { - final oldValue = _settingsController.customGameServer.value; + final oldValue = _dllController.customGameServer.value; if(oldValue == entry.key) { return; } - _settingsController.customGameServer.value = entry.key; - _settingsController.infoBarEntry?.close(); + _dllController.customGameServer.value = entry.key; + _dllController.infoBarEntry?.close(); if(!entry.key) { - _settingsController.updateReboot( + _dllController.updateGameServerDll( force: true ); } @@ -308,18 +310,18 @@ class _HostingPageState extends RebootPageState { )) ), Obx(() { - if(!_settingsController.customGameServer.value) { + if(!_dllController.customGameServer.value) { return const SizedBox.shrink(); } return createFileSetting( title: translations.settingsServerFileName, description: translations.settingsServerFileDescription, - controller: _settingsController.gameServerDll + controller: _dllController.gameServerDll ); }), Obx(() { - if(_settingsController.customGameServer.value) { + if(_dllController.customGameServer.value) { return const SizedBox.shrink(); } @@ -331,13 +333,13 @@ class _HostingPageState extends RebootPageState { subtitle: Text(translations.settingsServerMirrorDescription), content: TextFormBox( placeholder: translations.settingsServerMirrorPlaceholder, - controller: _settingsController.url, + controller: _dllController.url, validator: _checkUpdateUrl ) ); }), Obx(() { - if(_settingsController.customGameServer.value) { + if(_dllController.customGameServer.value) { return const SizedBox.shrink(); } @@ -350,13 +352,13 @@ class _HostingPageState extends RebootPageState { content: Obx(() => DropDownButton( onOpen: () => inDialog = true, onClose: () => inDialog = false, - leading: Text(_settingsController.timer.value.text), + leading: Text(_dllController.timer.value.text), items: UpdateTimer.values.map((entry) => MenuFlyoutItem( text: Text(entry.text), onPressed: () { - _settingsController.timer.value = entry; - _settingsController.infoBarEntry?.close(); - _settingsController.updateReboot( + _dllController.timer.value = entry; + _dllController.infoBarEntry?.close(); + _dllController.updateGameServerDll( force: true ); } @@ -431,7 +433,10 @@ class _HostingPageState extends RebootPageState { title: Text(translations.hostResetName), subtitle: Text(translations.hostResetDescription), content: Button( - onPressed: () => showResetDialog(_hostingController.reset), + onPressed: () => showResetDialog(() { + _hostingController.reset(); + _dllController.resetServer(); + }), child: Text(translations.hostResetContent), ) ); diff --git a/gui/lib/src/page/implementation/play_page.dart b/gui/lib/src/page/implementation/play_page.dart index a42f2db..b7d983b 100644 --- a/gui/lib/src/page/implementation/play_page.dart +++ b/gui/lib/src/page/implementation/play_page.dart @@ -1,9 +1,11 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:get/get.dart'; +import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; +import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; @@ -37,7 +39,8 @@ class PlayPage extends RebootPage { class _PlayPageState extends RebootPageState { final SettingsController _settingsController = Get.find(); final GameController _gameController = Get.find(); - + final DllController _dllController = Get.find(); + @override Widget build(BuildContext context) { return Column( @@ -94,6 +97,7 @@ class _PlayPageState extends RebootPageState { ), _options, _internalFiles, + _resetDefaults ]; SettingTile get _internalFiles => SettingTile( @@ -106,17 +110,17 @@ class _PlayPageState extends RebootPageState { createFileSetting( title: translations.settingsClientConsoleName, description: translations.settingsClientConsoleDescription, - controller: _settingsController.unrealEngineConsoleDll + controller: _dllController.unrealEngineConsoleDll ), createFileSetting( title: translations.settingsClientAuthName, description: translations.settingsClientAuthDescription, - controller: _settingsController.backendDll + controller: _dllController.backendDll ), createFileSetting( title: translations.settingsClientMemoryName, description: translations.settingsClientMemoryDescription, - controller: _settingsController.memoryLeakDll + controller: _dllController.memoryLeakDll ), ], ); @@ -141,4 +145,19 @@ class _PlayPageState extends RebootPageState { ) ] ); + + SettingTile get _resetDefaults => SettingTile( + icon: Icon( + FluentIcons.arrow_reset_24_regular + ), + title: Text(translations.gameResetDefaultsName), + subtitle: Text(translations.gameResetDefaultsDescription), + content: Button( + onPressed: () => showResetDialog(() { + _gameController.reset(); + _dllController.resetGame(); + }), + child: Text(translations.gameResetDefaultsContent), + ) + ); } \ No newline at end of file diff --git a/gui/lib/src/page/implementation/settings_page.dart b/gui/lib/src/page/implementation/settings_page.dart index 80eefd6..7f18cc5 100644 --- a/gui/lib/src/page/implementation/settings_page.dart +++ b/gui/lib/src/page/implementation/settings_page.dart @@ -42,7 +42,6 @@ class _SettingsPageState extends RebootPageState { List get settings => [ _language, _theme, - _resetDefaults, _installationDirectory ]; @@ -88,18 +87,6 @@ class _SettingsPageState extends RebootPageState { )).toList() )) ); - - SettingTile get _resetDefaults => SettingTile( - icon: Icon( - FluentIcons.arrow_reset_24_regular - ), - title: Text(translations.settingsUtilsResetDefaultsName), - subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle), - content: Button( - onPressed: () => showResetDialog(_settingsController.reset), - child: Text(translations.settingsUtilsResetDefaultsContent), - ) - ); SettingTile get _installationDirectory => SettingTile( icon: Icon( diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 74df51f..1895d84 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -9,6 +9,7 @@ import 'package:local_notifier/local_notifier.dart'; import 'package:path/path.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart'; +import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; @@ -39,7 +40,7 @@ class _LaunchButtonState extends State { final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); final BackendController _backendController = Get.find(); - final SettingsController _settingsController = Get.find(); + final DllController _dllController = Get.find(); InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameServerInfoBar; @@ -263,15 +264,21 @@ class _LaunchButtonState extends State { "OPENSSL_ia32cap": "~0x20000000" } ); + final instance = host ? _hostingController.instance.value : _gameController.instance.value; void onGameOutput(String line, bool error) { log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line"); - handleGameOutput( line: line, host: host, onShutdown: () => _onStop(reason: _StopReason.normal), onTokenError: () => _onStop(reason: _StopReason.tokenError), - onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError), + onBuildCorrupted: () { + if(instance?.launched == false) { + _onStop(reason: _StopReason.corruptedVersionError); + }else { + _onStop(reason: _StopReason.crash); + } + }, onLoggedIn: () =>_onLoggedIn(host), onMatchEnd: () => _onMatchEnd(version), onDisplayAttached: () => _onDisplayAttached(host, hostType, version) @@ -391,7 +398,7 @@ class _LaunchButtonState extends State { await _injectOrShowError(InjectableDll.console, host); _onGameClientInjected(); }else { - final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); + final gameServerPort = int.tryParse(_dllController.gameServerPort.text); if(gameServerPort != null) { await killProcessByPort(gameServerPort); } @@ -424,7 +431,7 @@ class _LaunchButtonState extends State { loading: true, duration: null ); - final gameServerPort = _settingsController.gameServerPort.text; + final gameServerPort = _dllController.gameServerPort.text; final localPingResult = await pingGameServer( "127.0.0.1:$gameServerPort", timeout: const Duration(minutes: 2) @@ -605,6 +612,7 @@ class _LaunchButtonState extends State { ); break; case _StopReason.tokenError: + _backendController.stop(); showRebootInfoBar( translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none), severity: InfoBarSeverity.error, @@ -615,6 +623,13 @@ class _LaunchButtonState extends State { ) ); break; + case _StopReason.crash: + showRebootInfoBar( + translations.fortniteCrashError(host ? "game server" : "client"), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; case _StopReason.unknownError: showRebootInfoBar( translations.unknownFortniteError(error ?? translations.unknownError), @@ -664,7 +679,7 @@ class _LaunchButtonState extends State { Future _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); - final (file, customDll) = _settingsController.getInjectableData(injectable); + final (file, customDll) = _dllController.getInjectableData(injectable); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); if(await file.exists()) { log("[${host ? 'HOST' : 'GAME'}] Path exists"); @@ -678,7 +693,7 @@ class _LaunchButtonState extends State { } log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); - await _settingsController.downloadCriticalDllInteractive(file.path); + await _dllController.downloadCriticalDllInteractive(file.path); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); return _getDllFileOrStop(injectable, host, true); } @@ -731,7 +746,8 @@ enum _StopReason { matchmakerError, tokenError, unknownError, - exitCode; + exitCode, + crash; bool get isError => name.contains("Error"); } \ No newline at end of file diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 47cdaf7..87c4c42 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.4" +version: "9.2.5" publish_to: 'none'