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 587961d..eb8f9c0 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -15,10 +15,19 @@ final Semaphore _semaphore = Semaphore(); String? _lastIp; String? _lastPort; -Future startEmbeddedBackend(bool detached) async => startProcess( +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"); + onError?.call(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..7f90f9b 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'; @@ -100,58 +102,49 @@ 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 { + 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 +264,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(); } } @@ -299,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 39581f5..22f7377 100644 Binary files a/gui/assets/backend/lawinserver.exe and b/gui/assets/backend/lawinserver.exe differ diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index a8a9e22..ade64df 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", @@ -118,9 +118,7 @@ "settingsUtilsResetDefaultsName": "Reset settings", "settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values", "settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", - "settingsUtilsResetDefaultsContent": "Reset", "settingsUtilsDialogSecondaryAction": "Close", - "settingsUtilsDialogPrimaryAction": "Reset", "selectFortniteName": "Fortnite version", "selectFortniteDescription": "Select the version of Fortnite you want to use", "manageVersionsName": "Manage versions", @@ -156,6 +154,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", @@ -261,6 +260,7 @@ "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", + "fortniteCrashError": "The {name} crashed after being launched", "serverNoLongerAvailableUnnamed": "The previous server is no longer available", "noServerFound": "No server found: invalid or expired link", "settingsUtilsThemeName": "Theme", @@ -320,6 +320,7 @@ "none": "none", "openLog": "Open log", "backendProcessError": "The backend shut down unexpectedly", + "backendErrorMessage": "The backend reported an unexpected error", "welcomeTitle": "Welcome to Reboot Launcher", "welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab", "welcomeAction": "Take the tour", @@ -363,5 +364,8 @@ "promptSettingsTabActionLabel": "Done", "automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!", "automaticGameServerDialogIgnore": "Ignore", - "automaticGameServerDialogStart": "Start server" + "automaticGameServerDialogStart": "Start server", + "gameResetDefaultsName": "Reset", + "gameResetDefaultsDescription": "Resets the game's settings to their default values", + "gameResetDefaultsContent": "Reset" } diff --git a/gui/lib/main.dart b/gui/lib/main.dart index 6490f9f..e54d4aa 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -12,6 +12,7 @@ import 'package:local_notifier/local_notifier.dart'; import 'package:package_info_plus/package_info_plus.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'; @@ -188,10 +189,11 @@ void _initWindow() => 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 bacbe6d..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,24 +135,20 @@ 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; } + 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 +170,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,9 +180,20 @@ class BackendController extends GetxController { } } - switch(type()){ + 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: @@ -173,6 +218,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; @@ -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 09f3585..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)) @@ -41,41 +36,9 @@ 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()); - _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() { @@ -83,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 64b8ac0..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; @@ -28,10 +30,11 @@ 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() { - _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")); @@ -62,6 +65,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(); } @@ -135,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/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/messenger/implementation/server.dart b/gui/lib/src/messenger/implementation/server.dart index 4f51123..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 @@ -307,7 +315,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 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 cc14d21..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); @@ -199,6 +201,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 @@ -246,14 +259,14 @@ class _HostingPageState extends RebootPageState { contentWidth: 64, content: TextFormBox( placeholder: translations.settingsServerPortName, - controller: _settingsController.gameServerPort, + controller: _dllController.gameServerPort, keyboardType: TextInputType.number, textAlign: TextAlign.center, inputFormatters: [ FilteringTextInputFormatter.digitsOnly ] ) - ), + ) ], ); @@ -273,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 ); } @@ -297,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(); } @@ -320,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(); } @@ -339,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 ); } @@ -420,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 5c63485..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; @@ -251,7 +252,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( @@ -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 394fc5d..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.2" +version: "9.2.5" 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