From 2bf084d1200a30485a101cc4d30fd480c0f757cd Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 4 Jun 2024 20:31:06 +0200 Subject: [PATCH] 9.1.3 --- README.md | 21 +- cli/lib/cli.dart | 3 +- cli/lib/src/game.dart | 13 +- common/lib/src/extension/process.dart | 4 +- common/lib/src/model/game_instance.dart | 5 +- common/lib/src/util/build.dart | 14 + common/lib/src/util/process.dart | 25 ++ gui/lib/l10n/reboot_en.arb | 2 +- gui/lib/main.dart | 223 ++++++------ .../src/controller/backend_controller.dart | 33 +- gui/lib/src/controller/game_controller.dart | 35 +- .../src/controller/hosting_controller.dart | 37 +- gui/lib/src/controller/info_controller.dart | 8 - gui/lib/src/controller/update_controller.dart | 84 +++-- gui/lib/src/dialog/implementation/error.dart | 8 +- .../src/page/implementation/home_page.dart | 167 ++++++--- .../src/page/implementation/info_page.dart | 41 ++- .../page/implementation/server_host_page.dart | 19 +- .../page/implementation/settings_page.dart | 5 + gui/lib/src/util/dll.dart | 80 +++-- gui/lib/src/util/log.dart | 12 +- gui/lib/src/util/os.dart | 45 ++- gui/lib/src/widget/game_start_button.dart | 317 +++++++++--------- gui/lib/src/widget/server_type_selector.dart | 2 + gui/lib/src/widget/version_selector.dart | 27 +- gui/lib/src/widget/version_selector_tile.dart | 8 +- gui/pubspec.yaml | 2 +- .../exe/custom-inno-setup-script.iss | 7 +- 28 files changed, 731 insertions(+), 516 deletions(-) delete mode 100644 gui/lib/src/controller/info_controller.dart diff --git a/README.md b/README.md index 9a89829..96f2c39 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ -# Reboot Launcher +![Banner](https://i.imgur.com/p0P4tcI.png) -![Screenshot (34)](https://github.com/Auties00/reboot_launcher/assets/28218457/de2cac8e-7060-4e11-a91f-e01e3c174b9c) -![Screenshot (35)](https://github.com/Auties00/reboot_launcher/assets/28218457/de43d2b8-09fc-4d34-beb1-aa6f7fcaa479) -![Screenshot (36)](https://github.com/Auties00/reboot_launcher/assets/28218457/3337f5cd-81d6-45d8-ab47-8018fb8a6cee) -![Screenshot (37)](https://github.com/Auties00/reboot_launcher/assets/28218457/51086ec7-5e68-4411-b704-7837970741c8) -![Screenshot (38)](https://github.com/Auties00/reboot_launcher/assets/28218457/9aca3e00-85e3-4580-95bd-fef8b389f40b) -![Screenshot (39)](https://github.com/Auties00/reboot_launcher/assets/28218457/faa5d3a3-18c2-4d53-84c5-6eadc0bf4069) -![Screenshot (33)](https://github.com/Auties00/reboot_launcher/assets/28218457/6c449aa6-e515-4680-9ee2-d219761f3268) +GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/) +Join our discord at https://discord.gg/reboot + +## Modules + +- COMMON: Shared business logic for CLI and GUI modules +- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart +- GUI: Stable graphical user interface to play and host Fortnite S0-14 + +## Installation + +Check the releases section \ No newline at end of file diff --git a/cli/lib/cli.dart b/cli/lib/cli.dart index 4d1db6e..2323653 100644 --- a/cli/lib/cli.dart +++ b/cli/lib/cli.dart @@ -5,7 +5,6 @@ import 'package:reboot_cli/src/game.dart'; import 'package:reboot_cli/src/reboot.dart'; import 'package:reboot_cli/src/server.dart'; import 'package:reboot_common/common.dart'; -import 'package:reboot_common/src/util/matchmaker.dart' as matchmaker; late String? username; late bool host; @@ -82,7 +81,7 @@ void main(List args) async { return; } - matchmaker.writeMatchmakingIp(result["matchmaking-address"]); + writeMatchmakingIp(result["matchmaking-address"]); autoRestart = result["auto-restart"]; await startGame(); } \ No newline at end of file diff --git a/cli/lib/src/game.dart b/cli/lib/src/game.dart index f3ad267..b562097 100644 --- a/cli/lib/src/game.dart +++ b/cli/lib/src/game.dart @@ -24,7 +24,7 @@ Future startGame() async { _gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, "")) ..exitCode.then((_) => _onClose()) - ..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose)); + ..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose)); _injectOrShowError("cobalt.dll"); } @@ -52,6 +52,17 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) { stdout.writeln(line); } + handleGameOutput( + line: line, + host: hosting, + onDisplayAttached: () {}, // TODO: Support virtual desktops + onLoggedIn: onLoggedIn, + onMatchEnd: onMatchEnd, + onShutdown: onShutdown, + onTokenError: onTokenError, + onBuildCorrupted: onBuildCorrupted + ); + if (line.contains(kShutdownLine)) { _onClose(); return; diff --git a/common/lib/src/extension/process.dart b/common/lib/src/extension/process.dart index 01dcd17..b0d2376 100644 --- a/common/lib/src/extension/process.dart +++ b/common/lib/src/extension/process.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; extension ProcessExtension on Process { - Stream get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n")); + Stream get stdOutput => this.stdout.expand((event) => utf8.decode(event, allowMalformed: true).split("\n")); - Stream get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n")); + Stream get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n")); } \ No newline at end of file diff --git a/common/lib/src/model/game_instance.dart b/common/lib/src/model/game_instance.dart index 39dcfd8..a4773ed 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/model/game_instance.dart @@ -1,11 +1,14 @@ import 'dart:io'; +import 'package:reboot_common/common.dart'; + class GameInstance { final String versionName; final int gamePid; final int? launcherPid; final int? eacPid; + final List injectedDlls; bool hosting; bool launched; bool movedToVirtualDesktop; @@ -19,7 +22,7 @@ class GameInstance { required this.eacPid, required this.hosting, required this.child - }): tokenError = false, launched = false, movedToVirtualDesktop = false; + }): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = []; void kill() { Process.killPid(gamePid, ProcessSignal.sigabrt); diff --git a/common/lib/src/util/build.dart b/common/lib/src/util/build.dart index b2040a8..e6d0246 100644 --- a/common/lib/src/util/build.dart +++ b/common/lib/src/util/build.dart @@ -145,9 +145,11 @@ Future _extractArchive(Completer stopped, String extension, File '"${tempFile.path}"' ], ); + var completed = false; process.stdOutput.listen((data) { final now = DateTime.now().millisecondsSinceEpoch; if(data.toLowerCase().contains("everything is ok")) { + completed = true; _onProgress(startTime, now, 100, true, options); process?.kill(ProcessSignal.sigabrt); return; @@ -166,6 +168,11 @@ Future _extractArchive(Completer stopped, String extension, File _onError(data, options); } }); + process.exitCode.then((_) { + if(!completed) { + _onError("Corrupted zip archive", options); + } + }); break; case ".rar": final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); @@ -183,10 +190,12 @@ Future _extractArchive(Completer stopped, String extension, File '"${options.destination.path}"' ] ); + var completed = false; process.stdOutput.listen((data) { final now = DateTime.now().millisecondsSinceEpoch; data = data.replaceAll("\r", "").replaceAll("\b", "").trim(); if(data == "All OK") { + completed = true; _onProgress(startTime, now, 100, true, options); process?.kill(ProcessSignal.sigabrt); return; @@ -205,6 +214,11 @@ Future _extractArchive(Completer stopped, String extension, File _onError(data, options); } }); + process.exitCode.then((_) { + if(!completed) { + _onError("Corrupted rar archive", options); + } + }); break; default: throw ArgumentError("Unexpected file extension: $extension}"); diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index e9ca711..1649fc6 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -238,6 +238,31 @@ List createRebootArgs(String username, String password, bool host, bool return args; } +void handleGameOutput({ + required String line, + required bool host, + required void Function() onDisplayAttached, + required void Function() onLoggedIn, + required void Function() onMatchEnd, + required void Function() onShutdown, + required void Function() onTokenError, + required void Function() onBuildCorrupted, +}) { + if (line.contains(kShutdownLine)) { + onShutdown(); + }else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ + onBuildCorrupted(); + }else if(kCannotConnectErrors.any((element) => line.contains(element))){ + onTokenError(); + }else if(kLoggedInLines.every((entry) => line.contains(entry))) { + onLoggedIn(); + }else if(line.contains(kGameFinishedLine) && host) { + onMatchEnd(); + }else if(line.contains(kDisplayInitializedLine) && host) { + onDisplayAttached(); + } +} + String _parseUsername(String username, bool host) { if(host) { return "Player${Random().nextInt(1000)}"; diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index 2d275ef..da2843d 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -260,7 +260,7 @@ "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", "corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", "corruptedDllError": "Cannot inject dll: {error}", - "tokenError": "Cannot log in into Fortnite: authentication error", + "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", "serverNoLongerAvailable": "{owner}'s server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available", diff --git a/gui/lib/main.dart b/gui/lib/main.dart index 31a6fe9..c63b7c5 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -17,7 +17,6 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/build_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/info_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; @@ -25,6 +24,7 @@ import 'package:reboot_launcher/src/dialog/implementation/error.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart'; import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/page/implementation/info_page.dart'; +import 'package:reboot_launcher/src/util/log.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; @@ -33,24 +33,30 @@ import 'package:system_theme/system_theme.dart'; import 'package:url_protocol/url_protocol.dart'; import 'package:version/version.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:win32/win32.dart'; -const double kDefaultWindowWidth = 1536; -const double kDefaultWindowHeight = 1224; +const double kDefaultWindowWidth = 1164; +const double kDefaultWindowHeight = 864; const String kCustomUrlSchema = "Reboot"; Version? appVersion; +bool appWithNoStorage = false; -void main() => runZonedGuarded( - () => _startApp(), - (error, stack) => onError(error, stack, false), - zoneSpecification: ZoneSpecification( - handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) - ) -); +void main() { + log("[APP] Called"); + runZonedGuarded( + () => _startApp(), + (error, stack) => onError(error, stack, false), + zoneSpecification: ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) + ) + ); +} Future _startApp() async { - final errors = []; + final errors = []; try { + log("[APP] Starting application"); final pathError = await _initPath(); if(pathError != null) { errors.add(pathError); @@ -66,10 +72,6 @@ Future _startApp() async { errors.add(notificationsError); } - WidgetsFlutterBinding.ensureInitialized(); - - _initWindow(); - final tilesError = InfoPage.initInfoTiles(); if(tilesError != null) { errors.add(tilesError); @@ -80,22 +82,24 @@ Future _startApp() async { errors.add(versionError); } - final storageError = await _initStorage(); - if(storageError != null) { - errors.add(storageError); - } + final storageErrors = await _initStorage(); + errors.addAll(storageErrors); + + WidgetsFlutterBinding.ensureInitialized(); + + _initWindow(); final urlError = await _initUrlHandler(); if(urlError != null) { errors.add(urlError); } - - _checkGameServer(); }catch(uncaughtError) { errors.add(uncaughtError); } finally{ - runApp(const RebootApplication()); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors)); + log("[APP] Started applications with errors: $errors"); + runApp(RebootApplication( + errors: errors, + )); } } @@ -132,10 +136,6 @@ Future _initPath() async { } } -void _handleErrors(List errors) { - errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); -} - Future _initVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); @@ -146,117 +146,104 @@ Future _initVersion() async { } } -Future _checkGameServer() async { - try { - var backendController = Get.find(); - var address = backendController.gameServerAddress.text; - if(isLocalHost(address)) { - return; - } - - var result = await pingGameServer(address); - if(result) { - return; - } - - var oldOwner = backendController.gameServerOwner.value; - backendController.joinLocalHost(); - WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( - oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner), - severity: InfoBarSeverity.warning, - duration: infoBarLongDuration - )); - }catch(_) { - // Intended behaviour - // Just ignore the error - } -} - Future _initUrlHandler() async { try { registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); - var appLinks = AppLinks(); - var initialUrl = await appLinks.getInitialLink(); - if(initialUrl != null) { - _joinServer(initialUrl); - } - - appLinks.uriLinkStream.listen(_joinServer); return null; }catch(error) { return error; } } -void _joinServer(Uri uri) { - var hostingController = Get.find(); - var backendController = Get.find(); - var uuid = _parseCustomUrl(uri); - var server = hostingController.findServerById(uuid); - if(server != null) { - backendController.joinServer(hostingController.uuid, server); - }else { - showInfoBar( - translations.noServerFound, - duration: infoBarLongDuration, - severity: InfoBarSeverity.error - ); - } -} - -String _parseCustomUrl(Uri uri) => uri.host; - void _initWindow() => doWhenWindowReady(() async { - await SystemTheme.accentColor.load(); - await windowManager.ensureInitialized(); - await Window.initialize(); - var settingsController = Get.find(); - var size = Size(settingsController.width, settingsController.height); - appWindow.size = size; - var offsetX = settingsController.offsetX; - var offsetY = settingsController.offsetY; - if(offsetX != null && offsetY != null){ - appWindow.position = Offset( - offsetX, - offsetY - ); - }else { - appWindow.alignment = Alignment.center; - } + try { + await SystemTheme.accentColor.load(); + await windowManager.ensureInitialized(); + await Window.initialize(); + var settingsController = Get.find(); + var size = Size(settingsController.width, settingsController.height); + appWindow.size = size; + var offsetX = settingsController.offsetX; + var offsetY = settingsController.offsetY; + if(offsetX != null && offsetY != null){ + appWindow.position = Offset( + offsetX, + offsetY + ); + }else { + appWindow.alignment = Alignment.center; + } - if(isWin11) { - await Window.setEffect( - effect: WindowEffect.acrylic, - color: Colors.transparent, - dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark - ); + if(isWin11) { + await Window.setEffect( + effect: WindowEffect.acrylic, + color: Colors.transparent, + dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark + ); + } + }catch(error, stackTrace) { + onError(error, stackTrace, false); + }finally { + appWindow.show(); } - - appWindow.show(); }); -Future _initStorage() async { +Future> _initStorage() async { + final errors = []; try { await GetStorage("game", settingsDirectory.path).initStorage; await GetStorage("backend", settingsDirectory.path).initStorage; await GetStorage("update", settingsDirectory.path).initStorage; await GetStorage("settings", settingsDirectory.path).initStorage; await GetStorage("hosting", settingsDirectory.path).initStorage; - Get.put(GameController()); - Get.put(BackendController()); - Get.put(BuildController()); - Get.put(SettingsController()); - Get.put(HostingController()); - Get.put(InfoController()); - Get.put(UpdateController()); - return null; }catch(error) { - return error; + appWithNoStorage = true; + errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); } + + try { + Get.put(GameController()); + }catch(error) { + errors.add(error); + } + + try { + Get.put(BackendController()); + }catch(error) { + errors.add(error); + } + + try { + Get.put(BuildController()); + }catch(error) { + errors.add(error); + } + + try { + Get.put(HostingController()); + }catch(error) { + errors.add(error); + } + + try { + Get.put(UpdateController()); + }catch(error) { + errors.add(error); + } + + try { + Get.put(SettingsController()); + }catch(error) { + errors.add(error); + } + + + return errors; } class RebootApplication extends StatefulWidget { - const RebootApplication({Key? key}) : super(key: key); + final List errors; + const RebootApplication({Key? key, required this.errors}) : super(key: key); @override State createState() => _RebootApplicationState(); @@ -265,6 +252,16 @@ class RebootApplication extends StatefulWidget { class _RebootApplicationState extends State { final SettingsController _settingsController = Get.find(); + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors)); + } + + void _handleErrors(List errors) { + errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); + } + @override Widget build(BuildContext context) => Obx(() => FluentApp( locale: Locale(_settingsController.language.value), diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index acd93a3..0b78831 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -5,9 +5,10 @@ import 'package:fluent_ui/fluent_ui.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'; class BackendController extends GetxController { - late final GetStorage storage; + late final GetStorage? storage; late final TextEditingController host; late final TextEditingController port; late final Rx type; @@ -21,13 +22,13 @@ class BackendController extends GetxController { HttpServer? remoteServer; BackendController() { - storage = GetStorage("backend"); + storage = appWithNoStorage ? null : GetStorage("backend"); 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; } @@ -36,13 +37,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)); - gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? "127.0.0.1"); + storage?.write("${type.value.name}_port", port.text)); + detached = RxBool(storage?.read("detached") ?? false); + detached.listen((value) => storage?.write("detached", value)); + gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1"); var lastValue = gameServerAddress.text; writeMatchmakingIp(lastValue); gameServerAddress.addListener(() { @@ -53,7 +54,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) { @@ -62,15 +63,15 @@ class BackendController extends GetxController { } }); gameServerAddressFocusNode = FocusNode(); - gameServerOwner = RxnString(storage.read("game_server_owner")); - gameServerOwner.listen((value) => storage.write("game_server_owner", value)); + gameServerOwner = RxnString(storage?.read("game_server_owner")); + gameServerOwner.listen((value) => storage?.write("game_server_owner", value)); } 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 : ""; @@ -79,7 +80,7 @@ class BackendController extends GetxController { } 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; } @@ -92,7 +93,7 @@ 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* { try { diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index 283f9c5..be9423a 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -9,10 +9,12 @@ import 'package:get_storage/get_storage.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/util/keyboard.dart'; +import '../../main.dart'; + class GameController extends GetxController { static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); - late final GetStorage _storage; + late final GetStorage? _storage; late final TextEditingController username; late final TextEditingController password; late final TextEditingController customLaunchArgs; @@ -23,38 +25,37 @@ class GameController extends GetxController { late final Rx consoleKey; GameController() { - _storage = GetStorage("game"); - Iterable decodedVersionsJson = jsonDecode( - _storage.read("versions") ?? "[]"); - var decodedVersions = decodedVersionsJson + _storage = appWithNoStorage ? null : GetStorage("game"); + Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); + final decodedVersions = decodedVersionsJson .map((entry) => FortniteVersion.fromJson(entry)) .toList(); versions = Rx(decodedVersions); versions.listen((data) => _saveVersions()); - var decodedSelectedVersionName = _storage.read("version"); - var decodedSelectedVersion = decodedVersions.firstWhereOrNull(( + final decodedSelectedVersionName = _storage?.read("version"); + final decodedSelectedVersion = decodedVersions.firstWhereOrNull(( element) => element.name == decodedSelectedVersionName); _selectedVersion = Rxn(decodedSelectedVersion); username = TextEditingController( - text: _storage.read("username") ?? kDefaultPlayerName); - username.addListener(() => _storage.write("username", username.text)); - password = TextEditingController(text: _storage.read("password") ?? ""); - password.addListener(() => _storage.write("password", password.text)); - customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? ""); + text: _storage?.read("username") ?? kDefaultPlayerName); + username.addListener(() => _storage?.write("username", username.text)); + 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)); + _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); + _storage?.write("console_key", newValue.usbHidUsage); _writeConsoleKey(newValue); }); } PhysicalKeyboardKey _readConsoleKey() { - final consoleKeyValue = _storage.read("console_key"); + final consoleKeyValue = _storage?.read("console_key"); if(consoleKeyValue == null) { return _kDefaultConsoleKey; } @@ -113,7 +114,7 @@ class GameController extends GetxController { Future _saveVersions() async { var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList()); - await _storage.write("versions", serialized); + await _storage?.write("versions", serialized); } bool get hasVersions => versions.value.isNotEmpty; @@ -124,7 +125,7 @@ class GameController extends GetxController { set selectedVersion(FortniteVersion? version) { _selectedVersion.value = version; - _storage.write("version", version?.name); + _storage?.write("version", version?.name); } void updateVersion(FortniteVersion version, Function(FortniteVersion) function) { diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index 3d48b9e..5687d24 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -2,11 +2,12 @@ import 'package:fluent_ui/fluent_ui.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:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; class HostingController extends GetxController { - late final GetStorage _storage; + late final GetStorage? _storage; late final String uuid; late final TextEditingController name; late final TextEditingController description; @@ -22,23 +23,23 @@ class HostingController extends GetxController { late final Rxn>> servers; HostingController() { - _storage = GetStorage("hosting"); - uuid = _storage.read("uuid") ?? const Uuid().v4(); - _storage.write("uuid", uuid); - name = TextEditingController(text: _storage.read("name")); - name.addListener(() => _storage.write("name", name.text)); - description = TextEditingController(text: _storage.read("description")); - description.addListener(() => _storage.write("description", description.text)); - password = TextEditingController(text: _storage.read("password") ?? ""); - password.addListener(() => _storage.write("password", password.text)); - discoverable = RxBool(_storage.read("discoverable") ?? false); - discoverable.listen((value) => _storage.write("discoverable", value)); - headless = RxBool(_storage.read("headless") ?? true); - headless.listen((value) => _storage.write("headless", value)); - virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true); - virtualDesktop.listen((value) => _storage.write("virtual_desktop", value)); - autoRestart = RxBool(_storage.read("auto_restart") ?? true); - autoRestart.listen((value) => _storage.write("auto_restart", value)); + _storage = appWithNoStorage ? null : GetStorage("hosting"); + uuid = _storage?.read("uuid") ?? const Uuid().v4(); + _storage?.write("uuid", uuid); + name = TextEditingController(text: _storage?.read("name")); + name.addListener(() => _storage?.write("name", name.text)); + description = TextEditingController(text: _storage?.read("description")); + description.addListener(() => _storage?.write("description", description.text)); + password = TextEditingController(text: _storage?.read("password") ?? ""); + password.addListener(() => _storage?.write("password", password.text)); + discoverable = RxBool(_storage?.read("discoverable") ?? false); + discoverable.listen((value) => _storage?.write("discoverable", value)); + headless = RxBool(_storage?.read("headless") ?? true); + headless.listen((value) => _storage?.write("headless", value)); + virtualDesktop = RxBool(_storage?.read("virtual_desktop") ?? true); + virtualDesktop.listen((value) => _storage?.write("virtual_desktop", value)); + autoRestart = RxBool(_storage?.read("auto_restart") ?? true); + autoRestart.listen((value) => _storage?.write("auto_restart", value)); started = RxBool(false); published = RxBool(false); showPassword = RxBool(false); diff --git a/gui/lib/src/controller/info_controller.dart b/gui/lib/src/controller/info_controller.dart deleted file mode 100644 index 83198e9..0000000 --- a/gui/lib/src/controller/info_controller.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:get/get.dart'; - -class InfoController extends GetxController { - List? links; - Map linksData; - - InfoController() : linksData = {}; -} diff --git a/gui/lib/src/controller/update_controller.dart b/gui/lib/src/controller/update_controller.dart index dc75387..781c79a 100644 --- a/gui/lib/src/controller/update_controller.dart +++ b/gui/lib/src/controller/update_controller.dart @@ -11,7 +11,7 @@ import 'package:version/version.dart'; import 'package:yaml/yaml.dart'; class UpdateController { - late final GetStorage _storage; + late final GetStorage? _storage; late final RxnInt timestamp; late final Rx status; late final Rx timer; @@ -21,17 +21,17 @@ class UpdateController { Future? _updater; UpdateController() { - _storage = GetStorage("update"); - timestamp = RxnInt(_storage.read("ts")); - timestamp.listen((value) => _storage.write("ts", value)); - var timerIndex = _storage.read("timer"); + _storage = appWithNoStorage ? null : GetStorage("update"); + timestamp = RxnInt(_storage?.read("ts")); + timestamp.listen((value) => _storage?.write("ts", value)); + var 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)); + 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)); + customGameServer = RxBool(_storage?.read("custom_game_server") ?? false); + customGameServer.listen((value) => _storage?.write("custom_game_server", value)); } Future notifyLauncherUpdate() async { @@ -65,17 +65,17 @@ class UpdateController { ); } - Future updateReboot([bool force = false]) async { + Future updateReboot({bool force = false, bool silent = false}) async { if(_updater != null) { return await _updater; } - final result = _updateReboot(force); + final result = _updateReboot(force, silent); _updater = result; return await result; } - Future _updateReboot([bool force = false]) async { + Future _updateReboot(bool force, bool silent) async { try { if(customGameServer.value) { status.value = UpdateStatus.success; @@ -92,34 +92,44 @@ class UpdateController { return; } - infoBarEntry = showInfoBar( - translations.downloadingDll("reboot"), - loading: true, - duration: null - ); + if(!silent) { + infoBarEntry = showInfoBar( + translations.downloadingDll("reboot"), + loading: true, + duration: null + ); + } timestamp.value = await downloadRebootDll(url.text); status.value = UpdateStatus.success; infoBarEntry?.close(); - infoBarEntry = showInfoBar( - translations.downloadDllSuccess("reboot"), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); + if(!silent) { + infoBarEntry = showInfoBar( + translations.downloadDllSuccess("reboot"), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } }catch(message) { - infoBarEntry?.close(); - var error = message.toString(); - error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - status.value = UpdateStatus.error; - showInfoBar( - translations.downloadDllError("reboot.dll", error.toString()), - duration: infoBarLongDuration, - severity: InfoBarSeverity.error, - action: Button( - onPressed: () => updateReboot(true), - child: Text(translations.downloadDllRetry), - ) - ); + if(!silent) { + infoBarEntry?.close(); + var error = message.toString(); + error = + error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + status.value = UpdateStatus.error; + showInfoBar( + translations.downloadDllError("reboot.dll", error.toString()), + duration: infoBarLongDuration, + severity: InfoBarSeverity.error, + action: Button( + onPressed: () => updateReboot( + force: true, + silent: silent + ), + child: Text(translations.downloadDllRetry), + ) + ); + } }finally { _updater = null; } diff --git a/gui/lib/src/dialog/implementation/error.dart b/gui/lib/src/dialog/implementation/error.dart index 358afcd..b9eb94f 100644 --- a/gui/lib/src/dialog/implementation/error.dart +++ b/gui/lib/src/dialog/implementation/error.dart @@ -4,14 +4,14 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import '../../util/log.dart'; + String? lastError; void onError(Object exception, StackTrace? stackTrace, bool framework) { - if(!kDebugMode) { - return; - } - + log("[ERROR] $exception"); + log("[STACKTRACE] $stackTrace"); if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){ return; } diff --git a/gui/lib/src/page/implementation/home_page.dart b/gui/lib/src/page/implementation/home_page.dart index 345f57d..6927875 100644 --- a/gui/lib/src/page/implementation/home_page.dart +++ b/gui/lib/src/page/implementation/home_page.dart @@ -2,20 +2,26 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; +import 'package:app_links/app_links.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; 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/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; +import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/implementation/dll.dart'; +import 'package:reboot_launcher/src/dialog/implementation/server.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart'; import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/dll.dart'; +import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/info_bar_area.dart'; @@ -33,6 +39,8 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WindowListener, AutomaticKeepAliveClientMixin { static const double _kDefaultPadding = 12.0; + final BackendController _backendController = Get.find(); + final HostingController _hostingController = Get.find(); final SettingsController _settingsController = Get.find(); final UpdateController _updateController = Get.find(); final GlobalKey _searchKey = GlobalKey(); @@ -45,9 +53,62 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void initState() { - windowManager.addListener(this); - WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdates()); super.initState(); + windowManager.addListener(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkUpdates(); + _initAppLink(); + _checkGameServer(); + }); + } + + void _initAppLink() async { + final appLinks = AppLinks(); + final initialUrl = await appLinks.getInitialLink(); + if(initialUrl != null) { + _joinServer(initialUrl); + } + + appLinks.uriLinkStream.listen(_joinServer); + } + + void _joinServer(Uri uri) { + final uuid = uri.host; + final server = _hostingController.findServerById(uuid); + if(server != null) { + _backendController.joinServer(_hostingController.uuid, server); + }else { + showInfoBar( + translations.noServerFound, + duration: infoBarLongDuration, + severity: InfoBarSeverity.error + ); + } + } + + Future _checkGameServer() async { + try { + final address = _backendController.gameServerAddress.text; + if(isLocalHost(address)) { + return; + } + + var result = await pingGameServer(address); + if(result) { + return; + } + + var oldOwner = _backendController.gameServerOwner.value; + _backendController.joinLocalHost(); + WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar( + oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner), + severity: InfoBarSeverity.warning, + duration: infoBarLongDuration + )); + }catch(_) { + // Intended behaviour + // Just ignore the error + } } void _checkUpdates() { @@ -58,7 +119,10 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA } for(final injectable in InjectableDll.values) { - downloadCriticalDllInteractive("${injectable.name}.dll"); + downloadCriticalDllInteractive( + injectable.path, + silent: true + ); } watchDlls().listen((filePath) => showDllDeletedDialog(() { @@ -157,54 +221,55 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA super.build(context); _settingsController.language.value; loadTranslations(context); - return Obx(() => NavigationPaneTheme( - data: NavigationPaneThemeData( - backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), - ), - child: NavigationView( - paneBodyBuilder: (pane, body) => _PaneBody( - padding: _kDefaultPadding, - controller: pagesController, - body: body - ), - appBar: NavigationAppBar( - height: 32, - title: _draggableArea, - actions: WindowTitleBar(focused: _focused()), - leading: _backButton, - automaticallyImplyLeading: false, - ), - pane: NavigationPane( - selected: pageIndex.value, - onChanged: (index) { - final lastPageIndex = pageIndex.value; - if(lastPageIndex != index) { - pageIndex.value = index; - }else if(pageStack.isNotEmpty) { - Navigator.of(pageKey.currentContext!).pop(); - final element = pageStack.removeLast(); - appStack.remove(element); - pagesController.add(null); - } - }, - menuButton: const SizedBox(), - displayMode: PaneDisplayMode.open, - items: _items, - customPane: _CustomPane(_settingsController), - header: const ProfileWidget(), - autoSuggestBox: _autoSuggestBox, - indicator: const StickyNavigationIndicator( - duration: Duration(milliseconds: 500), - curve: Curves.easeOut, - indicatorSize: 3.25 - ) - ), - contentShape: const RoundedRectangleBorder(), - onOpenSearch: () => _searchFocusNode.requestFocus(), - transitionBuilder: (child, animation) => child - ) - ), - ); + return Obx(() { + return NavigationPaneTheme( + data: NavigationPaneThemeData( + backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), + ), + child: NavigationView( + paneBodyBuilder: (pane, body) => _PaneBody( + padding: _kDefaultPadding, + controller: pagesController, + body: body + ), + appBar: NavigationAppBar( + height: 32, + title: _draggableArea, + actions: WindowTitleBar(focused: _focused()), + leading: _backButton, + automaticallyImplyLeading: false, + ), + pane: NavigationPane( + selected: pageIndex.value, + onChanged: (index) { + final lastPageIndex = pageIndex.value; + if(lastPageIndex != index) { + pageIndex.value = index; + }else if(pageStack.isNotEmpty) { + Navigator.of(pageKey.currentContext!).pop(); + final element = pageStack.removeLast(); + appStack.remove(element); + pagesController.add(null); + } + }, + menuButton: const SizedBox(), + displayMode: PaneDisplayMode.open, + items: _items, + customPane: _CustomPane(_settingsController), + header: const ProfileWidget(), + autoSuggestBox: _autoSuggestBox, + indicator: const StickyNavigationIndicator( + duration: Duration(milliseconds: 500), + curve: Curves.easeOut, + indicatorSize: 3.25 + ) + ), + contentShape: const RoundedRectangleBorder(), + onOpenSearch: () => _searchFocusNode.requestFocus(), + transitionBuilder: (child, animation) => child + ) + ); + }); } Widget get _backButton => StreamBuilder( diff --git a/gui/lib/src/page/implementation/info_page.dart b/gui/lib/src/page/implementation/info_page.dart index ba4ff3d..df3ae6c 100644 --- a/gui/lib/src/page/implementation/info_page.dart +++ b/gui/lib/src/page/implementation/info_page.dart @@ -69,9 +69,11 @@ class InfoPage extends RebootPage { class _InfoPageState extends RebootPageState { final SettingsController _settingsController = Get.find(); RxInt _counter = RxInt(kDebugMode ? 0 : 180); + late bool _showButton; @override void initState() { + _showButton = _settingsController.firstRun.value; if(_settingsController.firstRun.value) { Timer.periodic(const Duration(seconds: 1), (timer) { if (_counter.value <= 0) { @@ -89,24 +91,29 @@ class _InfoPageState extends RebootPageState { List get settings => InfoPage._infoTiles; @override - Widget? get button => Obx(() { - if(!_settingsController.firstRun.value) { + Widget? get button { + if(!_showButton) { return const SizedBox.shrink(); } - final totalSecondsLeft = _counter.value; - final minutesLeft = totalSecondsLeft ~/ 60; - final secondsLeft = totalSecondsLeft % 60; - return SizedBox( - width: double.infinity, - height: 48, - child: Button( - onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null, - child: Text( - totalSecondsLeft <= 0 ? "I have read the instructions" - : "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}" - ), - ) - ); - }); + return Obx(() { + final totalSecondsLeft = _counter.value; + final minutesLeft = totalSecondsLeft ~/ 60; + final secondsLeft = totalSecondsLeft % 60; + return SizedBox( + width: double.infinity, + height: 48, + child: Button( + onPressed: totalSecondsLeft <= 0 ? () { + _showButton = false; + pageIndex.value = RebootPageType.play.index; + } : null, + child: Text( + totalSecondsLeft <= 0 ? "I have read the instructions" + : "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}" + ), + ) + ); + }); + } } \ No newline at end of file diff --git a/gui/lib/src/page/implementation/server_host_page.dart b/gui/lib/src/page/implementation/server_host_page.dart index efc5734..e3c32b2 100644 --- a/gui/lib/src/page/implementation/server_host_page.dart +++ b/gui/lib/src/page/implementation/server_host_page.dart @@ -11,6 +11,7 @@ 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'; import 'package:reboot_launcher/src/controller/update_controller.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/implementation/data.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart'; @@ -65,8 +66,10 @@ class _HostingPageState extends RebootPageState { } @override - Widget get button => const LaunchButton( - host: true + Widget get button => LaunchButton( + host: true, + startLabel: translations.startHosting, + stopLabel: translations.stopHosting ); @override @@ -194,6 +197,8 @@ class _HostingPageState extends RebootPageState { title: Text(translations.settingsServerTypeName), subtitle: Text(translations.settingsServerTypeDescription), content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), items: { false: translations.settingsServerTypeEmbeddedName, @@ -209,7 +214,9 @@ class _HostingPageState extends RebootPageState { _updateController.customGameServer.value = entry.key; _updateController.infoBarEntry?.close(); if(!entry.key) { - _updateController.updateReboot(true); + _updateController.updateReboot( + force: true + ); } } )).toList() @@ -256,13 +263,17 @@ class _HostingPageState extends RebootPageState { title: Text(translations.settingsServerTimerName), subtitle: Text(translations.settingsServerTimerSubtitle), content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, leading: Text(_updateController.timer.value.text), items: UpdateTimer.values.map((entry) => MenuFlyoutItem( text: Text(entry.text), onPressed: () { _updateController.timer.value = entry; _updateController.infoBarEntry?.close(); - _updateController.updateReboot(true); + _updateController.updateReboot( + force: true + ); } )).toList() )) diff --git a/gui/lib/src/page/implementation/settings_page.dart b/gui/lib/src/page/implementation/settings_page.dart index 8bc2284..7dd05e8 100644 --- a/gui/lib/src/page/implementation/settings_page.dart +++ b/gui/lib/src/page/implementation/settings_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/implementation/data.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; @@ -46,6 +47,8 @@ class _SettingsPageState extends RebootPageState { title: Text(translations.settingsUtilsLanguageName), subtitle: Text(translations.settingsUtilsLanguageDescription), content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, leading: Text(_getLocaleName(_settingsController.language.value)), items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( text: Text(_getLocaleName(locale.languageCode)), @@ -60,6 +63,8 @@ class _SettingsPageState extends RebootPageState { title: Text(translations.settingsUtilsThemeName), subtitle: Text(translations.settingsUtilsThemeDescription), content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, leading: Text(_settingsController.themeMode.value.title), items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( text: Text(themeMode.title), diff --git a/gui/lib/src/util/dll.dart b/gui/lib/src/util/dll.dart index 4490d1c..06cdf70 100644 --- a/gui/lib/src/util/dll.dart +++ b/gui/lib/src/util/dll.dart @@ -13,59 +13,71 @@ import 'package:reboot_launcher/src/util/translations.dart'; final UpdateController _updateController = Get.find(); final Map> _operations = {}; -Future downloadCriticalDllInteractive(String filePath) { +Future downloadCriticalDllInteractive(String filePath, {bool silent = false}) { final old = _operations[filePath]; if(old != null) { return old; } - final newRun = _downloadCriticalDllInteractive(filePath); + final newRun = _downloadCriticalDllInteractive(filePath, silent); _operations[filePath] = newRun; return newRun; } -Future _downloadCriticalDllInteractive(String filePath) async { +Future _downloadCriticalDllInteractive(String filePath, bool silent) async { final fileName = path.basename(filePath).toLowerCase(); InfoBarEntry? entry; try { if (fileName == "reboot.dll") { - await _updateController.updateReboot(true); + await _updateController.updateReboot( + silent: silent + ); + return; + } + + if(File(filePath).existsSync()) { return; } final fileNameWithoutExtension = path.basenameWithoutExtension(filePath); - entry = showInfoBar( - translations.downloadingDll(fileNameWithoutExtension), - loading: true, - duration: null - ); + if(!silent) { + entry = showInfoBar( + translations.downloadingDll(fileNameWithoutExtension), + loading: true, + duration: null + ); + } await downloadCriticalDll(fileName, filePath); - entry.close(); - entry = await showInfoBar( - translations.downloadDllSuccess(fileNameWithoutExtension), - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - }catch(message) { entry?.close(); - var error = message.toString(); - error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; - error = error.toLowerCase(); - final completer = Completer(); - await showInfoBar( - 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; + if(!silent) { + entry = await showInfoBar( + translations.downloadDllSuccess(fileNameWithoutExtension), + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + } + }catch(message) { + if(!silent) { + entry?.close(); + var error = message.toString(); + error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; + error = error.toLowerCase(); + final completer = Completer(); + await showInfoBar( + 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; + } }finally { _operations.remove(fileName); } diff --git a/gui/lib/src/util/log.dart b/gui/lib/src/util/log.dart index 2f7a3be..8ddffc4 100644 --- a/gui/lib/src/util/log.dart +++ b/gui/lib/src/util/log.dart @@ -17,7 +17,13 @@ File _createLoggingFile() { } void log(String message) async { - await _semaphore.acquire(); - await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true); - _semaphore.release(); + try { + await _semaphore.acquire(); + print(message); + await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true); + }catch(error) { + print(error); + }finally { + _semaphore.release(); + } } \ No newline at end of file diff --git a/gui/lib/src/util/os.dart b/gui/lib/src/util/os.dart index ea8d355..6a71d36 100644 --- a/gui/lib/src/util/os.dart +++ b/gui/lib/src/util/os.dart @@ -305,10 +305,25 @@ final class Win32Process extends Struct { external int HWndLength; external Pointer HWnd; + + external Pointer excluded; } int _filter(int HWnd, int lParam) { final structure = Pointer.fromAddress(lParam).cast(); + if(structure.ref.excluded != nullptr) { + final excludedWindowName = structure.ref.excluded.toDartString(); + final windowNameLength = GetWindowTextLength(HWnd); + if(windowNameLength > 0) { + final windowNamePointer = calloc(windowNameLength + 1).cast(); + GetWindowText(HWnd, windowNamePointer, windowNameLength); + final windowName = windowNamePointer.toDartString(length: windowNameLength); + if(windowName.toLowerCase().contains(excludedWindowName.toLowerCase())) { + return TRUE; + } + } + } + final pidPointer = calloc(); GetWindowThreadProcessId(HWnd, pidPointer); final pid = pidPointer.value; @@ -330,9 +345,13 @@ int _filter(int HWnd, int lParam) { return TRUE; } -List _getHWnds(int pid) { +List _getHWnds(int pid, String? excludedWindowName) { final result = calloc(); result.ref.pid = pid; + if(excludedWindowName != null) { + result.ref.excluded = excludedWindowName.toNativeUtf16(); + } + EnumWindows(Pointer.fromFunction(_filter, TRUE), result.address); final length = result.ref.HWndLength; final HWndsPointer = result.ref.HWnd; @@ -400,24 +419,26 @@ class VirtualDesktopManager { List getDesktops() => windowManager.getDesktops(); - Future moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1)}) async { - final hWNDs = _getHWnds(pid); - if(hWNDs.isEmpty) { - await Future.delayed(pollTime); - await moveWindowToDesktop(pid, desktop, pollTime: pollTime); - return; - } - - for(final hWND in hWNDs) { + Future moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1), int remainingPolls = 10, String? excludedWindowName}) async { + for(final hWND in _getHWnds(pid, excludedWindowName)) { final window = applicationViewCollection.getViewForHWnd(hWND); if(window != null) { windowManager.moveWindowToDesktop(window, desktop); - return; + return true; } } + if(remainingPolls <= 0) { + return false; + } + await Future.delayed(pollTime); - await moveWindowToDesktop(pid, desktop, pollTime: pollTime); + return await moveWindowToDesktop( + pid, + desktop, + pollTime: pollTime, + remainingPolls: remainingPolls - 1 + ); } IVirtualDesktop createDesktop() => windowManager.createDesktop(); diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 9b1275f..e7f3e01 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:async/async.dart'; import 'package:dart_ipify/dart_ipify.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:path/path.dart'; @@ -12,7 +13,6 @@ import 'package:reboot_launcher/src/controller/backend_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'; -import 'package:reboot_launcher/src/controller/update_controller.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; @@ -27,10 +27,10 @@ import 'package:reboot_launcher/src/util/translations.dart'; class LaunchButton extends StatefulWidget { final bool host; - final String? startLabel; - final String? stopLabel; + final String startLabel; + final String stopLabel; - const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel}) : super(key: key); + const LaunchButton({Key? key, required this.host, required this.startLabel, required this.stopLabel}) : super(key: key); @override State createState() => _LaunchButtonState(); @@ -43,7 +43,6 @@ class _LaunchButtonState extends State { final HostingController _hostingController = Get.find(); final BackendController _backendController = Get.find(); final SettingsController _settingsController = Get.find(); - final UpdateController _updateController = Get.find(); InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameServerInfoBar; CancelableOperation? _operation; @@ -60,52 +59,42 @@ class _LaunchButtonState extends State { onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()), child: Align( alignment: Alignment.center, - child: Text(_hasStarted ? _stopMessage : _startMessage) + child: Text((widget.host ? _hostingController.started() : _gameController.started()) ? widget.stopLabel : widget.startLabel) ) ), )), ), ); - bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started(); - void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started; - String get _startMessage => widget.startLabel ?? (widget.host ? translations.startHosting : translations.startGame); - - String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame); - - Future _toggle({bool forceGUI = false}) async { - log("[${widget.host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)"); - if (_hasStarted) { - log("[${widget.host ? 'HOST' : 'GAME'}] User asked to close the current instance"); + Future _toggle({bool? host, bool forceGUI = false}) async { + host ??= widget.host; + log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)"); + if (host ? _hostingController.started() : _gameController.started()) { + log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance"); _onStop( reason: _StopReason.normal ); return; } - if(_operation != null) { - log("[${widget.host ? 'HOST' : 'GAME'}] Already started, ignoring user action"); - return; - } - final version = _gameController.selectedVersion; - log("[${widget.host ? 'HOST' : 'GAME'}] Version data: $version"); + log("[${host ? 'HOST' : 'GAME'}] Version data: $version"); if(version == null){ - log("[${widget.host ? 'HOST' : 'GAME'}] No version selected"); + log("[${host ? 'HOST' : 'GAME'}] No version selected"); _onStop( reason: _StopReason.missingVersionError ); return; } - log("[${widget.host ? 'HOST' : 'GAME'}] Setting started..."); - _setStarted(widget.host, true); - log("[${widget.host ? 'HOST' : 'GAME'}] Set started"); - log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); + log("[${host ? 'HOST' : 'GAME'}] Setting started..."); + _setStarted(host, true); + log("[${host ? 'HOST' : 'GAME'}] Set started"); + log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); for (final injectable in InjectableDll.values) { - if(await _getDllFileOrStop(injectable, widget.host) == null) { + if(await _getDllFileOrStop(injectable, host) == null) { return; } } @@ -113,7 +102,7 @@ class _LaunchButtonState extends State { try { final executable = version.gameExecutable; if(executable == null){ - log("[${widget.host ? 'HOST' : 'GAME'}] No executable found"); + log("[${host ? 'HOST' : 'GAME'}] No executable found"); _onStop( reason: _StopReason.missingExecutableError, error: version.location.path @@ -121,27 +110,27 @@ class _LaunchButtonState extends State { return; } - log("[${widget.host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); + log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); final backendResult = _backendController.started() || await _backendController.toggleInteractive(); if(!backendResult){ - log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start backend"); + log("[${host ? 'HOST' : 'GAME'}] Cannot start backend"); _onStop( reason: _StopReason.backendError ); return; } - log("[${widget.host ? 'HOST' : 'GAME'}] Backend works"); + log("[${host ? 'HOST' : 'GAME'}] Backend works"); final headless = !forceGUI && _hostingController.headless.value; final virtualDesktop = _hostingController.virtualDesktop.value; - log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)"); - final linkedHostingInstance = await _startMatchMakingServer(version, headless, virtualDesktop, false); - log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); - await _startGameProcesses(version, widget.host, headless, virtualDesktop, linkedHostingInstance); - if(!widget.host) { + log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)"); + final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, virtualDesktop, false); + log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); + await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance); + if(!host) { _showLaunchingGameClientWidget(); } - if(linkedHostingInstance != null || widget.host){ + if(linkedHostingInstance != null || host){ _showLaunchingGameServerWidget(); } } catch (exception, stackTrace) { @@ -153,34 +142,34 @@ class _LaunchButtonState extends State { } } - Future _startMatchMakingServer(FortniteVersion version, bool headless, bool virtualDesktop, bool forceLinkedHosting) async { - log("[${widget.host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically..."); - if(widget.host){ - log("[${widget.host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary"); + Future _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool virtualDesktop, bool forceLinkedHosting) async { + log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically..."); + if(host){ + log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary"); return null; } if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) { - log("[${widget.host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server"); + log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server"); return null; } if(_hostingController.started()){ - log("[${widget.host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server"); + log("[${host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server"); return null; } final response = forceLinkedHosting || await _askForAutomaticGameServer(); if(!response) { - log("[${widget.host ? 'HOST' : 'GAME'}] The user disabled the automatic server"); + log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server"); return null; } - log("[${widget.host ? 'HOST' : 'GAME'}] Starting implicit game server..."); + log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server..."); final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null); - log("[${widget.host ? 'HOST' : 'GAME'}] Started implicit game server..."); + log("[${host ? 'HOST' : 'GAME'}] Started implicit game server..."); _setStarted(true, true); - log("[${widget.host ? 'HOST' : 'GAME'}] Set implicit game server as started"); + log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started"); return instance; } @@ -245,11 +234,6 @@ class _LaunchButtonState extends State { } Future _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async { - if(!_hasStarted) { - log("[${host ? 'HOST' : 'GAME'}] Discarding start game process request as the state is no longer started"); - return null; - } - log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); final gameArgs = createRebootArgs( _gameController.username.text, @@ -265,40 +249,52 @@ class _LaunchButtonState extends State { wrapProcess: false, name: "${version.name}-${host ? 'HOST' : 'GAME'}" ); - gameProcess.stdOutput.listen((line) => _onGameOutput(line, version, host, virtualDesktop, false)); - gameProcess.stdError.listen((line) => _onGameOutput(line, version, host, virtualDesktop, true)); - watchProcess(gameProcess.pid).then((_) async { + 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), + onLoggedIn: () =>_onLoggedIn(host), + onMatchEnd: () => _onMatchEnd(version, virtualDesktop), + onDisplayAttached: () => _onDisplayAttached(headless, virtualDesktop, version) + ); + } + gameProcess.stdOutput.listen((line) => onGameOutput(line, false)); + gameProcess.stdError.listen((line) => onGameOutput(line, true)); + gameProcess.exitCode.then((_) async { final instance = host ? _hostingController.instance.value : _gameController.instance.value; if(instance == null) { + log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running"); return; } - if(!host || !headless || instance.launched) { - _onStop(reason: _StopReason.exitCode); + if(!host || instance.launched) { + log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): stop signal"); + _onStop( + reason: _StopReason.exitCode, + host: host + ); return; } - await _restartGameServer(version, virtualDesktop, _StopReason.exitCode); + log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): restart signal"); + instance.launched = true; + await _onStop( + reason: _StopReason.exitCode, + host: true + ); + await _toggle( + forceGUI: true, + host: true + ); }); return gameProcess.pid; } - Future _restartGameServer(FortniteVersion version, bool virtualDesktop, _StopReason reason) async { - if (widget.host) { - await _onStop(reason: reason); - _toggle(forceGUI: true); - } else { - await _onStop(reason: reason, host: true); - final linkedHostingInstance = - await _startMatchMakingServer(version, false, virtualDesktop, true); - _gameController.instance.value?.child = linkedHostingInstance; - if (linkedHostingInstance != null) { - _setStarted(true, true); - _showLaunchingGameServerWidget(); - } - } - } - Future _createPausedProcess(FortniteVersion version, File? file) async { if (file == null) { return null; @@ -314,58 +310,8 @@ class _LaunchButtonState extends State { return pid; } - void _onGameOutput(String line, FortniteVersion version, bool host, bool virtualDesktop, bool error) async { - if (line.contains(kShutdownLine)) { - _onStop( - reason: _StopReason.normal - ); - }else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ - _onStop( - reason: _StopReason.corruptedVersionError - ); - }else if(kCannotConnectErrors.any((element) => line.contains(element))){ - _onStop( - reason: _StopReason.tokenError - ); - }else if(kLoggedInLines.every((entry) => line.contains(entry))) { - final instance = host ? _hostingController.instance.value : _gameController.instance.value; - if(instance != null && !instance.launched) { - instance.launched = true; - instance.tokenError = false; - await _injectOrShowError(InjectableDll.memory, host); - if(!host){ - await _injectOrShowError(InjectableDll.console, host); - _onGameClientInjected(); - }else { - final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); - if(gameServerPort != null) { - await killProcessByPort(gameServerPort); - } - await _injectOrShowError(InjectableDll.reboot, host); - _onGameServerInjected(); - } - } - }else if(line.contains(kGameFinishedLine) && host) { - if(_hostingController.autoRestart.value) { - final notification = LocalNotification( - title: translations.gameServerEnd, - body: translations.gameServerRestart(_kRebootDelay.inSeconds), - ); - notification.show(); - Future.delayed(_kRebootDelay).then((_) { - _restartGameServer(version, virtualDesktop, _StopReason.normal); - }); - }else { - final notification = LocalNotification( - title: translations.gameServerEnd, - body: translations.gameServerShutdown(_kRebootDelay.inSeconds) - ); - notification.show(); - Future.delayed(_kRebootDelay).then((_) { - _onStop(reason: _StopReason.normal, host: true); - }); - } - }else if(line.contains(kDisplayInitializedLine) && host && virtualDesktop) { + Future _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async { + if(!headless && virtualDesktop) { final hostingInstance = _hostingController.instance.value; if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) { hostingInstance.movedToVirtualDesktop = true; @@ -373,10 +319,18 @@ class _LaunchButtonState extends State { final windowManager = VirtualDesktopManager.getInstance(); _virtualDesktop = windowManager.createDesktop(); windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)"); + var success = false; try { - await windowManager.moveWindowToDesktop(hostingInstance.gamePid, _virtualDesktop!); + success = await windowManager.moveWindowToDesktop( + hostingInstance.gamePid, + _virtualDesktop!, + excludedWindowName: "Reboot" + ); }catch(error) { log("[VIRTUAL_DESKTOP] $error"); + success = false; + } + if(!success) { try { windowManager.removeDesktop(_virtualDesktop!); }catch(error) { @@ -392,6 +346,63 @@ class _LaunchButtonState extends State { } } + void _onMatchEnd(FortniteVersion version, bool virtualDesktop) { + if(_hostingController.autoRestart.value) { + final notification = LocalNotification( + title: translations.gameServerEnd, + body: translations.gameServerRestart(_kRebootDelay.inSeconds), + ); + notification.show(); + Future.delayed(_kRebootDelay).then((_) async { + log("[RESTARTER] Stopping server..."); + await _onStop( + reason: _StopReason.normal, + host: true + ); + log("[RESTARTER] Stopped server"); + log("[RESTARTER] Starting server..."); + await _toggle( + host: true + ); + log("[RESTARTER] Started server"); + }); + }else { + final notification = LocalNotification( + title: translations.gameServerEnd, + body: translations.gameServerShutdown(_kRebootDelay.inSeconds) + ); + notification.show(); + Future.delayed(_kRebootDelay).then((_) { + log("[RESTARTER] Stopping server..."); + _onStop( + reason: _StopReason.normal, + host: true + ); + log("[RESTARTER] Stopped server"); + }); + } + } + + Future _onLoggedIn(bool host) async { + final instance = host ? _hostingController.instance.value : _gameController.instance.value; + if(instance != null && !instance.launched) { + instance.launched = true; + instance.tokenError = false; + await _injectOrShowError(InjectableDll.memory, host); + if(!host){ + await _injectOrShowError(InjectableDll.console, host); + _onGameClientInjected(); + }else { + final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); + if(gameServerPort != null) { + await killProcessByPort(gameServerPort); + } + await _injectOrShowError(InjectableDll.reboot, host); + _onGameServerInjected(); + } + } + } + void _onGameClientInjected() { _gameClientInfoBar?.close(); showInfoBar( @@ -411,11 +422,11 @@ class _LaunchButtonState extends State { duration: null ); final gameServerPort = _settingsController.gameServerPort.text; - _gameServerInfoBar?.close(); final localPingResult = await pingGameServer( "127.0.0.1:$gameServerPort", timeout: const Duration(minutes: 2) ); + _gameServerInfoBar?.close(); if (!localPingResult) { showInfoBar( translations.gameServerStartWarning, @@ -424,7 +435,6 @@ class _LaunchButtonState extends State { ); return; } - _backendController.joinLocalHost(); final accessible = await _checkGameServer(theme, gameServerPort); if (!accessible) { @@ -487,6 +497,20 @@ class _LaunchButtonState extends State { } Future _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { + if(host == null) { + await _operation?.cancel(); + _operation = null; + await _backendController.worker?.cancel(); + } + + host = host ?? widget.host; + final instance = host ? _hostingController.instance.value : _gameController.instance.value; + if(host){ + _hostingController.instance.value = null; + }else { + _gameController.instance.value = null; + } + if(_virtualDesktop != null) { try { final instance = VirtualDesktopManager.getInstance(); @@ -496,20 +520,12 @@ class _LaunchButtonState extends State { } } - if(host == null) { - await _operation?.cancel(); - _operation = null; - await _backendController.worker?.cancel(); - } - - host = host ?? widget.host; log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace"); log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}"); if(host) { _hostingController.discardServer(); } - final instance = host ? _hostingController.instance.value : _gameController.instance.value; if(instance != null) { if(reason == _StopReason.normal) { instance.launched = true; @@ -518,25 +534,21 @@ class _LaunchButtonState extends State { instance.kill(); final child = instance.child; if(child != null) { - _onStop( + await _onStop( reason: reason, host: child.hosting ); } - - if(host){ - _hostingController.instance.value = null; - }else { - _gameController.instance.value = null; - } } _setStarted(host, false); - if(host) { - _gameServerInfoBar?.close(); - }else { - _gameClientInfoBar?.close(); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + if(host == true) { + _gameServerInfoBar?.close(); + }else { + _gameClientInfoBar?.close(); + } + }); switch(reason) { case _StopReason.backendError: @@ -558,7 +570,6 @@ class _LaunchButtonState extends State { ); break; case _StopReason.exitCode: - final instance = host ? _hostingController.instance.value : _gameController.instance.value; if(instance != null && !instance.launched) { showInfoBar( translations.corruptedVersionError, @@ -566,7 +577,6 @@ class _LaunchButtonState extends State { duration: infoBarLongDuration, ); } - break; case _StopReason.corruptedVersionError: showInfoBar( @@ -584,7 +594,7 @@ class _LaunchButtonState extends State { break; case _StopReason.tokenError: showInfoBar( - translations.tokenError, + translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"), severity: InfoBarSeverity.error, duration: infoBarLongDuration, ); @@ -622,6 +632,7 @@ class _LaunchButtonState extends State { log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}..."); await injectDll(gameProcess, dllPath); + instance.injectedDlls.add(injectable); log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}"); } catch (error, stackTrace) { log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace"); diff --git a/gui/lib/src/widget/server_type_selector.dart b/gui/lib/src/widget/server_type_selector.dart index 15c6151..3a3fee5 100644 --- a/gui/lib/src/widget/server_type_selector.dart +++ b/gui/lib/src/widget/server_type_selector.dart @@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; 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/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/util/translations.dart'; class ServerTypeSelector extends StatefulWidget { @@ -18,6 +19,7 @@ class _ServerTypeSelectorState extends State { @override Widget build(BuildContext context) { return Obx(() => DropDownButton( + onOpen: () => inDialog = true, leading: Text(_controller.type.value.label), items: ServerType.values .map((type) => _createItem(type)) diff --git a/gui/lib/src/widget/version_selector.dart b/gui/lib/src/widget/version_selector.dart index b10adff..7806499 100644 --- a/gui/lib/src/widget/version_selector.dart +++ b/gui/lib/src/widget/version_selector.dart @@ -14,6 +14,7 @@ import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart'; +import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:url_launcher/url_launcher.dart'; class VersionSelector extends StatefulWidget { @@ -39,16 +40,22 @@ class _VersionSelectorState extends State { @override Widget build(BuildContext context) => Obx(() { return _createOptionsMenu( - version: _gameController.selectedVersion, - close: false, - child: FlyoutTarget( - controller: _flyoutController, - child: DropDownButton( - leading: Text(_gameController.selectedVersion?.name ?? translations.selectVersion), - items: _createSelectorItems(context) - ), - ) - ); + version: _gameController.selectedVersion, + close: false, + child: FlyoutTarget( + controller: _flyoutController, + child: DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text( + _gameController.selectedVersion?.name ?? translations.selectVersion, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + items: _createSelectorItems(context) + ), + ) + ); }); List _createSelectorItems(BuildContext context) { diff --git a/gui/lib/src/widget/version_selector_tile.dart b/gui/lib/src/widget/version_selector_tile.dart index c77ca45..beb4da8 100644 --- a/gui/lib/src/widget/version_selector_tile.dart +++ b/gui/lib/src/widget/version_selector_tile.dart @@ -10,5 +10,11 @@ SettingTile get versionSelectSettingTile => SettingTile( ), title: Text(translations.selectFortniteName), subtitle: Text(translations.selectFortniteDescription), - content: const VersionSelector() + contentWidth: null, + content: ConstrainedBox( + constraints: BoxConstraints( + minWidth: SettingTile.kDefaultContentWidth, + ), + child: const VersionSelector() + ) ); \ No newline at end of file diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 3fda0f1..6704b54 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.1.0" +version: "9.1.3" publish_to: 'none' diff --git a/gui/windows/packaging/exe/custom-inno-setup-script.iss b/gui/windows/packaging/exe/custom-inno-setup-script.iss index c06ad28..41fac78 100644 --- a/gui/windows/packaging/exe/custom-inno-setup-script.iss +++ b/gui/windows/packaging/exe/custom-inno-setup-script.iss @@ -10,7 +10,7 @@ AppPublisher={{PUBLISHER_NAME}} AppPublisherURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}} -DefaultDirName={autopf}\{{DISPLAY_NAME}}; +DefaultDirName={autopf}\{{DISPLAY_NAME}} DisableProgramGroupPage=yes OutputBaseFilename={{OUTPUT_BASE_FILENAME}} Compression=zip @@ -28,8 +28,11 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +[Dirs] +Name: "{app}"; Permissions: everyone-full + [Files] -Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full [Run] Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden