diff --git a/cli/lib/cli.dart b/cli/lib/cli.dart index 511d4ee..bab2faf 100644 --- a/cli/lib/cli.dart +++ b/cli/lib/cli.dart @@ -1,87 +1,89 @@ -import 'dart:io'; +import 'dart:collection'; -import 'package:args/args.dart'; -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'; +class Parser { + final List commands; -late String? username; -late bool host; -late bool verbose; -late String dll; -late FortniteVersion version; -late bool autoRestart; + Parser({required this.commands}); -void main(List args) async { - stdout.writeln("Reboot Launcher"); - stdout.writeln("Wrote by Auties00"); - stdout.writeln("Version 1.0"); - - kill(); - - var parser = ArgParser() - ..addOption("path", mandatory: true) - ..addOption("username") - ..addOption("server-type", allowed: ServerType.values.map((entry) => entry.name), defaultsTo: ServerType.embedded.name) - ..addOption("server-host") - ..addOption("server-port") - ..addOption("matchmaking-address") - ..addOption("dll", defaultsTo: rebootDllFile.path) - ..addFlag("update", defaultsTo: true, negatable: true) - ..addFlag("log", defaultsTo: false) - ..addFlag("host", defaultsTo: false) - ..addFlag("auto-restart", defaultsTo: false, negatable: true); - var result = parser.parse(args); - - dll = result["dll"]; - host = result["host"]; - username = result["username"] ?? kDefaultPlayerName; - verbose = result["log"]; - version = FortniteVersion(name: "Dummy", location: Directory(result["path"])); - - await downloadRequiredDLLs(); - if(result["update"]) { - stdout.writeln("Updating reboot dll..."); - try { - await downloadRebootDll(kRebootDownloadUrl); - }catch(error){ - stderr.writeln("Cannot update reboot dll: $error"); + CommandCall? parse(List args) { + var position = 0; + var allowedCommands = _toMap(commands); + var allowedParameters = {}; + Command? command; + CommandCall? head; + CommandCall? tail; + String? parameterName; + while(position < args.length) { + final current = args[position].toLowerCase(); + if(parameterName != null) { + tail?.parameters[parameterName] = current; + parameterName = null; + }else if(allowedParameters.contains(current.toLowerCase())) { + parameterName = current.substring(2); + if(args.elementAtOrNull(position + 1) == '"') { + position++; + } + }else { + final newCommand = allowedCommands[current]; + if(newCommand != null) { + final newCall = CommandCall(name: newCommand.name); + if(head == null) { + head = newCall; + tail = newCall; + } + if(tail != null) { + tail.subCall = newCall; + } + tail = newCall; + command = newCommand; + allowedCommands = _toMap(newCommand.subCommands); + allowedParameters = _toParameters(command); + } + } + position++; } + return head; } - stdout.writeln("Launching game..."); - var executable = version.shippingExecutable; - if(executable == null){ - throw Exception("Missing game executable at: ${version.location.path}"); - } + Set _toParameters(Command? parent) => parent?.parameters + .map((e) => '--${e.toLowerCase()}') + .toSet() ?? {}; - final serverHost = result["server-host"]?.trim(); - if(serverHost?.isEmpty == true){ - throw Exception("Missing host name"); - } - - final serverPort = result["server-port"]?.trim(); - if(serverPort?.isEmpty == true){ - throw Exception("Missing port"); - } - - final serverPortNumber = serverPort == null ? null : int.tryParse(serverPort); - if(serverPort != null && serverPortNumber == null){ - throw Exception("Invalid port, use only numbers"); - } - - var started = await startServerCli( - serverHost, - serverPortNumber, - ServerType.values.firstWhere((element) => element.name == result["server-type"]) + Map _toMap(List children) => Map.fromIterable( + children, + key: (command) => command.name.toLowerCase(), + value: (command) => command ); - if(!started){ - stderr.writeln("Cannot start server!"); - return; - } +} - writeMatchmakingIp(result["matchmaking-address"]); - autoRestart = result["auto-restart"]; - await startGame(); +class Command { + final String name; + final List parameters; + final List subCommands; + + const Command({required this.name, required this.parameters, required this.subCommands}); + + @override + String toString() => 'Command{name: $name, parameters: $parameters, subCommands: $subCommands}'; +} + +class Parameter { + final String name; + final bool Function(String) validator; + + const Parameter({required this.name, required this.validator}); + + @override + String toString() => 'Parameter{name: $name, validator: $validator}'; +} + +class CommandCall { + final String name; + final Map parameters; + CommandCall? subCall; + + CommandCall({required this.name}) : parameters = {}; + + @override + String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}'; } \ No newline at end of file diff --git a/cli/lib/main.dart b/cli/lib/main.dart new file mode 100644 index 0000000..6a48004 --- /dev/null +++ b/cli/lib/main.dart @@ -0,0 +1,102 @@ +import 'package:interact/interact.dart'; +import 'package:reboot_cli/cli.dart'; +import 'package:tint/tint.dart'; + +const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []); +const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []); +const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]); +const Command _play = Command(name: 'play', parameters: [], subCommands: []); +const Command _host = Command(name: 'host', parameters: [], subCommands: []); +const Command _backend = Command(name: 'backend', parameters: [], subCommands: []); + +void main(List args) { + _welcome(); + + final parser = Parser(commands: [_build, _play, _host, _backend]); + final command = parser.parse(args); + print(command); + _handleRootCommand(command); +} + +void _handleRootCommand(CommandCall? call) { + switch(call == null ? null : call.name) { + case 'build': + _handleBuildCommand(call?.subCall); + break; + case 'play': + _handleBuildCommand(call?.subCall); + break; + case 'host': + _handleBuildCommand(call?.subCall); + break; + case 'backend': + _handleBuildCommand(call?.subCall); + break; + default: + _askRootCommand(); + break; + } +} + +void _askRootCommand() { + final commands = [_build.name, _play.name, _host.name, _backend.name]; + final commandSelector = Select.withTheme( + prompt: ' Select a command:', + options: commands, + theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '') + ); + _handleRootCommand(CommandCall(name: commands[commandSelector.interact()])); +} + +void _handleBuildCommand(CommandCall? call) { + switch(call == null ? null : call.name) { + case 'import': + _handleBuildImportCommand(call!); + break; + case 'download': + _handleBuildDownloadCommand(call!); + break; + default: + _askBuildCommand(); + break; + } +} + +void _handleBuildImportCommand(CommandCall call) { + final version = call.parameters['path']; + final path = call.parameters['path']; + print(version); + print(path); +} + +void _handleBuildDownloadCommand(CommandCall call) { + +} + +void _askBuildCommand() { + final commands = [_buildImport.name, _buildDownload.name]; + final commandSelector = Select.withTheme( + prompt: ' Select a build command:', + options: commands, + theme: Theme.colorfulTheme.copyWith(inputPrefix: '❓', inputSuffix: '') + ); + _handleBuildCommand(CommandCall(name: commands[commandSelector.interact()])); +} + +void _handlePlayCommand(CommandCall? call) { + +} + +void _handleHostCommand(CommandCall? call) { + +} + +void _handleBackendCommand(CommandCall? call) { + +} + +void _welcome() => print(""" +🎮 Reboot Launcher +🔥 Launch, manage, and play Fortnite using Project Reboot! +🚀 Developed by Auties00 - Version 10.0.7 +""".green()); \ No newline at end of file diff --git a/cli/lib/src/game.dart b/cli/lib/src/game.dart deleted file mode 100644 index 10ecbdb..0000000 --- a/cli/lib/src/game.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'dart:io'; - -import 'package:process_run/process_run.dart'; -import 'package:reboot_cli/cli.dart'; -import 'package:reboot_common/common.dart'; - -Process? _gameProcess; -Process? _launcherProcess; -Process? _eacProcess; - -Future startGame() async { - await _startLauncherProcess(version); - await _startEacProcess(version); - - var executable = await version.shippingExecutable; - if (executable == null) { - throw Exception("${version.location.path} no longer contains a Fortnite executable, did you delete or move it?"); - } - - if (username == null) { - username = "Reboot${host ? 'Host' : 'Player'}"; - stdout.writeln("No username was specified, using $username by default. Use --username to specify one"); - } - - _gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, "")) - ..exitCode.then((_) => _onClose()) - ..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose)); - _injectOrShowError("cobalt.dll"); -} - - -Future _startLauncherProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.launcherExecutable == null) { - return; - } - - _launcherProcess = await Process.start(dummyVersion.launcherExecutable!.path, []); - suspend(_launcherProcess!.pid); -} - -Future _startEacProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.eacExecutable == null) { - return; - } - - _eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []); - suspend(_eacProcess!.pid); -} - -void _onGameOutput(String line, String dll, bool hosting, bool verbose) { - if(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; - } - - if(kCannotConnectErrors.any((element) => line.contains(element))){ - stderr.writeln("The backend doesn't work! Token expired"); - _onClose(); - return; - } - - if(line.contains("Region ")){ - if(hosting) { - _injectOrShowError(dll, false); - }else { - _injectOrShowError("console.dll"); - } - - _injectOrShowError("memory.dll"); - } -} - -void _kill() { - _gameProcess?.kill(ProcessSignal.sigabrt); - _launcherProcess?.kill(ProcessSignal.sigabrt); - _eacProcess?.kill(ProcessSignal.sigabrt); -} - -Future _injectOrShowError(String binary, [bool locate = true]) async { - if (_gameProcess == null) { - return; - } - - try { - stdout.writeln("Injecting $binary..."); - var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary); - if(!dll.existsSync()){ - throw Exception("Cannot inject $dll: missing file"); - } - - await injectDll(_gameProcess!.pid, dll); - } catch (exception) { - throw Exception("Cannot inject binary: $binary"); - } -} - -void _onClose() { - _kill(); - sleep(const Duration(seconds: 3)); - stdout.writeln("The game was closed"); - if(autoRestart){ - stdout.writeln("Restarting automatically game"); - startGame(); - return; - } - - exit(0); -} \ No newline at end of file diff --git a/cli/lib/src/reboot.dart b/cli/lib/src/reboot.dart deleted file mode 100644 index 93ca9c9..0000000 --- a/cli/lib/src/reboot.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:http/http.dart' as http; -import 'package:reboot_common/common.dart'; - -// TODO: Use github -const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll"; -const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll"; -const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll"; -const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip"; - -Future downloadRequiredDLLs() async { - stdout.writeln("Downloading necessary components..."); - var consoleDll = File("${dllsDirectory.path}\\console.dll"); - if(!consoleDll.existsSync()){ - var response = await http.get(Uri.parse(_consoleDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download console.dll"); - } - - await consoleDll.writeAsBytes(response.bodyBytes); - } - - var craniumDll = File("${dllsDirectory.path}\\cobalt.dll"); - if(!craniumDll.existsSync()){ - var response = await http.get(Uri.parse(_baseDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download cobalt.dll"); - } - - await craniumDll.writeAsBytes(response.bodyBytes); - } - - var memoryFixDll = File("${dllsDirectory.path}\\memory.dll"); - if(!memoryFixDll.existsSync()){ - var response = await http.get(Uri.parse(_memoryFixDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download memory.dll"); - } - - await memoryFixDll.writeAsBytes(response.bodyBytes); - } - - if(!backendDirectory.existsSync()){ - var response = await http.get(Uri.parse(_embeddedConfigDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download embedded server config"); - } - - var tempZip = File("${tempDirectory.path}/reboot_config.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, backendDirectory.path); - } -} \ No newline at end of file diff --git a/cli/lib/src/server.dart b/cli/lib/src/server.dart deleted file mode 100644 index 29a9f1f..0000000 --- a/cli/lib/src/server.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'dart:io'; - -import 'package:reboot_common/common.dart'; -import 'package:reboot_common/src/util/backend.dart' as server; - -Future startServerCli(String? host, int? port, ServerType type) async { - stdout.writeln("Starting backend server..."); - switch(type){ - case ServerType.local: - final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort); - if(result == null){ - throw Exception("Local backend server is not running"); - } - - stdout.writeln("Detected local backend server"); - return true; - case ServerType.embedded: - stdout.writeln("Starting an embedded server..."); - await server.startEmbeddedBackend(false); - var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort); - if(result == null){ - throw Exception("Cannot start embedded server"); - } - - return true; - case ServerType.remote: - if(host == null){ - throw Exception("Missing host for remote server"); - } - - if(port == null){ - throw Exception("Missing host for remote server"); - } - - stdout.writeln("Starting a reverse proxy to $host:$port"); - return await _changeReverseProxyState(host, port) != null; - } -} - -Future _changeReverseProxyState(String host, int port) async { - try{ - var uri = await pingBackend(host, port); - if(uri == null){ - return null; - } - - return await server.startRemoteBackendProxy(uri); - }catch(error){ - throw Exception("Cannot start reverse proxy"); - } -} - -void kill() async { - try { - await Process.run("taskkill", ["/f", "/im", "FortniteLauncher.exe"]); - await Process.run("taskkill", ["/f", "/im", "FortniteClient-Win64-Shipping_EAC.exe"]); - }catch(_){ - - } -} diff --git a/cli/pubspec.yaml b/cli/pubspec.yaml index db03abc..ff6acc5 100644 --- a/cli/pubspec.yaml +++ b/cli/pubspec.yaml @@ -1,17 +1,18 @@ name: reboot_cli description: Command Line Interface for Project Reboot -version: "1.0.0" +version: "10.0.7" publish_to: 'none' environment: - sdk: ">=2.19.0 <=3.3.4" + sdk: ">=2.19.0 <=3.5.3" dependencies: reboot_common: path: ./../common - args: ^2.3.1 - process_run: ^0.13.1 + tint: ^2.0.1 + interact: ^2.2.0 + args: ^2.6.0 dependency_overrides: xml: ^6.3.0 diff --git a/common/lib/src/model/server_result.dart b/common/lib/src/model/server_result.dart index 348c9f5..8ca28d3 100644 --- a/common/lib/src/model/server_result.dart +++ b/common/lib/src/model/server_result.dart @@ -1,9 +1,12 @@ +import 'dart:io'; + class ServerResult { final ServerResultType type; + final ServerImplementation? implementation; final Object? error; final StackTrace? stackTrace; - ServerResult(this.type, {this.error, this.stackTrace}); + ServerResult(this.type, {this.implementation, this.error, this.stackTrace}); @override String toString() { @@ -11,22 +14,32 @@ class ServerResult { } } +class ServerImplementation { + final Process? process; + final HttpServer? server; + + ServerImplementation({this.process, this.server}); +} + enum ServerResultType { starting, + startMissingHostError, + startMissingPortError, + startIllegalPortError, + startFreeingPort, + startFreePortSuccess, + startFreePortError, + startPingingRemote, + startPingingLocal, + startPingError, + startedImplementation, startSuccess, startError, stopping, stopSuccess, - stopError, - missingHostError, - missingPortError, - illegalPortError, - freeingPort, - freePortSuccess, - freePortError, - pingingRemote, - pingingLocal, - pingError; + stopError; + + bool get isStart => name.contains("start"); bool get isError => name.contains("Error"); diff --git a/common/lib/src/util/backend.dart b/common/lib/src/util/backend.dart index 12d20e3..8f620aa 100644 --- a/common/lib/src/util/backend.dart +++ b/common/lib/src/util/backend.dart @@ -15,6 +15,122 @@ 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* { + Process? process; + HttpServer? server; + try { + host = host.trim(); + port = port.trim(); + if(type != ServerType.local || port != kDefaultBackendPort.toString()) { + yield ServerResult(ServerResultType.starting); + } + + if (host.isEmpty) { + yield ServerResult(ServerResultType.startMissingHostError); + return; + } + + if (port.isEmpty) { + yield ServerResult(ServerResultType.startMissingPortError); + return; + } + + final portNumber = int.tryParse(port); + if (portNumber == null) { + yield ServerResult(ServerResultType.startIllegalPortError); + return; + } + + if ((type != ServerType.local || port != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { + yield ServerResult(ServerResultType.startFreeingPort); + final result = await freeBackendPort(); + if(!result) { + yield ServerResult(ServerResultType.startFreePortError); + return; + } + + yield ServerResult(ServerResultType.startFreePortSuccess); + } + + switch(type){ + case ServerType.embedded: + process = await startEmbeddedBackend(detached, onError: onError); + yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(process: process)); + break; + case ServerType.remote: + yield ServerResult(ServerResultType.startPingingRemote); + final uriResult = await pingBackend(host, portNumber); + if(uriResult == null) { + yield ServerResult(ServerResultType.startPingError); + return; + } + + server = await startRemoteBackendProxy(uriResult); + yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server)); + break; + case ServerType.local: + if(portNumber != kDefaultBackendPort) { + yield ServerResult(ServerResultType.startPingingLocal); + final uriResult = await pingBackend(kDefaultBackendHost, portNumber); + if(uriResult == null) { + yield ServerResult(ServerResultType.startPingError); + return; + } + + server = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$port")); + yield ServerResult(ServerResultType.startedImplementation, implementation: ServerImplementation(server: server)); + } + break; + } + + yield ServerResult(ServerResultType.startPingingLocal); + final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort); + if(uriResult == null) { + yield ServerResult(ServerResultType.startPingError); + process?.kill(ProcessSignal.sigterm); + server?.close(force: true); + return; + } + + yield ServerResult(ServerResultType.startSuccess); + }catch(error, stackTrace) { + yield ServerResult( + ServerResultType.startError, + error: error, + stackTrace: stackTrace + ); + process?.kill(ProcessSignal.sigterm); + server?.close(force: true); + } +} + +Stream stopBackend({required ServerType type, required ServerImplementation? implementation}) async* { + yield ServerResult(ServerResultType.stopping); + try{ + switch(type){ + case ServerType.embedded: + final process = implementation?.process; + if(process != null) { + Process.killPid(process.pid, ProcessSignal.sigterm); + } + break; + case ServerType.remote: + await implementation?.server?.close(force: true); + break; + case ServerType.local: + await implementation?.server?.close(force: true); + break; + } + yield ServerResult(ServerResultType.stopSuccess); + }catch(error, stackTrace){ + yield ServerResult( + ServerResultType.stopError, + error: error, + stackTrace: stackTrace + ); + } +} + Future startEmbeddedBackend(bool detached, {void Function(String)? onError}) async { final process = await startProcess( executable: backendStartExecutable, @@ -25,7 +141,9 @@ Future startEmbeddedBackend(bool detached, {void Function(String)? onEr log("[BACKEND] Error: $error"); onError?.call(error); }); - process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); + if(!detached) { + process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode")); + } return process; } diff --git a/common/lib/src/util/dll.dart b/common/lib/src/util/dll.dart index bbfa5c6..7a4de11 100644 --- a/common/lib/src/util/dll.dart +++ b/common/lib/src/util/dll.dart @@ -7,11 +7,17 @@ import 'package:reboot_common/common.dart'; final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll"); final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll"); + const String kRebootBelowS20DownloadUrl = "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip"; const String kRebootAboveS20DownloadUrl = "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip"; +const String _kRebootBelowS20FallbackDownloadUrl = + "https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip"; +const String _kRebootAboveS20FallbackDownloadUrl = + "https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip"; + Future hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async { final lastUpdate = await _getLastUpdate(lastUpdateMs); final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists(); @@ -45,12 +51,15 @@ Future downloadDependency(InjectableDll dll, String outputPath) async { await output.writeAsBytes(response.bodyBytes, flush: true); } -Future downloadRebootDll(File file, String url) async { +Future downloadRebootDll(File file, String url, bool aboveS20) async { Directory? outputDir; try { - final response = await http.get(Uri.parse(url)); + var response = await http.get(Uri.parse(url)); if(response.statusCode != 200) { - throw Exception("Cannot download reboot.zip: status code ${response.statusCode}"); + response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl)); + if(response.statusCode != 200) { + throw Exception("status code ${response.statusCode}"); + } } outputDir = await installationDirectory.createTemp("reboot_out"); diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index 714a0b4..ad881c2 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -168,20 +168,6 @@ bool resume(int pid) { } } - -Future watchProcess(int pid) => Isolate.run(() { - final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid); - if (processHandle == 0) { - return; - } - - try { - WaitForSingleObject(processHandle, INFINITE); - }finally { - CloseHandle(processHandle); - } -}); - List createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) { log("[PROCESS] Generating reboot args"); if(password.isEmpty) { @@ -292,16 +278,8 @@ final class _ExtendedProcess implements Process { _stdout = attached ? delegate.stdout.asBroadcastStream() : null, _stderr = attached ? delegate.stderr.asBroadcastStream() : null; - @override - Future get exitCode { - try { - return _delegate.exitCode; - }catch(_) { - return watchProcess(_delegate.pid) - .then((_) => -1); - } - } + Future get exitCode => _delegate.exitCode; @override bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal); diff --git a/gui/dependencies/dlls/RebootFallback.zip b/gui/dependencies/dlls/RebootFallback.zip new file mode 100644 index 0000000..6026edf Binary files /dev/null and b/gui/dependencies/dlls/RebootFallback.zip differ diff --git a/gui/dependencies/dlls/RebootS20Fallback.zip b/gui/dependencies/dlls/RebootS20Fallback.zip new file mode 100644 index 0000000..df8b3be Binary files /dev/null and b/gui/dependencies/dlls/RebootS20Fallback.zip differ diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index bcfbae9..7c57c8b 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -128,7 +128,7 @@ "importVersionDescription": "Import a new version of Fortnite into the launcher", "addLocalBuildName": "Add a version from this PC's local storage", "addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work", - "addVersion": "Add version", + "addVersion": "New version", "downloadBuildName": "Download any version from the cloud", "downloadBuildDescription": "Download any Fortnite build easily from the cloud", "downloadBuildContent": "Download build", diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index 207cd38..1c4f983 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -35,10 +35,8 @@ class BackendController extends GetxController { late final RxBool started; late final RxBool detached; late final List _infoBars; - StreamSubscription? worker; - int? embeddedProcessPid; - HttpServer? localServer; - HttpServer? remoteServer; + StreamSubscription? _worker; + ServerImplementation? _implementation; BackendController() { _storage = appWithNoStorage ? null : GetStorage(storageName); @@ -48,11 +46,6 @@ class BackendController extends GetxController { host.text = _readHost(); port.text = _readPort(); _storage?.write("type", value.index); - if (!started.value) { - return; - } - - stop(); }); host = TextEditingController(text: _readHost()); host.addListener(() => @@ -148,18 +141,27 @@ class BackendController extends GetxController { detached.value = false; } - Future toggleInteractive() async { + Future toggle() { + if(started.value) { + return stop(interactive: true); + }else { + return start(interactive: true); + } + } + + Future start({required bool interactive}) async { + if(started.value) { + return true; + } + _cancel(); - final stream = started.value ? stop() : start( - onExit: () { - _cancel(); - _showRebootInfoBar( - translations.backendProcessError, - severity: InfoBarSeverity.error - ); - }, + final stream = startBackend( + type: type.value, + host: host.text, + port: port.text, + detached: detached.value, onError: (errorMessage) { - _cancel(); + stop(interactive: false); _showRebootInfoBar( translations.backendErrorMessage, severity: InfoBarSeverity.error, @@ -173,265 +175,203 @@ class BackendController extends GetxController { ); final completer = Completer(); InfoBarEntry? entry; - worker = stream.listen((event) { + _worker = stream.listen((event) { entry?.close(); - entry = _handeEvent(event); + entry = _handeEvent(event, interactive); if(event.type.isError) { completer.complete(false); }else if(event.type.isSuccess) { completer.complete(true); } }); - return await completer.future; } - Stream start({required void Function() onExit, required void Function(String) onError}) async* { - try { - if(started.value) { - return; - } - - final serverType = type.value; - final hostData = this.host.text.trim(); - final portData = this.port.text.trim(); - started.value = true; - if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) { - yield ServerResult(ServerResultType.starting); - } - - if (hostData.isEmpty) { - yield ServerResult(ServerResultType.missingHostError); - started.value = false; - return; - } - - if (portData.isEmpty) { - yield ServerResult(ServerResultType.missingPortError); - started.value = false; - return; - } - - final portNumber = int.tryParse(portData); - if (portNumber == null) { - yield ServerResult(ServerResultType.illegalPortError); - started.value = false; - return; - } - - if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { - yield ServerResult(ServerResultType.freeingPort); - final result = await freeBackendPort(); - yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); - if(!result) { - started.value = false; - return; - } - } - - switch(serverType){ - case ServerType.embedded: - final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) { - if(started.value) { - started.value = false; - onError(errorMessage); - } - }); - watchProcess(process.pid).then((_) { - if(started.value) { - started.value = false; - onExit(); - } - }); - embeddedProcessPid = process.pid; - break; - case ServerType.remote: - yield ServerResult(ServerResultType.pingingRemote); - final uriResult = await pingBackend(hostData, portNumber); - if(uriResult == null) { - yield ServerResult(ServerResultType.pingError); - started.value = false; - return; - } - - remoteServer = await startRemoteBackendProxy(uriResult); - break; - case ServerType.local: - if(portNumber != kDefaultBackendPort) { - yield ServerResult(ServerResultType.pingingLocal); - final uriResult = await pingBackend(kDefaultBackendHost, portNumber); - if(uriResult == null) { - yield ServerResult(ServerResultType.pingError); - started.value = false; - return; - } - - localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData")); - }else { - // If the local server is running on port 3551 there is no reverse proxy running - // We only need to check if everything is working - started.value = false; - } - - break; - } - - yield ServerResult(ServerResultType.pingingLocal); - final uriResult = await pingBackend(kDefaultBackendHost, kDefaultBackendPort); - if(uriResult == null) { - yield ServerResult(ServerResultType.pingError); - remoteServer?.close(force: true); - localServer?.close(force: true); - started.value = false; - return; - } - - yield ServerResult(ServerResultType.startSuccess); - }catch(error, stackTrace) { - yield ServerResult( - ServerResultType.startError, - error: error, - stackTrace: stackTrace - ); - remoteServer?.close(force: true); - localServer?.close(force: true); - started.value = false; - } - } - - Stream stop() async* { + Future stop({required bool interactive}) async { if(!started.value) { - return; + return true; } - yield ServerResult(ServerResultType.stopping); - started.value = false; - try{ - switch(type()){ - case ServerType.embedded: - final embeddedProcessPid = this.embeddedProcessPid; - if(embeddedProcessPid != null) { - Process.killPid(embeddedProcessPid, ProcessSignal.sigterm); - this.embeddedProcessPid = null; - } - break; - case ServerType.remote: - await remoteServer?.close(force: true); - remoteServer = null; - break; - case ServerType.local: - await localServer?.close(force: true); - localServer = null; - break; + _cancel(); + final stream = stopBackend( + type: type.value, + implementation: _implementation + ); + final completer = Completer(); + InfoBarEntry? entry; + _worker = stream.listen((event) { + entry?.close(); + entry = _handeEvent(event, interactive); + if(event.type.isError) { + completer.complete(false); + }else if(event.type.isSuccess) { + completer.complete(true); } - yield ServerResult(ServerResultType.stopSuccess); - }catch(error, stackTrace){ - yield ServerResult( - ServerResultType.stopError, - error: error, - stackTrace: stackTrace - ); - started.value = true; - } + }); + return await completer.future; } void _cancel() { - worker?.cancel(); // Do not await or it will hang + _worker?.cancel(); // Do not await or it will hang _infoBars.forEach((infoBar) => infoBar.close()); _infoBars.clear(); } - InfoBarEntry _handeEvent(ServerResult event) { - log("[BACKEND] Handling event: $event"); + InfoBarEntry? _handeEvent(ServerResult event, bool interactive) { + log("[BACKEND] Handling event: $event (interactive: $interactive, start: ${event.type.isStart}, error: ${event.type.isError})"); + started.value = event.type.isStart && !event.type.isError; switch (event.type) { case ServerResultType.starting: - return _showRebootInfoBar( - translations.startingServer, - severity: InfoBarSeverity.info, - loading: true, - duration: null - ); + if(interactive) { + return _showRebootInfoBar( + translations.startingServer, + severity: InfoBarSeverity.info, + loading: true, + duration: null + ); + }else { + return null; + } case ServerResultType.startSuccess: - return _showRebootInfoBar( - type.value == ServerType.local ? translations.checkedServer : translations.startedServer, - severity: InfoBarSeverity.success - ); + if(interactive) { + return _showRebootInfoBar( + type.value == ServerType.local ? translations.checkedServer : translations.startedServer, + severity: InfoBarSeverity.success + ); + }else { + return null; + } case ServerResultType.startError: - return _showRebootInfoBar( - type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration - ); + if(interactive) { + return _showRebootInfoBar( + type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration + ); + }else { + return null; + } case ServerResultType.stopping: - return _showRebootInfoBar( - translations.stoppingServer, - severity: InfoBarSeverity.info, - loading: true, - duration: null - ); + if(interactive) { + return _showRebootInfoBar( + translations.stoppingServer, + severity: InfoBarSeverity.info, + loading: true, + duration: null + ); + }else { + return null; + } case ServerResultType.stopSuccess: - return _showRebootInfoBar( - translations.stoppedServer, - severity: InfoBarSeverity.success - ); + if(interactive) { + return _showRebootInfoBar( + translations.stoppedServer, + severity: InfoBarSeverity.success + ); + }else { + return null; + } case ServerResultType.stopError: - return _showRebootInfoBar( - translations.stopServerError(event.error ?? translations.unknownError), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration - ); - case ServerResultType.missingHostError: - return _showRebootInfoBar( - translations.missingHostNameError, - severity: InfoBarSeverity.error - ); - case ServerResultType.missingPortError: - return _showRebootInfoBar( - translations.missingPortError, - severity: InfoBarSeverity.error - ); - case ServerResultType.illegalPortError: - return _showRebootInfoBar( - translations.illegalPortError, - severity: InfoBarSeverity.error - ); - case ServerResultType.freeingPort: - return _showRebootInfoBar( - translations.freeingPort, - loading: true, - duration: null - ); - case ServerResultType.freePortSuccess: - return _showRebootInfoBar( - translations.freedPort, - severity: InfoBarSeverity.success, - duration: infoBarShortDuration - ); - case ServerResultType.freePortError: - return _showRebootInfoBar( - translations.freePortError(event.error ?? translations.unknownError), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration - ); - case ServerResultType.pingingRemote: - return _showRebootInfoBar( - translations.pingingServer(ServerType.remote.name), - severity: InfoBarSeverity.info, - loading: true, - duration: null - ); - case ServerResultType.pingingLocal: - return _showRebootInfoBar( - translations.pingingServer(type.value.name), - severity: InfoBarSeverity.info, - loading: true, - duration: null - ); - case ServerResultType.pingError: - return _showRebootInfoBar( - translations.pingError(type.value.name), - severity: InfoBarSeverity.error - ); + if(interactive) { + return _showRebootInfoBar( + translations.stopServerError(event.error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration + ); + }else { + return null; + } + case ServerResultType.startMissingHostError: + if(interactive) { + return _showRebootInfoBar( + translations.missingHostNameError, + severity: InfoBarSeverity.error + ); + }else { + return null; + } + case ServerResultType.startMissingPortError: + if(interactive) { + return _showRebootInfoBar( + translations.missingPortError, + severity: InfoBarSeverity.error + ); + }else { + return null; + } + case ServerResultType.startIllegalPortError: + if(interactive) { + return _showRebootInfoBar( + translations.illegalPortError, + severity: InfoBarSeverity.error + ); + }else { + return null; + } + case ServerResultType.startFreeingPort: + if(interactive) { + return _showRebootInfoBar( + translations.freeingPort, + loading: true, + duration: null + ); + }else { + return null; + } + case ServerResultType.startFreePortSuccess: + if(interactive) { + return _showRebootInfoBar( + translations.freedPort, + severity: InfoBarSeverity.success, + duration: infoBarShortDuration + ); + }else { + return null; + } + case ServerResultType.startFreePortError: + if(interactive) { + return _showRebootInfoBar( + translations.freePortError(event.error ?? translations.unknownError), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration + ); + }else { + return null; + } + case ServerResultType.startPingingRemote: + if(interactive) { + return _showRebootInfoBar( + translations.pingingServer(ServerType.remote.name), + severity: InfoBarSeverity.info, + loading: true, + duration: null + ); + }else { + return null; + } + case ServerResultType.startPingingLocal: + if(interactive) { + return _showRebootInfoBar( + translations.pingingServer(type.value.name), + severity: InfoBarSeverity.info, + loading: true, + duration: null + ); + }else { + return null; + } + case ServerResultType.startPingError: + if(interactive) { + return _showRebootInfoBar( + translations.pingError(type.value.name), + severity: InfoBarSeverity.error + ); + }else { + return null; + } + case ServerResultType.startedImplementation: + _implementation = event.implementation; + return null; } } @@ -597,4 +537,11 @@ class BackendController extends GetxController { } return result; } + + Future restart() async { + if(started.value) { + await stop(interactive: false); + await start(interactive: true); + } + } } \ No newline at end of file diff --git a/gui/lib/src/controller/dll_controller.dart b/gui/lib/src/controller/dll_controller.dart index 0b54843..06b25b1 100644 --- a/gui/lib/src/controller/dll_controller.dart +++ b/gui/lib/src/controller/dll_controller.dart @@ -27,7 +27,6 @@ class DllController extends GetxController { late final RxBool customGameServer; late final RxnInt timestamp; late final Rx status; - InfoBarEntry? infoBarEntry; DllController() { _storage = appWithNoStorage ? null : GetStorage(storageName); @@ -75,6 +74,7 @@ class DllController extends GetxController { } Future updateGameServerDll({bool force = false, bool silent = false}) async { + InfoBarEntry? infoBarEntry; try { if(customGameServer.value) { status.value = UpdateStatus.success; @@ -100,8 +100,8 @@ class DllController extends GetxController { } await Future.wait( [ - downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text), - downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text), + downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text, false), + downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text, true), Future.delayed(const Duration(seconds: 1)) ], eagerError: false diff --git a/gui/lib/src/widget/game/game_start_button.dart b/gui/lib/src/widget/game/game_start_button.dart index 18c1a0e..e3ca230 100644 --- a/gui/lib/src/widget/game/game_start_button.dart +++ b/gui/lib/src/widget/game/game_start_button.dart @@ -110,7 +110,7 @@ class _LaunchButtonState extends State { } log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); - final backendResult = _backendController.started() || await _backendController.toggleInteractive(); + final backendResult = _backendController.started() || await _backendController.toggle(); if(!backendResult){ log("[${host ? 'HOST' : 'GAME'}] Cannot start backend"); _onStop( @@ -526,7 +526,7 @@ class _LaunchButtonState extends State { } await _operation?.cancel(); _operation = null; - _backendController.stop(); + _backendController.stop(interactive: false); } host = host ?? widget.host; @@ -629,7 +629,7 @@ class _LaunchButtonState extends State { ); break; case _StopReason.tokenError: - _backendController.stop(); + _backendController.stop(interactive: false); showRebootInfoBar( translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")), severity: InfoBarSeverity.error, diff --git a/gui/lib/src/widget/page/backend_page.dart b/gui/lib/src/widget/page/backend_page.dart index f661fdf..7337096 100644 --- a/gui/lib/src/widget/page/backend_page.dart +++ b/gui/lib/src/widget/page/backend_page.dart @@ -162,7 +162,12 @@ class _BackendPageState extends RebootPageState { key: backendDetachedOverlayTargetKey, child: ToggleSwitch( checked: _backendController.detached(), - onChanged: (value) => _backendController.detached.value = value + onChanged: (value) async { + _backendController.detached.value = value; + if(_backendController.started.value) { + await _backendController.restart(); + } + } ), ), ], diff --git a/gui/lib/src/widget/page/home_page.dart b/gui/lib/src/widget/page/home_page.dart index 4372ce1..033a8af 100644 --- a/gui/lib/src/widget/page/home_page.dart +++ b/gui/lib/src/widget/page/home_page.dart @@ -75,6 +75,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA lastPage = index; _pageController.jumpToPage(index); + pagesController.add(null); }); } @@ -152,7 +153,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA try { if(_backendController.started.value) { - await _backendController.toggleInteractive(); + await _backendController.toggle(); } }catch(error) { log("[BACKEND] Cannot stop backend on exit: $error"); @@ -524,36 +525,6 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA ); } - Widget get _backButton => StreamBuilder( - stream: pagesController.stream, - builder: (context, _) => Button( - style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.symmetric( - vertical: 12.0, - horizontal: 16.0 - )), - backgroundColor: WidgetStateProperty.all(Colors.transparent), - shape: WidgetStateProperty.all(Border()) - ), - onPressed: appStack.isEmpty && !inDialog ? null : () { - if(inDialog) { - Navigator.of(appNavigatorKey.currentContext!).pop(); - }else { - final lastPage = appStack.removeLast(); - pageStack.remove(lastPage); - if (lastPage is int) { - hitBack = true; - pageIndex.value = lastPage; - } else { - Navigator.of(pageKey.currentContext!).pop(); - } - } - pagesController.add(null); - }, - child: const Icon(FluentIcons.back, size: 12.0), - ) - ); - Widget get _autoSuggestBox => Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, diff --git a/gui/lib/src/widget/page/settings_page.dart b/gui/lib/src/widget/page/settings_page.dart index 14e68e6..29b5df3 100644 --- a/gui/lib/src/widget/page/settings_page.dart +++ b/gui/lib/src/widget/page/settings_page.dart @@ -1,3 +1,6 @@ +import 'dart:math'; + +import 'package:async/async.dart'; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; @@ -36,6 +39,7 @@ class SettingsPage extends RebootPage { class _SettingsPageState extends RebootPageState { final SettingsController _settingsController = Get.find(); final DllController _dllController = Get.find(); + int? _downloadFromMirrorId; @override Widget? get button => null; @@ -115,7 +119,6 @@ class _SettingsPageState extends RebootPageState { } _dllController.customGameServer.value = entry.key; - _dllController.infoBarEntry?.close(); if(!entry.key) { _dllController.updateGameServerDll( force: true @@ -141,11 +144,7 @@ class _SettingsPageState extends RebootPageState { child: TextFormBox( placeholder: translations.settingsServerMirrorPlaceholder, controller: _dllController.beforeS20Mirror, - onChanged: (value) { - if(Uri.tryParse(value) != null) { - _dllController.updateGameServerDll(force: true); - } - }, + onChanged: _scheduleMirrorDownload ), ), const SizedBox(width: 8.0), @@ -194,6 +193,24 @@ class _SettingsPageState extends RebootPageState { } }); + void _scheduleMirrorDownload(String value) async { + if(_downloadFromMirrorId != null) { + return; + } + + if(Uri.tryParse(value) == null) { + return; + } + + final id = Random.secure().nextInt(1000000); + _downloadFromMirrorId = id; + await Future.delayed(const Duration(seconds: 2)); + if(_downloadFromMirrorId == id) { + await _dllController.updateGameServerDll(force: true); + } + _downloadFromMirrorId = null; + } + Widget get _internalFilesNewServerSource => Obx(() { if(!_dllController.customGameServer.value) { return SettingTile( @@ -209,11 +226,7 @@ class _SettingsPageState extends RebootPageState { child: TextFormBox( placeholder: translations.settingsServerMirrorPlaceholder, controller: _dllController.aboveS20Mirror, - onChanged: (value) { - if(Uri.tryParse(value) != null) { - _dllController.updateGameServerDll(force: true); - } - }, + onChanged: _scheduleMirrorDownload ), ), const SizedBox(width: 8.0), @@ -273,7 +286,6 @@ class _SettingsPageState extends RebootPageState { text: Text(entry.text), onPressed: () { _dllController.timer.value = entry; - _dllController.infoBarEntry?.close(); _dllController.updateGameServerDll( force: true ); diff --git a/gui/lib/src/widget/server/server_start_button.dart b/gui/lib/src/widget/server/server_start_button.dart index 9aa398b..6f59073 100644 --- a/gui/lib/src/widget/server/server_start_button.dart +++ b/gui/lib/src/widget/server/server_start_button.dart @@ -45,7 +45,7 @@ class _ServerButtonState extends State { builder: (context, snapshot) => Obx(() => Text(_buttonText)) ), ), - onPressed: () => _controller.toggleInteractive() + onPressed: () => _controller.toggle() ) ) ); diff --git a/gui/lib/src/widget/server/server_type_selector.dart b/gui/lib/src/widget/server/server_type_selector.dart index 82fdf8e..2b5e860 100644 --- a/gui/lib/src/widget/server/server_type_selector.dart +++ b/gui/lib/src/widget/server/server_type_selector.dart @@ -32,18 +32,16 @@ class _ServerTypeSelectorState extends State { )); } - MenuFlyoutItem _createItem(ServerType type) { - return MenuFlyoutItem( - text: Text(type.label), - onPressed: () async { - _controller.stop(); - _controller.type.value = type; - } - ); - } + MenuFlyoutItem _createItem(ServerType type) => MenuFlyoutItem( + text: Text(type.label), + onPressed: () async { + await _controller.stop(interactive: false); + _controller.type.value = type; + } + ); } -extension ServerTypeExtension on ServerType { +extension _ServerTypeExtension on ServerType { String get label { return this == ServerType.embedded ? translations.embedded : this == ServerType.remote ? translations.remote diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 2462172..efeb2d4 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.6" +version: "10.0.7" publish_to: 'none' @@ -43,6 +43,7 @@ dependencies: # Async helpers async: ^2.11.0 sync: ^0.3.0 + synchronized: ^3.3.0+3 # State management get: ^4.6.6