diff --git a/cli/lib/main.dart b/cli/lib/main.dart index 40b6a8d..70e5bbe 100644 --- a/cli/lib/main.dart +++ b/cli/lib/main.dart @@ -16,7 +16,12 @@ const Command _build = Command(name: 'versions', parameters: [], subCommands: [_ const Command _play = Command(name: 'play', parameters: [], subCommands: []); const Command _host = Command(name: 'host', parameters: [], subCommands: []); const Command _backend = Command(name: 'backend', parameters: [], subCommands: []); -final List _versions = downloadableBuilds.map((build) => build.version.toString()).toList(growable: false); +final List _versions = downloadableBuilds.map((build) => build.gameVersion).toList(growable: false); +const String _playVersionAction = 'Play'; +const String _hostVersionAction = 'Host'; +const String _deleteVersionAction = 'Delete'; +const String _infoVersionAction = 'Info'; +const List _versionActions = [_playVersionAction, _hostVersionAction, _deleteVersionAction, _infoVersionAction]; void main(List args) async { enableLoggingToConsole = false; @@ -98,19 +103,46 @@ Future _handleBuildCommand(CommandCall? call) async { } void _handleBuildListCommand(CommandCall commandCall) { - final versions = readVersions(); + List versions; + try { + versions = readVersions(); + }catch(error) { + print("❌ $error"); + return; + } + + if(versions.isEmpty) { + print("❌ No versions found"); + return; + } + final versionSelector = Select.withTheme( prompt: ' Select a version:', - options: versions.map((version) => version.content.toString()).toList(growable: false), + options: versions.map((version) => version.gameVersion).toList(growable: false), theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌') ); final version = versions[versionSelector.interact()]; final actionSelector = Select.withTheme( prompt: ' Select an action:', - options: ['Play', 'Host', 'Delete', 'Open in Explorer'], + options: _versionActions, theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '', successSuffix: '', errorPrefix: '❌') ); - actionSelector.interact(); + final action = _versionActions[actionSelector.interact()]; + switch(action) { + case _playVersionAction: + break; + case _hostVersionAction: + break; + case _deleteVersionAction: + break; + case _infoVersionAction: + print(''); + print(""" +🏷️ ${"Version: ".cyan()} ${version.gameVersion} +📁 ${"Location:".cyan()} ${version.location.path} +""".green()); + break; + } } Future _handleBuildImportCommand(CommandCall call) async { @@ -125,8 +157,8 @@ Future _handleBuildImportCommand(CommandCall call) async { } final fortniteVersion = FortniteVersion( - name: "dummy", - content: Version.parse(version), + name: '', + gameVersion: version, location: Directory(path) ); writeVersion(fortniteVersion); @@ -197,18 +229,32 @@ Future _checkBuildPath(String path, bool existing) async { if (existing) { final checker = Spinner.withTheme( icon: '✅', - rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished looking for FortniteClient-Win64-Shipping.exe' : 'Looking for FortniteClient-Win64-Shipping.exe...', + rightPrompt: (status) { + switch(status) { + case SpinnerStateType.inProgress: + return 'Looking for FortniteClient-Win64-Shipping.exe...'; + case SpinnerStateType.done: + return 'Finished looking for FortniteClient-Win64-Shipping.exe'; + case SpinnerStateType.failed: + return 'Failed to look for FortniteClient-Win64-Shipping.exe'; + } + }, theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '❌', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' ')) ).interact(); - final result = await Future.wait([ - Future.delayed(const Duration(seconds: 1)).then((_) => true), - Isolate.run(() => FortniteVersionExtension.findFiles(directory, "FortniteClient-Win64-Shipping.exe") != null) - ]).then((values) => values.reduce((first, second) => first && second)); - checker.done(); - if(!result) { - print("❌ Cannot find FortniteClient-Win64-Shipping.exe: $path"); + + final files = await findFiles(directory, "FortniteClient-Win64-Shipping.exe") + .withMinimumDuration(const Duration(seconds: 1)); + if(files.isEmpty) { + print("❌ Cannot find FortniteClient-Win64-Shipping.exe in $path"); return false; } + + if(files.length > 1) { + print("❌ There must be only one executable named FortniteClient-Win64-Shipping.exe in $path"); + return false; + } + + checker.done(); } return true; @@ -309,7 +355,7 @@ Future _handleBuildDownloadCommand(CommandCall call) async { } final parsedVersion = Version.parse(version); - final build = downloadableBuilds.firstWhereOrNull((build) => build.version == parsedVersion); + final build = downloadableBuilds.firstWhereOrNull((build) => Version.parse(build.gameVersion) == parsedVersion); if(build == null) { print(''); print("❌ Cannot find mirror for version: $parsedVersion"); @@ -339,8 +385,8 @@ Future _handleBuildDownloadCommand(CommandCall call) async { downloader.done(); receivePort.close(); final fortniteVersion = FortniteVersion( - name: "dummy", - content: parsedVersion, + name: "dummy", + gameVersion: version, location: parsedDirectory ); writeVersion(fortniteVersion); diff --git a/cli/lib/src/controller/config.dart b/cli/lib/src/controller/config.dart index 405f5b2..3faff9b 100644 --- a/cli/lib/src/controller/config.dart +++ b/cli/lib/src/controller/config.dart @@ -9,10 +9,20 @@ List readVersions() { return []; } - Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync()); - return decodedVersionsJson - .map((entry) => FortniteVersion.fromJson(entry)) - .toList(); + try { + Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync()); + return decodedVersionsJson + .map((entry) { + try { + return FortniteVersion.fromJson(entry); + }catch(error) { + throw "Cannot parse version: $error"; + } + }) + .toList(); + }catch(error) { + throw "Cannot parse versions: $error"; + } } void writeVersion(FortniteVersion version) { diff --git a/cli/lib/src/util/extensions.dart b/cli/lib/src/util/extensions.dart index baa2fa1..c97c9b3 100644 --- a/cli/lib/src/util/extensions.dart +++ b/cli/lib/src/util/extensions.dart @@ -7,4 +7,14 @@ extension IterableExtension on Iterable { } return null; } +} + +extension FutureExtension on Future { + Future withMinimumDuration(Duration duration) async { + final result = await Future.wait([ + Future.delayed(duration), + this + ]); + return result.last; + } } \ No newline at end of file diff --git a/common/lib/src/constant/game.dart b/common/lib/src/constant/game.dart index 0df782e..b712ecd 100644 --- a/common/lib/src/constant/game.dart +++ b/common/lib/src/constant/game.dart @@ -30,4 +30,5 @@ const String kShippingExe = "FortniteClient-Win64-Shipping.exe"; const String kLauncherExe = "FortniteLauncher.exe"; const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe"; const String kCrashReportExe = "CrashReportClient.exe"; +const String kGFSDKAftermathLibDll = "GFSDK_Aftermath_Lib.dll"; final Version kMaxAllowedVersion = Version.parse("30.10"); \ 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 f3f9844..42d6be0 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/model/game_instance.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; -import 'package:version/version.dart'; class GameInstance { final String version; + final bool host; final int gamePid; final int? launcherPid; final int? eacPid; @@ -18,6 +18,7 @@ class GameInstance { GameInstance({ required this.version, + required this.host, required this.gamePid, required this.launcherPid, required this.eacPid, diff --git a/common/lib/src/util/backend.dart b/common/lib/src/util/backend.dart index 9dc1bf6..21eda2a 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -15,7 +15,13 @@ final Semaphore _semaphore = Semaphore(); String? _lastIp; String? _lastPort; -Stream startBackend({required ServerType type, required String host, required String port, required bool detached, required void Function(String) onError}) async* { +Stream startBackend({ + required ServerType type, + required String host, + required String port, + required bool detached, + required void Function(String) onError +}) async* { Process? process; HttpServer? server; try { @@ -147,7 +153,13 @@ Future startEmbeddedBackend(bool detached, {void Function(String)? onEr } }); if(!detached) { - process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); + process.exitCode.then((exitCode) { + if(!killed) { + log("[BACKEND] Exit code: $exitCode"); + onError?.call("Exit code: $exitCode"); + killed = true; + } + }); } return process; } diff --git a/common/lib/src/util/game.dart b/common/lib/src/util/game.dart index 92114d5..f7630af 100644 --- a/common/lib/src/util/game.dart +++ b/common/lib/src/util/game.dart @@ -369,7 +369,7 @@ Future extractGameVersion(Directory directory) => Isolate.run(() async { log("[VERSION] Engine build: $engineVersionBuild"); gameVersion = _buildToGameVersion[engineVersionBuild] ?? defaultGameVersion; } - log("[VERSION] Returning $gameVersion"); + log("[VERSION] Parsed game version: $gameVersion"); return gameVersion; } } diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index 7f95747..9983aa3 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -243,8 +243,8 @@ "gameServerStarted": "The game server was started successfully", "gameClientStarted": "The game client was started successfully", "checkingGameServer": "Checking if other players can join the game server...", - "checkGameServerFixMessage": "Other players can't join the game server as port {port} isn't open", - "checkGameServerFixAction": "Fix", + "checkGameServerFixMessage": "The game server was started successfully, but other players can't join yet as port {port} isn't open", + "checkGameServerFixAction": "Learn more", "infoName": "Info", "emptyVersionName": "Empty version name", "versionAlreadyExists": "This version already exists", @@ -379,5 +379,9 @@ "importedVersion": "Successfully imported version", "importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory", "importVersionMultipleShippingExesError": "Cannot import version: only one {name} should exist in the directory", - "importVersionUnsupportedVersionError": "This version of Fortnite is not supported by the launcher" + "importVersionUnsupportedVersionError": "This version of Fortnite is not supported by the launcher", + "downloadManually": "Download manually", + "gameServerPortEqualsBackendPort": "The game server port cannot be {backendPort} as its reserved for the backend", + "gameServer": "game server", + "client": "client" } diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index a428d19..0c7e506 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -163,24 +163,26 @@ class BackendController extends GetxController { port: port.text, detached: detached.value, onError: (errorMessage) { - stop(interactive: false); - Get.find() - .instance - .value - ?.kill(); - Get.find() - .instance - .value - ?.kill(); - _showRebootInfoBar( - translations.backendErrorMessage, - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - action: Button( - onPressed: () => launchUrl(launcherLogFile.uri), - child: Text(translations.openLog), - ) - ); + if(started.value) { + stop(interactive: false); + Get.find() + .instance + .value + ?.kill(); + Get.find() + .instance + .value + ?.kill(); + _showRebootInfoBar( + translations.backendErrorMessage, + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) + ); + } } ); final completer = Completer(); diff --git a/gui/lib/src/messenger/info_bar.dart b/gui/lib/src/messenger/info_bar.dart index 9f1dcc6..068d144 100644 --- a/gui/lib/src/messenger/info_bar.dart +++ b/gui/lib/src/messenger/info_bar.dart @@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4); const infoBarShortDuration = Duration(seconds: 2); const _height = 64.0; -InfoBarEntry showRebootInfoBar(dynamic text, { +InfoBarEntry showRebootInfoBar(String text, { InfoBarSeverity severity = InfoBarSeverity.info, bool loading = false, Duration? duration = infoBarShortDuration, diff --git a/gui/lib/src/widget/game/game_start_button.dart b/gui/lib/src/widget/game/game_start_button.dart index a7a8919..f6172dd 100644 --- a/gui/lib/src/widget/game/game_start_button.dart +++ b/gui/lib/src/widget/game/game_start_button.dart @@ -7,6 +7,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:local_notifier/local_notifier.dart'; import 'package:path/path.dart'; +import 'package:port_forwarder/port_forwarder.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'; @@ -14,7 +15,6 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/messenger/info_bar.dart'; -import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -110,6 +110,7 @@ class _LaunchButtonState extends State { return; } log("[${host ? 'HOST' : 'GAME'}] Backend works"); + final headless = _hostingController.headless.value; log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)"); final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false); @@ -121,6 +122,16 @@ class _LaunchButtonState extends State { return; } + if(host || linkedHostingInstance != null) { + if (_dllController.gameServerPort.text == kDefaultBackendPort.toString()) { + _onStop( + reason: _StopReason.gameServerPortError, + host: host + ); + return; + } + } + if(!host) { _showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null); }else { @@ -131,7 +142,7 @@ class _LaunchButtonState extends State { reason: _StopReason.corruptedVersionError, error: exception.toString(), stackTrace: stackTrace, - host: host + host: host ); } catch (exception, stackTrace) { _onStop( @@ -213,6 +224,7 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); final instance = GameInstance( version: version.gameVersion, + host: host, gamePid: gameProcess, launcherPid: launcherProcess, eacPid: eacProcess, @@ -232,6 +244,22 @@ class _LaunchButtonState extends State { Future _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async { log("[${host ? 'HOST' : 'GAME'}] Starting game process..."); + try { + log("[${host ? 'HOST' : 'GAME'}] Deleting $kGFSDKAftermathLibDll..."); + final dlls = await findFiles(version.location, kGFSDKAftermathLibDll); + log("[${host ? 'HOST' : 'GAME'}] Found ${dlls.length} to delete for $kGFSDKAftermathLibDll"); + for(final dll in dlls) { + log("[${host ? 'HOST' : 'GAME'}] Deleting ${dll.path}..."); + final result = await delete(dll); + if(result) { + log("[${host ? 'HOST' : 'GAME'}] Deleted ${dll.path}"); + }else { + log("[${host ? 'HOST' : 'GAME'}] Cannot delete ${dll.path}"); + } + } + }catch(_) { + + } final shippingExecutables = await findFiles(version.location, kShippingExe); if(shippingExecutables.isEmpty){ log("[${host ? 'HOST' : 'GAME'}] No game executable found"); @@ -421,36 +449,30 @@ class _LaunchButtonState extends State { _gameClientInfoBar?.close(); } - final theme = FluentTheme.of(appNavigatorKey.currentContext!); try { - _gameServerInfoBar = showRebootInfoBar( - translations.waitingForGameServer, - loading: true, - duration: null - ); final gameServerPort = _dllController.gameServerPort.text; - final pingOperation = pingGameServerOrTimeout( - "127.0.0.1:$gameServerPort", - const Duration(minutes: 2) - ); - this._pingOperation = pingOperation; - final localPingResult = await pingOperation.future; - _gameServerInfoBar?.close(); - if (!localPingResult) { - showRebootInfoBar( - translations.gameServerStartWarning, - severity: InfoBarSeverity.error, - duration: infoBarLongDuration - ); + final started = await _checkLocalGameServer(gameServerPort); + if(!started) { + if (_hostingController.instance.value?.killed != true) { + showRebootInfoBar( + translations.gameServerStartWarning, + severity: InfoBarSeverity.error, + duration: infoBarLongDuration + ); + } return; } _backendController.joinLocalhost(); - final accessible = await _checkGameServer(theme, gameServerPort); + final accessible = await _checkPublicGameServer(gameServerPort); if (!accessible) { showRebootInfoBar( - translations.gameServerStartLocalWarning, - severity: InfoBarSeverity.warning, - duration: infoBarLongDuration + translations.gameServerStartLocalWarning, + severity: InfoBarSeverity.warning, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"), + child: Text(translations.checkGameServerFixAction), + ), ); return; } @@ -469,7 +491,30 @@ class _LaunchButtonState extends State { } } - Future _checkGameServer(FluentThemeData theme, String gameServerPort) async { + Future _checkLocalGameServer(String gameServerPort) async { + try { + _gameServerInfoBar = showRebootInfoBar( + translations.waitingForGameServer, + loading: true, + duration: null + ); + final gameServerPort = _dllController.gameServerPort.text; + final pingOperation = pingGameServerOrTimeout( + "127.0.0.1:$gameServerPort", + const Duration(minutes: 2) + ); + this._pingOperation = pingOperation; + final localPingResult = await pingOperation.future; + _gameServerInfoBar?.close(); + return localPingResult; + }catch(_) { + _gameServerInfoBar?.close(); + return false; + } + } + + + Future _checkPublicGameServer(String gameServerPort) async { try { _gameServerInfoBar = showRebootInfoBar( translations.checkingGameServer, @@ -477,43 +522,64 @@ class _LaunchButtonState extends State { duration: null ); final publicIp = await Ipify.ipv4(); - final available = await pingGameServer("$publicIp:$gameServerPort"); - if(available) { + var pingOperation = await pingGameServerOrTimeout( + "$publicIp:$gameServerPort", + const Duration(seconds: 10) + ); + _pingOperation = pingOperation; + var publicPingResult = await pingOperation.future; + if (publicPingResult) { _gameServerInfoBar?.close(); return true; } - final pingOperation = pingGameServerOrTimeout( + final gateway = await Gateway.discover(); + if (gateway == null) { + _gameServerInfoBar?.close(); + return false; + } + + final forwarded = await gateway.openPort( + protocol: PortType.udp, + externalPort: int.parse(gameServerPort), + portDescription: "Reboot Game Server" + ); + if (!forwarded) { + _gameServerInfoBar?.close(); + return false; + } + + // Give the modem a couple of seconds just in case + // This is not technically necessary, but I can't guarantee that the modem has no race conditions + // So might as well wait + await Future.delayed(const Duration(seconds: 5)); + + pingOperation = await pingGameServerOrTimeout( "$publicIp:$gameServerPort", - const Duration(days: 1) + const Duration(seconds: 10) ); - this._pingOperation = pingOperation; + _pingOperation = pingOperation; + publicPingResult = await pingOperation.future; _gameServerInfoBar?.close(); - _gameServerInfoBar = showRebootInfoBar( - translations.checkGameServerFixMessage(gameServerPort), - action: Button( - onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"), - child: Text(translations.checkGameServerFixAction), - ), - severity: InfoBarSeverity.warning, - duration: null, - loading: true - ); - final result = await pingOperation.future; - _gameServerInfoBar?.close(); - return result; + return publicPingResult; }catch(_) { _gameServerInfoBar?.close(); return false; } } - Future _onStop({required _StopReason reason, required bool host, String? error, StackTrace? stackTrace}) async { + Future _onStop({ + required _StopReason reason, + required bool host, + String? error, + StackTrace? stackTrace, + bool interactive = true + }) async { if(host) { try { _pingOperation?.complete(false); } catch (_) { - // Ignore: might be running, don't bother checking + // Ignore: might have been already terminated, don't bother checking } finally { _pingOperation = null; } @@ -545,107 +611,121 @@ class _LaunchButtonState extends State { if(child != null) { await _onStop( reason: reason, - host: host + host: child.host, + error: error, + stackTrace: stackTrace, + interactive: false ); } _setStarted(host, false); - WidgetsBinding.instance.addPostFrameCallback((_) { - if(host == true) { - _gameServerInfoBar?.close(); - }else { - _gameClientInfoBar?.close(); - } - }); - switch(reason) { - case _StopReason.backendError: - case _StopReason.matchmakerError: - case _StopReason.normal: - break; - case _StopReason.missingVersionError: - showRebootInfoBar( - translations.missingVersionError, - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; - case _StopReason.missingExecutableError: - showRebootInfoBar( - translations.missingExecutableError, - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; - case _StopReason.multipleExecutablesError: - showRebootInfoBar( - translations.multipleExecutablesError(error ?? translations.unknown), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; - case _StopReason.exitCode: - if(instance != null && !instance.launched) { - final injectedDlls = instance.injectedDlls; + if(interactive) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if(host == true) { + _gameServerInfoBar?.close(); + }else { + _gameClientInfoBar?.close(); + } + }); + + switch(reason) { + case _StopReason.backendError: + case _StopReason.matchmakerError: + case _StopReason.normal: + break; + case _StopReason.missingVersionError: showRebootInfoBar( - translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), + translations.missingVersionError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, ); - } - break; - case _StopReason.corruptedVersionError: - showRebootInfoBar( - translations.corruptedVersionError, + break; + case _StopReason.missingExecutableError: + showRebootInfoBar( + translations.missingExecutableError, severity: InfoBarSeverity.error, duration: infoBarLongDuration, - action: Button( - onPressed: () => launchUrl(launcherLogFile.uri), - child: Text(translations.openLog), - ) - ); - break; - case _StopReason.corruptedDllError: - showRebootInfoBar( - translations.corruptedDllError(error ?? translations.unknownError), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; - case _StopReason.missingCustomDllError: - showRebootInfoBar( - translations.missingCustomDllError(error!), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; - case _StopReason.tokenError: - _backendController.stop(interactive: false); - final injectedDlls = instance?.injectedDlls; - showRebootInfoBar( - translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), + ); + break; + case _StopReason.multipleExecutablesError: + showRebootInfoBar( + translations.multipleExecutablesError(error ?? translations.unknown), severity: InfoBarSeverity.error, duration: infoBarLongDuration, - action: Button( - onPressed: () => launchUrl(launcherLogFile.uri), - child: Text(translations.openLog), - ) - ); - 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), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - ); - break; + ); + break; + case _StopReason.exitCode: + if(instance != null && !instance.launched) { + final injectedDlls = instance.injectedDlls; + showRebootInfoBar( + translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + } + break; + case _StopReason.corruptedVersionError: + final injectedDlls = instance?.injectedDlls ?? []; + showRebootInfoBar( + translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) + ); + break; + case _StopReason.corruptedDllError: + showRebootInfoBar( + translations.corruptedDllError(error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + case _StopReason.missingCustomDllError: + showRebootInfoBar( + translations.missingCustomDllError(error!), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + case _StopReason.tokenError: + _backendController.stop(interactive: false); + final injectedDlls = instance?.injectedDlls; + showRebootInfoBar( + translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) + ); + break; + case _StopReason.crash: + showRebootInfoBar( + translations.fortniteCrashError(host ? translations.gameServer : translations.client), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + case _StopReason.unknownError: + showRebootInfoBar( + translations.unknownFortniteError(error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + case _StopReason.gameServerPortError: + showRebootInfoBar( + translations.gameServerPortEqualsBackendPort(kDefaultBackendPort), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + ); + break; + } } } @@ -698,9 +778,9 @@ class _LaunchButtonState extends State { if(customDll) { log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); _onStop( - reason: _StopReason.missingCustomDllError, - error: injectable.name, - host: host + reason: _StopReason.missingCustomDllError, + error: injectable.name, + host: host ); return null; } @@ -765,6 +845,7 @@ enum _StopReason { matchmakerError, tokenError, unknownError, + gameServerPortError, exitCode, crash; diff --git a/gui/lib/src/widget/page/host_page.dart b/gui/lib/src/widget/page/host_page.dart index 2ac82b6..f08a45b 100644 --- a/gui/lib/src/widget/page/host_page.dart +++ b/gui/lib/src/widget/page/host_page.dart @@ -131,7 +131,7 @@ class _HostingPageState extends RebootPageState { FluentIcons.password_24_regular ), title: Text(translations.hostGameServerPasswordName), - subtitle: Text(translations.hostGameServerDescriptionDescription), + subtitle: Text(translations.hostGameServerPasswordDescription), content: Obx(() => OverlayTarget( key: hostInfoPasswordOverlayTargetKey, child: TextFormBox( diff --git a/gui/lib/src/widget/version/download_version.dart b/gui/lib/src/widget/version/download_version.dart index c8df923..993dbe3 100644 --- a/gui/lib/src/widget/version/download_version.dart +++ b/gui/lib/src/widget/version/download_version.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:isolate'; import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; @@ -12,6 +11,7 @@ import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/types.dart'; import 'package:reboot_launcher/src/widget/file/file_selector.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:windows_taskbar/windows_taskbar.dart'; class DownloadVersionDialog extends StatefulWidget { @@ -75,15 +75,24 @@ class _DownloadVersionDialogState extends State { buttons: _stopButton ); case _DownloadStatus.error: - return ErrorDialog( - exception: _error ?? Exception(translations.unknownError), - stackTrace: _stackTrace, - errorMessageBuilder: (exception) { - var error = exception.toString(); - error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error; - error = error.toLowerCase(); - return translations.downloadVersionError(error); - } + final build = _build.value; + var error = _error?.toString() ?? translations.unknownError; + error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error; + error = error.toLowerCase(); + return InfoDialog( + text: translations.downloadVersionError(error), + buttons: [ + DialogButton( + type: ButtonType.secondary, + text: translations.defaultDialogSecondaryAction + ), + if(build != null) + DialogButton( + type: ButtonType.primary, + text: translations.downloadManually, + onTap: () => launchUrlString(build.link) + ), + ], ); case _DownloadStatus.done: return InfoDialog( diff --git a/gui/lib/src/widget/version/version_selector.dart b/gui/lib/src/widget/version/version_selector.dart index dc63cb7..a45a1fb 100644 --- a/gui/lib/src/widget/version/version_selector.dart +++ b/gui/lib/src/widget/version/version_selector.dart @@ -266,9 +266,6 @@ class _VersionSelectorState extends State { ) ); } - - @override - GameController get gameController => _gameController; } enum _ContextualOption { diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 88ae1c6..e24dff7 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Graphical User Interface for Project Reboot -version: "10.0.8" +version: "10.0.9" publish_to: 'none' @@ -54,6 +54,7 @@ dependencies: file_picker: ^8.1.2 url_launcher: ^6.3.0 local_notifier: ^0.1.6 + port_forwarder: ^1.0.0 # Server browser supabase_flutter: ^2.7.0