This commit is contained in:
Alessandro Autiero
2025-04-16 15:43:34 +02:00
parent dc2d4c4377
commit c3ede3b745
15 changed files with 374 additions and 200 deletions

View File

@@ -16,7 +16,12 @@ const Command _build = Command(name: 'versions', parameters: [], subCommands: [_
const Command _play = Command(name: 'play', parameters: [], subCommands: []); const Command _play = Command(name: 'play', parameters: [], subCommands: []);
const Command _host = Command(name: 'host', parameters: [], subCommands: []); const Command _host = Command(name: 'host', parameters: [], subCommands: []);
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []); const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
final List<String> _versions = downloadableBuilds.map((build) => build.version.toString()).toList(growable: false); final List<String> _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<String> _versionActions = [_playVersionAction, _hostVersionAction, _deleteVersionAction, _infoVersionAction];
void main(List<String> args) async { void main(List<String> args) async {
enableLoggingToConsole = false; enableLoggingToConsole = false;
@@ -98,19 +103,46 @@ Future<void> _handleBuildCommand(CommandCall? call) async {
} }
void _handleBuildListCommand(CommandCall commandCall) { void _handleBuildListCommand(CommandCall commandCall) {
final versions = readVersions(); List<FortniteVersion> versions;
try {
versions = readVersions();
}catch(error) {
print("$error");
return;
}
if(versions.isEmpty) {
print("❌ No versions found");
return;
}
final versionSelector = Select.withTheme( final versionSelector = Select.withTheme(
prompt: ' Select a version:', 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: '') theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
); );
final version = versions[versionSelector.interact()]; final version = versions[versionSelector.interact()];
final actionSelector = Select.withTheme( final actionSelector = Select.withTheme(
prompt: ' Select an action:', prompt: ' Select an action:',
options: ['Play', 'Host', 'Delete', 'Open in Explorer'], options: _versionActions,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '') 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<void> _handleBuildImportCommand(CommandCall call) async { Future<void> _handleBuildImportCommand(CommandCall call) async {
@@ -125,8 +157,8 @@ Future<void> _handleBuildImportCommand(CommandCall call) async {
} }
final fortniteVersion = FortniteVersion( final fortniteVersion = FortniteVersion(
name: "dummy", name: '',
content: Version.parse(version), gameVersion: version,
location: Directory(path) location: Directory(path)
); );
writeVersion(fortniteVersion); writeVersion(fortniteVersion);
@@ -197,18 +229,32 @@ Future<bool> _checkBuildPath(String path, bool existing) async {
if (existing) { if (existing) {
final checker = Spinner.withTheme( final checker = Spinner.withTheme(
icon: '', 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(' ')) theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
).interact(); ).interact();
final result = await Future.wait([
Future.delayed(const Duration(seconds: 1)).then((_) => true), final files = await findFiles(directory, "FortniteClient-Win64-Shipping.exe")
Isolate.run(() => FortniteVersionExtension.findFiles(directory, "FortniteClient-Win64-Shipping.exe") != null) .withMinimumDuration(const Duration(seconds: 1));
]).then((values) => values.reduce((first, second) => first && second)); if(files.isEmpty) {
checker.done(); print("❌ Cannot find FortniteClient-Win64-Shipping.exe in $path");
if(!result) {
print("❌ Cannot find FortniteClient-Win64-Shipping.exe: $path");
return false; 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; return true;
@@ -309,7 +355,7 @@ Future<void> _handleBuildDownloadCommand(CommandCall call) async {
} }
final parsedVersion = Version.parse(version); 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) { if(build == null) {
print(''); print('');
print("❌ Cannot find mirror for version: $parsedVersion"); print("❌ Cannot find mirror for version: $parsedVersion");
@@ -339,8 +385,8 @@ Future<void> _handleBuildDownloadCommand(CommandCall call) async {
downloader.done(); downloader.done();
receivePort.close(); receivePort.close();
final fortniteVersion = FortniteVersion( final fortniteVersion = FortniteVersion(
name: "dummy", name: "dummy",
content: parsedVersion, gameVersion: version,
location: parsedDirectory location: parsedDirectory
); );
writeVersion(fortniteVersion); writeVersion(fortniteVersion);

View File

@@ -9,10 +9,20 @@ List<FortniteVersion> readVersions() {
return []; return [];
} }
Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync()); try {
return decodedVersionsJson Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync());
.map((entry) => FortniteVersion.fromJson(entry)) return decodedVersionsJson
.toList(); .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) { void writeVersion(FortniteVersion version) {

View File

@@ -8,3 +8,13 @@ extension IterableExtension<E> on Iterable<E> {
return null; return null;
} }
} }
extension FutureExtension<T> on Future<T> {
Future<T> withMinimumDuration(Duration duration) async {
final result = await Future.wait([
Future.delayed(duration),
this
]);
return result.last;
}
}

View File

@@ -30,4 +30,5 @@ const String kShippingExe = "FortniteClient-Win64-Shipping.exe";
const String kLauncherExe = "FortniteLauncher.exe"; const String kLauncherExe = "FortniteLauncher.exe";
const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe"; const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe";
const String kCrashReportExe = "CrashReportClient.exe"; const String kCrashReportExe = "CrashReportClient.exe";
const String kGFSDKAftermathLibDll = "GFSDK_Aftermath_Lib.dll";
final Version kMaxAllowedVersion = Version.parse("30.10"); final Version kMaxAllowedVersion = Version.parse("30.10");

View File

@@ -1,11 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance { class GameInstance {
final String version; final String version;
final bool host;
final int gamePid; final int gamePid;
final int? launcherPid; final int? launcherPid;
final int? eacPid; final int? eacPid;
@@ -18,6 +18,7 @@ class GameInstance {
GameInstance({ GameInstance({
required this.version, required this.version,
required this.host,
required this.gamePid, required this.gamePid,
required this.launcherPid, required this.launcherPid,
required this.eacPid, required this.eacPid,

View File

@@ -15,7 +15,13 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp; String? _lastIp;
String? _lastPort; String? _lastPort;
Stream<ServerResult> startBackend({required ServerType type, required String host, required String port, required bool detached, required void Function(String) onError}) async* { Stream<ServerResult> startBackend({
required ServerType type,
required String host,
required String port,
required bool detached,
required void Function(String) onError
}) async* {
Process? process; Process? process;
HttpServer? server; HttpServer? server;
try { try {
@@ -147,7 +153,13 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
} }
}); });
if(!detached) { 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; return process;
} }

View File

@@ -369,7 +369,7 @@ Future<String> extractGameVersion(Directory directory) => Isolate.run(() async {
log("[VERSION] Engine build: $engineVersionBuild"); log("[VERSION] Engine build: $engineVersionBuild");
gameVersion = _buildToGameVersion[engineVersionBuild] ?? defaultGameVersion; gameVersion = _buildToGameVersion[engineVersionBuild] ?? defaultGameVersion;
} }
log("[VERSION] Returning $gameVersion"); log("[VERSION] Parsed game version: $gameVersion");
return gameVersion; return gameVersion;
} }
} }

View File

@@ -243,8 +243,8 @@
"gameServerStarted": "The game server was started successfully", "gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully", "gameClientStarted": "The game client was started successfully",
"checkingGameServer": "Checking if other players can join the game server...", "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", "checkGameServerFixMessage": "The game server was started successfully, but other players can't join yet as port {port} isn't open",
"checkGameServerFixAction": "Fix", "checkGameServerFixAction": "Learn more",
"infoName": "Info", "infoName": "Info",
"emptyVersionName": "Empty version name", "emptyVersionName": "Empty version name",
"versionAlreadyExists": "This version already exists", "versionAlreadyExists": "This version already exists",
@@ -379,5 +379,9 @@
"importedVersion": "Successfully imported version", "importedVersion": "Successfully imported version",
"importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory", "importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory",
"importVersionMultipleShippingExesError": "Cannot import version: only one {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"
} }

View File

@@ -163,24 +163,26 @@ class BackendController extends GetxController {
port: port.text, port: port.text,
detached: detached.value, detached: detached.value,
onError: (errorMessage) { onError: (errorMessage) {
stop(interactive: false); if(started.value) {
Get.find<GameController>() stop(interactive: false);
.instance Get.find<GameController>()
.value .instance
?.kill(); .value
Get.find<HostingController>() ?.kill();
.instance Get.find<HostingController>()
.value .instance
?.kill(); .value
_showRebootInfoBar( ?.kill();
translations.backendErrorMessage, _showRebootInfoBar(
severity: InfoBarSeverity.error, translations.backendErrorMessage,
duration: infoBarLongDuration, severity: InfoBarSeverity.error,
action: Button( duration: infoBarLongDuration,
onPressed: () => launchUrl(launcherLogFile.uri), action: Button(
child: Text(translations.openLog), onPressed: () => launchUrl(launcherLogFile.uri),
) child: Text(translations.openLog),
); )
);
}
} }
); );
final completer = Completer<bool>(); final completer = Completer<bool>();

View File

@@ -5,7 +5,7 @@ const infoBarLongDuration = Duration(seconds: 4);
const infoBarShortDuration = Duration(seconds: 2); const infoBarShortDuration = Duration(seconds: 2);
const _height = 64.0; const _height = 64.0;
InfoBarEntry showRebootInfoBar(dynamic text, { InfoBarEntry showRebootInfoBar(String text, {
InfoBarSeverity severity = InfoBarSeverity.info, InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false, bool loading = false,
Duration? duration = infoBarShortDuration, Duration? duration = infoBarShortDuration,

View File

@@ -7,6 +7,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:port_forwarder/port_forwarder.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/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/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart'; import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.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/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -110,6 +110,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
log("[${host ? 'HOST' : 'GAME'}] Backend works"); log("[${host ? 'HOST' : 'GAME'}] Backend works");
final headless = _hostingController.headless.value; final headless = _hostingController.headless.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false); final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false);
@@ -121,6 +122,16 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
if(host || linkedHostingInstance != null) {
if (_dllController.gameServerPort.text == kDefaultBackendPort.toString()) {
_onStop(
reason: _StopReason.gameServerPortError,
host: host
);
return;
}
}
if(!host) { if(!host) {
_showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null); _showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
}else { }else {
@@ -131,7 +142,7 @@ class _LaunchButtonState extends State<LaunchButton> {
reason: _StopReason.corruptedVersionError, reason: _StopReason.corruptedVersionError,
error: exception.toString(), error: exception.toString(),
stackTrace: stackTrace, stackTrace: stackTrace,
host: host host: host
); );
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
_onStop( _onStop(
@@ -213,6 +224,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance( final instance = GameInstance(
version: version.gameVersion, version: version.gameVersion,
host: host,
gamePid: gameProcess, gamePid: gameProcess,
launcherPid: launcherProcess, launcherPid: launcherProcess,
eacPid: eacProcess, eacPid: eacProcess,
@@ -232,6 +244,22 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async { Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process..."); 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); final shippingExecutables = await findFiles(version.location, kShippingExe);
if(shippingExecutables.isEmpty){ if(shippingExecutables.isEmpty){
log("[${host ? 'HOST' : 'GAME'}] No game executable found"); log("[${host ? 'HOST' : 'GAME'}] No game executable found");
@@ -421,36 +449,30 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
} }
final theme = FluentTheme.of(appNavigatorKey.currentContext!);
try { try {
_gameServerInfoBar = showRebootInfoBar(
translations.waitingForGameServer,
loading: true,
duration: null
);
final gameServerPort = _dllController.gameServerPort.text; final gameServerPort = _dllController.gameServerPort.text;
final pingOperation = pingGameServerOrTimeout( final started = await _checkLocalGameServer(gameServerPort);
"127.0.0.1:$gameServerPort", if(!started) {
const Duration(minutes: 2) if (_hostingController.instance.value?.killed != true) {
); showRebootInfoBar(
this._pingOperation = pingOperation; translations.gameServerStartWarning,
final localPingResult = await pingOperation.future; severity: InfoBarSeverity.error,
_gameServerInfoBar?.close(); duration: infoBarLongDuration
if (!localPingResult) { );
showRebootInfoBar( }
translations.gameServerStartWarning,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
return; return;
} }
_backendController.joinLocalhost(); _backendController.joinLocalhost();
final accessible = await _checkGameServer(theme, gameServerPort); final accessible = await _checkPublicGameServer(gameServerPort);
if (!accessible) { if (!accessible) {
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStartLocalWarning, translations.gameServerStartLocalWarning,
severity: InfoBarSeverity.warning, severity: InfoBarSeverity.warning,
duration: infoBarLongDuration duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrlString("https://github.com/Auties00/reboot_launcher/blob/master/documentation/$currentLocale/PortForwarding.md"),
child: Text(translations.checkGameServerFixAction),
),
); );
return; return;
} }
@@ -469,7 +491,30 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<bool> _checkGameServer(FluentThemeData theme, String gameServerPort) async { Future<bool> _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<bool> _checkPublicGameServer(String gameServerPort) async {
try { try {
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkingGameServer, translations.checkingGameServer,
@@ -477,43 +522,64 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final publicIp = await Ipify.ipv4(); final publicIp = await Ipify.ipv4();
final available = await pingGameServer("$publicIp:$gameServerPort"); var pingOperation = await pingGameServerOrTimeout(
if(available) { "$publicIp:$gameServerPort",
const Duration(seconds: 10)
);
_pingOperation = pingOperation;
var publicPingResult = await pingOperation.future;
if (publicPingResult) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return true; 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", "$publicIp:$gameServerPort",
const Duration(days: 1) const Duration(seconds: 10)
); );
this._pingOperation = pingOperation; _pingOperation = pingOperation;
publicPingResult = await pingOperation.future;
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
_gameServerInfoBar = showRebootInfoBar( return publicPingResult;
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;
}catch(_) { }catch(_) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
return false; return false;
} }
} }
Future<void> _onStop({required _StopReason reason, required bool host, String? error, StackTrace? stackTrace}) async { Future<void> _onStop({
required _StopReason reason,
required bool host,
String? error,
StackTrace? stackTrace,
bool interactive = true
}) async {
if(host) { if(host) {
try { try {
_pingOperation?.complete(false); _pingOperation?.complete(false);
} catch (_) { } catch (_) {
// Ignore: might be running, don't bother checking // Ignore: might have been already terminated, don't bother checking
} finally { } finally {
_pingOperation = null; _pingOperation = null;
} }
@@ -545,107 +611,121 @@ class _LaunchButtonState extends State<LaunchButton> {
if(child != null) { if(child != null) {
await _onStop( await _onStop(
reason: reason, reason: reason,
host: host host: child.host,
error: error,
stackTrace: stackTrace,
interactive: false
); );
} }
_setStarted(host, false); _setStarted(host, false);
WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close();
}else {
_gameClientInfoBar?.close();
}
});
switch(reason) { if(interactive) {
case _StopReason.backendError: WidgetsBinding.instance.addPostFrameCallback((_) {
case _StopReason.matchmakerError: if(host == true) {
case _StopReason.normal: _gameServerInfoBar?.close();
break; }else {
case _StopReason.missingVersionError: _gameClientInfoBar?.close();
showRebootInfoBar( }
translations.missingVersionError, });
severity: InfoBarSeverity.error,
duration: infoBarLongDuration, switch(reason) {
); case _StopReason.backendError:
break; case _StopReason.matchmakerError:
case _StopReason.missingExecutableError: case _StopReason.normal:
showRebootInfoBar( break;
translations.missingExecutableError, case _StopReason.missingVersionError:
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;
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")), translations.missingVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
} break;
break; case _StopReason.missingExecutableError:
case _StopReason.corruptedVersionError: showRebootInfoBar(
showRebootInfoBar( translations.missingExecutableError,
translations.corruptedVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( );
onPressed: () => launchUrl(launcherLogFile.uri), break;
child: Text(translations.openLog), case _StopReason.multipleExecutablesError:
) showRebootInfoBar(
); translations.multipleExecutablesError(error ?? translations.unknown),
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, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( );
onPressed: () => launchUrl(launcherLogFile.uri), break;
child: Text(translations.openLog), case _StopReason.exitCode:
) if(instance != null && !instance.launched) {
); final injectedDlls = instance.injectedDlls;
break; showRebootInfoBar(
case _StopReason.crash: translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
showRebootInfoBar( severity: InfoBarSeverity.error,
translations.fortniteCrashError(host ? "game server" : "client"), duration: infoBarLongDuration,
severity: InfoBarSeverity.error, );
duration: infoBarLongDuration, }
); break;
break; case _StopReason.corruptedVersionError:
case _StopReason.unknownError: final injectedDlls = instance?.injectedDlls ?? [];
showRebootInfoBar( showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError), translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); action: Button(
break; 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<LaunchButton> {
if(customDll) { if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop( _onStop(
reason: _StopReason.missingCustomDllError, reason: _StopReason.missingCustomDllError,
error: injectable.name, error: injectable.name,
host: host host: host
); );
return null; return null;
} }
@@ -765,6 +845,7 @@ enum _StopReason {
matchmakerError, matchmakerError,
tokenError, tokenError,
unknownError, unknownError,
gameServerPortError,
exitCode, exitCode,
crash; crash;

View File

@@ -131,7 +131,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
FluentIcons.password_24_regular FluentIcons.password_24_regular
), ),
title: Text(translations.hostGameServerPasswordName), title: Text(translations.hostGameServerPasswordName),
subtitle: Text(translations.hostGameServerDescriptionDescription), subtitle: Text(translations.hostGameServerPasswordDescription),
content: Obx(() => OverlayTarget( content: Obx(() => OverlayTarget(
key: hostInfoPasswordOverlayTargetKey, key: hostInfoPasswordOverlayTargetKey,
child: TextFormBox( child: TextFormBox(

View File

@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.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/translations.dart';
import 'package:reboot_launcher/src/util/types.dart'; import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.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'; import 'package:windows_taskbar/windows_taskbar.dart';
class DownloadVersionDialog extends StatefulWidget { class DownloadVersionDialog extends StatefulWidget {
@@ -75,15 +75,24 @@ class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
buttons: _stopButton buttons: _stopButton
); );
case _DownloadStatus.error: case _DownloadStatus.error:
return ErrorDialog( final build = _build.value;
exception: _error ?? Exception(translations.unknownError), var error = _error?.toString() ?? translations.unknownError;
stackTrace: _stackTrace, error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
errorMessageBuilder: (exception) { error = error.toLowerCase();
var error = exception.toString(); return InfoDialog(
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error; text: translations.downloadVersionError(error),
error = error.toLowerCase(); buttons: [
return translations.downloadVersionError(error); DialogButton(
} type: ButtonType.secondary,
text: translations.defaultDialogSecondaryAction
),
if(build != null)
DialogButton(
type: ButtonType.primary,
text: translations.downloadManually,
onTap: () => launchUrlString(build.link)
),
],
); );
case _DownloadStatus.done: case _DownloadStatus.done:
return InfoDialog( return InfoDialog(

View File

@@ -266,9 +266,6 @@ class _VersionSelectorState extends State<VersionSelector> {
) )
); );
} }
@override
GameController get gameController => _gameController;
} }
enum _ContextualOption { enum _ContextualOption {

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "10.0.8" version: "10.0.9"
publish_to: 'none' publish_to: 'none'
@@ -54,6 +54,7 @@ dependencies:
file_picker: ^8.1.2 file_picker: ^8.1.2
url_launcher: ^6.3.0 url_launcher: ^6.3.0
local_notifier: ^0.1.6 local_notifier: ^0.1.6
port_forwarder: ^1.0.0
# Server browser # Server browser
supabase_flutter: ^2.7.0 supabase_flutter: ^2.7.0