From ef7f34e0e3549c7a521cabaf8875fe55d9ba1c09 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 25 Oct 2022 19:42:09 +0200 Subject: [PATCH] ip for click to play --- lib/cli.dart | 358 ++------------------ lib/main.dart | 8 +- lib/src/controller/server_controller.dart | 46 ++- lib/src/controller/settings_controller.dart | 27 +- lib/src/dialog/add_local_version.dart | 17 +- lib/src/dialog/add_server_version.dart | 56 +-- lib/src/dialog/dialog.dart | 10 +- lib/src/dialog/dialog_button.dart | 4 +- lib/src/dialog/server_dialogs.dart | 124 ++++--- lib/src/model/fortnite_version.dart | 6 +- lib/src/page/info_page.dart | 11 +- lib/src/page/launcher_page.dart | 27 +- lib/src/page/server_page.dart | 12 +- lib/src/page/settings_page.dart | 100 +++--- lib/src/util/checks.dart | 8 + lib/src/util/os.dart | 19 -- lib/src/util/patcher.dart | 66 ++-- lib/src/util/reboot.dart | 4 +- lib/src/util/server.dart | 54 ++- lib/src/widget/home/launch_button.dart | 113 +++--- lib/src/widget/home/version_selector.dart | 33 +- lib/src/widget/os/file_selector.dart | 85 ----- lib/src/widget/shared/smart_input.dart | 10 +- lib/src/widget/shared/warning_info.dart | 28 -- pubspec.yaml | 6 +- release/release.bat | 2 +- 26 files changed, 509 insertions(+), 725 deletions(-) delete mode 100644 lib/src/widget/os/file_selector.dart delete mode 100644 lib/src/widget/shared/warning_info.dart diff --git a/lib/cli.dart b/lib/cli.dart index 49ef029..27b5984 100644 --- a/lib/cli.dart +++ b/lib/cli.dart @@ -1,102 +1,46 @@ -import 'dart:collection'; -import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; -import 'package:process_run/shell.dart'; +import 'package:reboot_launcher/src/cli/compatibility.dart'; +import 'package:reboot_launcher/src/cli/config.dart'; +import 'package:reboot_launcher/src/cli/game.dart'; +import 'package:reboot_launcher/src/cli/reboot.dart'; +import 'package:reboot_launcher/src/cli/server.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/model/game_type.dart'; -import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/reboot.dart'; -import 'package:reboot_launcher/src/util/server.dart'; -import 'package:shelf_proxy/shelf_proxy.dart'; -import 'package:win32_suspend_process/win32_suspend_process.dart'; -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:win32/win32.dart'; -import 'package:shelf/shelf_io.dart' as shelf_io; -import 'package:http/http.dart' as http; - -// Needed because binaries can't be loaded in any other way -const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll"; -const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll"; - -Process? _gameProcess; -Process? _eacProcess; -Process? _launcherProcess; void main(List args){ handleCLI(args); } -Future> _getControllerJson(String name) async { - var folder = await _getWindowsPath(FOLDERID_Documents); - if(folder == null){ - throw Exception("Missing documents folder"); - } - - var file = File("$folder/$name.gs"); - if(!file.existsSync()){ - return HashMap(); - } - - return jsonDecode(file.readAsStringSync()); -} - -Future _getWindowsPath(String folderID) { - final Pointer> pathPtrPtr = calloc>(); - final Pointer knownFolderID = calloc()..ref.setGUID(folderID); - - try { - final int hr = SHGetKnownFolderPath( - knownFolderID, - KF_FLAG_DEFAULT, - NULL, - pathPtrPtr, - ); - - if (FAILED(hr)) { - if (hr == E_INVALIDARG || hr == E_FAIL) { - throw WindowsException(hr); - } - return Future.value(); - } - - final String path = pathPtrPtr.value.toDartString(); - return Future.value(path); - } finally { - calloc.free(pathPtrPtr); - calloc.free(knownFolderID); - } -} - Future handleCLI(List args) async { stdout.writeln("Reboot Launcher"); stdout.writeln("Wrote by Auties00"); - stdout.writeln("Version 3.13"); + stdout.writeln("Version 4.4"); - _killOld(); + kill(); - var gameJson = await _getControllerJson("game"); - var serverJson = await _getControllerJson("server"); - var settingsJson = await _getControllerJson("settings"); - var versions = _getVersions(gameJson); + var gameJson = await getControllerJson("game"); + var serverJson = await getControllerJson("server"); + var settingsJson = await getControllerJson("settings"); + var versions = getVersions(gameJson); var parser = ArgParser() ..addCommand("list") ..addCommand("launch") ..addOption("version", defaultsTo: gameJson["version"]) ..addOption("username") - ..addOption("server-type", allowed: _getServerTypes(), defaultsTo: _getDefaultServerType(serverJson)) + ..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson)) ..addOption("server-host") ..addOption("server-port") ..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path) - ..addOption("type", allowed: _getTypes(), defaultsTo: _getDefaultType(gameJson)) + ..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson)) ..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true) - ..addFlag("log", defaultsTo: false); + ..addFlag("log", defaultsTo: false) + ..addFlag("memory-fix", defaultsTo: false, negatable: true); var result = parser.parse(args); if (result.command?.name == "list") { stdout.writeln("Versions list: "); @@ -106,220 +50,44 @@ Future handleCLI(List args) async { } var dll = result["dll"]; - var type = _getType(result); + var type = getGameType(result); var username = result["username"]; username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"]; var verbose = result["log"]; - var dummyVersion = _createVersion(gameJson["version"], result["version"], versions); - await _updateDLLs(); + var dummyVersion = _createVersion(gameJson["version"], result["version"], result["memory-fix"], versions); + await downloadRequiredDLLs(); if(result["update"]) { stdout.writeln("Updating reboot dll..."); await downloadRebootDll(0); } stdout.writeln("Launching game(type: ${type.name})..."); - await _startLauncherProcess(dummyVersion); - if (result["type"] == "headless_server") { - if(dummyVersion.executable == null){ - throw Exception("Missing game executable at: ${dummyVersion.location.path}"); - } - - await patch(dummyVersion.executable!); + if(dummyVersion.executable == null){ + throw Exception("Missing game executable at: ${dummyVersion.location.path}"); } - var serverType = _getServerType(result); + if (result["type"] == "headless_server") { + await patchHeadless(dummyVersion.executable!); + }else if(result["type"] == "client"){ + await patchMatchmaking(dummyVersion.executable!); + } + + var serverType = getServerType(result); var host = result["server-host"] ?? serverJson["${serverType.id}_host"]; var port = result["server-port"] ?? serverJson["${serverType.id}_port"]; - var started = await _startServerIfNeeded(host, port, serverType); + var started = await startServer(host, port, serverType); if(!started){ stderr.writeln("Cannot start server!"); return; } - await _startGameProcess(username, type, verbose, dll, dummyVersion); + await startGame(username, type, verbose, dll, dummyVersion); } -void _killOld() async { - var shell = Shell( - commandVerbose: false, - commentVerbose: false, - verbose: false - ); - try { - await shell.run("taskkill /f /im FortniteLauncher.exe"); - await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe"); - }catch(_){ - - } -} - -Iterable _getTypes() => GameType.values.map((entry) => entry.id); - -Iterable _getServerTypes() => ServerType.values.map((entry) => entry.id); - -String _getDefaultServerType(Map json) { - var type = ServerType.values.elementAt(json["type"] ?? 0); - return type.id; -} - -GameType _getType(ArgResults result) { - var type = GameType.of(result["type"]); - if(type == null){ - throw Exception("Unknown game type: $result. Use --type only with ${_getTypes().join(", ")}"); - } - - return type; -} - -ServerType _getServerType(ArgResults result) { - var type = ServerType.of(result["server-type"]); - if(type == null){ - throw Exception("Unknown server type: $result. Use --server-type only with ${_getServerTypes().join(", ")}"); - } - - return type; -} - -String _getDefaultType(Map json){ - var type = GameType.values.elementAt(json["type"] ?? 0); - switch(type){ - case GameType.client: - return "client"; - case GameType.server: - return "server"; - case GameType.headlessServer: - return "headless_server"; - } -} - -Future _updateDLLs() async { - stdout.writeln("Downloading necessary components..."); - var consoleDll = await loadBinary("console.dll", true); - 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 = await loadBinary("cranium.dll", true); - if(!craniumDll.existsSync()){ - var response = await http.get(Uri.parse(_craniumDownload)); - if(response.statusCode != 200){ - throw Exception("Cannot download cranium.dll"); - } - - await craniumDll.writeAsBytes(response.bodyBytes); - } -} - -List _getVersions(Map gameJson) { - Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]"); - return iterable.map((entry) => FortniteVersion.fromJson(entry)) - .toList(); -} - -Future _startGameProcess(String? username, GameType type, bool verbose, String dll, FortniteVersion version) async { - var gamePath = version.executable?.path; - if (gamePath == null) { - throw Exception("${version.location - .path} no longer contains a Fortnite executable. Did you delete it?"); - } - - var hosting = type != GameType.client; - if (username == null) { - username = "Reboot${hosting ? 'Host' : 'Player'}"; - stdout.writeln("No username was specified, using $username by default. Use --username to specify one"); - } - - _gameProcess = await Process.start(gamePath, createRebootArgs(username, type == GameType.headlessServer)) - ..exitCode.then((_) => _onClose()) - ..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose)); - await _injectOrShowError("cranium.dll"); -} - -void _onClose() { - _kill(); - stdout.writeln("The game was closed"); - exit(0); -} - -Future _startLauncherProcess(FortniteVersion dummyVersion) async { - if (dummyVersion.launcher == null) { - return; - } - - _launcherProcess = await Process.start(dummyVersion.launcher!.path, []); - Win32Process(_launcherProcess!.pid).suspend(); -} - -Future _startServerIfNeeded(String? host, String? port, ServerType type) async { - stdout.writeln("Starting lawin server..."); - switch(type){ - case ServerType.local: - var result = await ping(host ?? "127.0.0.1", port ?? "3551"); - if(result == null){ - throw Exception("Local lawin server is not running"); - } - - stdout.writeln("Detected local lawin server"); - return true; - case ServerType.embedded: - stdout.writeln("Starting an embedded server..."); - return await _changeEmbeddedServerState(); - 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 _changeEmbeddedServerState() async { - - return true; -} - -Future _changeReverseProxyState(String host, String port) async { - host = host.trim(); - if(host.isEmpty){ - throw Exception("Missing host name"); - } - - port = port.trim(); - if(port.isEmpty){ - throw Exception("Missing port"); - } - - if(int.tryParse(port) == null){ - throw Exception("Invalid port, use only numbers"); - } - - try{ - var uri = await ping(host, port); - if(uri == null){ - return null; - } - - return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551); - }catch(error){ - throw Exception("Cannot start reverse proxy"); - } -} - - -FortniteVersion _createVersion(String? versionName, String? versionPath, List versions) { +FortniteVersion _createVersion(String? versionName, String? versionPath, bool memoryFix, List versions) { if (versionPath != null) { - return FortniteVersion(name: "dummy", location: Directory(versionPath)); + return FortniteVersion(name: "dummy", location: Directory(versionPath), memoryFix: memoryFix); } if(versionName != null){ @@ -332,68 +100,4 @@ FortniteVersion _createVersion(String? versionName, String? versionPath, List _injectOrShowError(String binary, [bool locate = true]) async { - if (_gameProcess == null) { - return; - } - - try { - stdout.writeln("Injecting $binary..."); - var dll = locate ? await loadBinary(binary, true) : File(binary); - if(!dll.existsSync()){ - throw Exception("Cannot inject $dll: missing file"); - } - - await injectDll(_gameProcess!.pid, dll.path); - } catch (exception) { - throw Exception("Cannot inject binary: $binary"); - } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 526e7a1..b7d1aa2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; @@ -12,6 +13,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/page/home_page.dart'; +import 'package:reboot_launcher/src/util/error.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:system_theme/system_theme.dart'; @@ -26,6 +28,7 @@ void main(List args) async { } WidgetsFlutterBinding.ensureInitialized(); + await SystemTheme.accentColor.load(); await GetStorage.init("game"); await GetStorage.init("server"); @@ -45,7 +48,10 @@ void main(List args) async { appWindow.show(); }); - runApp(const RebootApplication()); + runZonedGuarded(() => + runApp(const RebootApplication()), + (error, stack) => onError(error, stack, false) + ); } class RebootApplication extends StatefulWidget { diff --git a/lib/src/controller/server_controller.dart b/lib/src/controller/server_controller.dart index bf95751..d2a5d9f 100644 --- a/lib/src/controller/server_controller.dart +++ b/lib/src/controller/server_controller.dart @@ -3,8 +3,10 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:reboot_launcher/src/dialog/server_dialogs.dart'; import 'package:reboot_launcher/src/util/server.dart'; +import '../dialog/snackbar.dart'; import '../model/server_type.dart'; class ServerController extends GetxController { @@ -17,6 +19,7 @@ class ServerController extends GetxController { late final Rx type; late final RxBool warning; late RxBool started; + late int embeddedServerCounter; Process? embeddedServer; HttpServer? reverseProxy; @@ -36,7 +39,7 @@ class ServerController extends GetxController { if(value == ServerType.remote){ reverseProxy?.close(force: true); reverseProxy = null; - started(false); + started.value = false; return; } @@ -53,6 +56,8 @@ class ServerController extends GetxController { warning.listen((value) => _storage.write("lawin_value", value)); started = RxBool(false); + + embeddedServerCounter = 0; } String _readHost() { @@ -65,8 +70,9 @@ class ServerController extends GetxController { return _storage.read("${type.value.id}_port") ?? _serverPort; } - Future start() async { - var result = await checkServerPreconditions(host.text, port.text, type.value); + Future start(bool needsFreePort) async { + var lastCounter = ++embeddedServerCounter; + var result = await checkServerPreconditions(host.text, port.text, type.value, needsFreePort); if(result.type != ServerResultType.canStart){ return result; } @@ -74,7 +80,16 @@ class ServerController extends GetxController { try{ switch(type()){ case ServerType.embedded: - embeddedServer = await startEmbeddedServer(); + await _startEmbeddedServer(); + embeddedServer?.exitCode.then((value) async { + if (!started() || lastCounter != embeddedServerCounter) { + return; + } + + started.value = false; + await freeLawinPort(); + showUnexpectedError(); + }); break; case ServerType.remote: var uriResult = await result.uri!; @@ -97,21 +112,34 @@ class ServerController extends GetxController { ); } - var myself = await pingSelf(); + var myself = await pingSelf(port.text); if(myself == null){ return ServerResult( - type: ServerResultType.cannotPingServer + type: ServerResultType.cannotPingServer, + pid: embeddedServer?.pid ); } - started(true); return ServerResult( type: ServerResultType.started ); } + Future _startEmbeddedServer() async { + var result = await startEmbeddedServer(); + if(result != null){ + embeddedServer = result; + return; + } + + showMessage("The server is corrupted, trying to fix it"); + await serverLocation.parent.delete(recursive: true); + await downloadServerInteractive(true); + await _startEmbeddedServer(); + } + Future stop() async { - started(false); + started.value = false; try{ switch(type()){ case ServerType.embedded: @@ -125,7 +153,7 @@ class ServerController extends GetxController { } return true; }catch(_){ - started(true); + started.value = true; return false; } } diff --git a/lib/src/controller/settings_controller.dart b/lib/src/controller/settings_controller.dart index c0e2cc8..075bb87 100644 --- a/lib/src/controller/settings_controller.dart +++ b/lib/src/controller/settings_controller.dart @@ -1,13 +1,10 @@ -import 'dart:convert'; -import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; -import 'package:reboot_launcher/src/model/fortnite_version.dart'; -import 'package:reboot_launcher/src/model/game_type.dart'; +import 'package:ini/ini.dart'; import 'package:reboot_launcher/src/util/os.dart'; -import 'package:system_theme/system_theme.dart'; +import 'package:reboot_launcher/src/util/server.dart'; class SettingsController extends GetxController { late final GetStorage _storage; @@ -15,7 +12,7 @@ class SettingsController extends GetxController { late final TextEditingController rebootDll; late final TextEditingController consoleDll; late final TextEditingController craniumDll; - late final RxBool autoUpdate; + late final TextEditingController matchmakingIp; SettingsController() { _storage = GetStorage("settings"); @@ -23,9 +20,23 @@ class SettingsController extends GetxController { rebootDll = _createController("reboot", "reboot.dll"); consoleDll = _createController("console", "console.dll"); craniumDll = _createController("cranium", "cranium.dll"); - autoUpdate = RxBool(_storage.read("auto_update") ?? true); + matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1"); + matchmakingIp.addListener(() async { + var text = matchmakingIp.text; + _storage.write("ip", text); + if(await serverConfig.exists()){ + var config = Config.fromString(await serverConfig.readAsString()); + if(text.contains(":")){ + config.set("GameServer", "ip", text.substring(0, text.indexOf(":"))); + config.set("GameServer", "port", text.substring(text.indexOf(":") + 1)); + }else { + config.set("GameServer", "ip", text); + config.set("GameServer", "port", "7777"); + } - autoUpdate.listen((value) => _storage.write("auto_update", value)); + serverConfig.writeAsString(config.toString()); + } + }); } TextEditingController _createController(String key, String name) { diff --git a/lib/src/dialog/add_local_version.dart b/lib/src/dialog/add_local_version.dart index 056a255..0d4eeab 100644 --- a/lib/src/dialog/add_local_version.dart +++ b/lib/src/dialog/add_local_version.dart @@ -8,12 +8,14 @@ import 'package:reboot_launcher/src/dialog/dialog_button.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import '../util/checks.dart'; -import '../widget/os/file_selector.dart'; +import '../widget/shared/file_selector.dart'; +import '../widget/shared/smart_check_box.dart'; class AddLocalVersion extends StatelessWidget { final GameController _gameController = Get.find(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _gamePathController = TextEditingController(); + final CheckboxController _injectMemoryFixController = CheckboxController(); AddLocalVersion({Key? key}) : super(key: key); @@ -47,6 +49,15 @@ class AddLocalVersion extends StatelessWidget { folder: true ), + const SizedBox( + height: 16.0 + ), + + SmartCheckBox( + controller: _injectMemoryFixController, + content: const Text("Inject memory leak fix") + ), + const SizedBox(height: 8.0), ], ), @@ -61,7 +72,9 @@ class AddLocalVersion extends StatelessWidget { onTap: () { _gameController.addVersion(FortniteVersion( name: _nameController.text, - location: Directory(_gamePathController.text))); + location: Directory(_gamePathController.text), + memoryFix: _injectMemoryFixController.value + )); }, ) ] diff --git a/lib/src/dialog/add_server_version.dart b/lib/src/dialog/add_server_version.dart index 21dc48e..6d09f3c 100644 --- a/lib/src/dialog/add_server_version.dart +++ b/lib/src/dialog/add_server_version.dart @@ -15,7 +15,8 @@ import 'package:reboot_launcher/src/widget/home/version_name_input.dart'; import '../util/checks.dart'; import '../widget/home/build_selector.dart'; -import '../widget/os/file_selector.dart'; +import '../widget/shared/file_selector.dart'; +import '../widget/shared/smart_check_box.dart'; import 'dialog.dart'; class AddServerVersion extends StatefulWidget { @@ -30,6 +31,7 @@ class _AddServerVersionState extends State { final BuildController _buildController = Get.find(); final TextEditingController _nameController = TextEditingController(); final TextEditingController _pathController = TextEditingController(); + final CheckboxController _injectMemoryFixController = CheckboxController(); late Future _future; DownloadStatus _status = DownloadStatus.none; String _timeLeft = "00:00:00"; @@ -97,7 +99,12 @@ class _AddServerVersionState extends State { ]; case DownloadStatus.error: - return [DialogButton(type: ButtonType.only)]; + return [ + DialogButton( + type: ButtonType.only, + onTap: () => Navigator.of(context).pop(), + ) + ]; default: return [ DialogButton( @@ -154,7 +161,9 @@ class _AddServerVersionState extends State { _status = DownloadStatus.done; _gameController.addVersion(FortniteVersion( name: _nameController.text, - location: Directory(_pathController.text))); + location: Directory(_pathController.text), + memoryFix: _injectMemoryFixController.value + )); }); } @@ -230,7 +239,7 @@ class _AddServerVersionState extends State { padding: const EdgeInsets.only(bottom: 16), child: SizedBox( width: double.infinity, - child: Text("An error was occurred while downloading:$_error", + child: Text("An error occurred while downloading:$_error", textAlign: TextAlign.center)), ); } @@ -269,24 +278,27 @@ class _AddServerVersionState extends State { const SizedBox( height: 8, ), - if(_manifestDownloadProcess != null) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "${_downloadProgress.round()}%", - style: FluentTheme.maybeOf(context)?.typography.body, - ), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "${_downloadProgress.round()}%", + style: FluentTheme.maybeOf(context)?.typography.body, + ), + + if(_manifestDownloadProcess != null) Text( "Time left: $_timeLeft", style: FluentTheme.maybeOf(context)?.typography.body, ), - ], - ), - if(_manifestDownloadProcess != null) - const SizedBox( - height: 8, - ), + ], + ), + + const SizedBox( + height: 8, + ), + SizedBox( width: double.infinity, child: ProgressBar(value: _downloadProgress.toDouble())), @@ -313,7 +325,13 @@ class _AddServerVersionState extends State { windowTitle: "Select download destination", controller: _pathController, validator: checkDownloadDestination, - folder: true), + folder: true + ), + const SizedBox(height: 16.0), + SmartCheckBox( + controller: _injectMemoryFixController, + content: const Text("Inject memory leak fix") + ), const SizedBox(height: 8.0), ], ); diff --git a/lib/src/dialog/dialog.dart b/lib/src/dialog/dialog.dart index 8979b7a..4f4c9af 100644 --- a/lib/src/dialog/dialog.dart +++ b/lib/src/dialog/dialog.dart @@ -53,7 +53,7 @@ class FormDialog extends AbstractDialog { } DialogButton _createFormButton(DialogButton entry, BuildContext context) { - if (entry.type == ButtonType.secondary) { + if (entry.type != ButtonType.primary) { return entry; } @@ -102,8 +102,9 @@ class InfoDialog extends AbstractDialog { class ProgressDialog extends AbstractDialog { final String text; + final Function()? onStop; - const ProgressDialog({required this.text, super.key}); + const ProgressDialog({required this.text, this.onStop, super.key}); @override Widget build(BuildContext context) { @@ -118,8 +119,9 @@ class ProgressDialog extends AbstractDialog { ), buttons: [ DialogButton( - text: "Close", - type: ButtonType.only + text: "Close", + type: ButtonType.only, + onTap: onStop ) ] ); diff --git a/lib/src/dialog/dialog_button.dart b/lib/src/dialog/dialog_button.dart index fcc37b9..8e9eda1 100644 --- a/lib/src/dialog/dialog_button.dart +++ b/lib/src/dialog/dialog_button.dart @@ -52,7 +52,9 @@ class _DialogButtonState extends State { ); } - void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null); + void _onDefaultSecondaryActionTap() { + Navigator.of(context).pop(null); + } } enum ButtonType { diff --git a/lib/src/dialog/server_dialogs.dart b/lib/src/dialog/server_dialogs.dart index 21217a7..0b16888 100644 --- a/lib/src/dialog/server_dialogs.dart +++ b/lib/src/dialog/server_dialogs.dart @@ -14,49 +14,53 @@ import '../util/server.dart'; extension ServerControllerDialog on ServerController { static Semaphore semaphore = Semaphore(); - Future changeStateInteractive(bool ignorePrompts, [bool isRetry = false]) async { - try { + Future changeStateInteractive(bool onlyIfNeeded, [bool isRetry = false]) async { + try{ semaphore.acquire(); if (type() == ServerType.local) { - return _checkLocalServerInteractive(ignorePrompts); + return _checkLocalServerInteractive(onlyIfNeeded); } var oldStarted = started(); - started(!started()); - if (oldStarted) { - var result = await stop(); - if (!result) { - started(true); - _showCannotStopError(); - return true; - } - - return false; + if(oldStarted && onlyIfNeeded){ + return true; } - var result = await start(); - var handled = await _handleResultType(result, ignorePrompts, isRetry); - if (!handled) { - started(false); - return false; - } - - embeddedServer?.exitCode.then((value) { - if (!started()) { - return; - } - - _showUnexpectedError(); - started(false); - }); - - return handled; + started.value = !started.value; + return await _doStateChange(oldStarted, onlyIfNeeded, isRetry); }finally{ semaphore.release(); } } - Future _handleResultType(ServerResult result, bool ignorePrompts, bool isRetry) async { + Future _doStateChange(bool oldStarted, bool onlyIfNeeded, bool isRetry) async { + if (oldStarted) { + var result = await stop(); + if (!result) { + started.value = true; + _showCannotStopError(); + return true; + } + + return false; + } + + var result = await start(!onlyIfNeeded); + if(result.type == ServerResultType.ignoreStart) { + started.value = false; + return true; + } + + var handled = await _handleResultType(oldStarted, onlyIfNeeded, isRetry, result); + if (!handled) { + started.value = false; + return false; + } + + return handled; + } + + Future _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, ServerResult result) async { switch (result.type) { case ServerResultType.missingHostError: _showMissingHostError(); @@ -68,6 +72,10 @@ extension ServerControllerDialog on ServerController { _showIllegalPortError(); return false; case ServerResultType.cannotPingServer: + if(!started() || result.pid != embeddedServer?.pid){ + return false; + } + _showPingErrorDialog(); return false; case ServerResultType.portTakenError: @@ -82,18 +90,18 @@ extension ServerControllerDialog on ServerController { } await freeLawinPort(); - return changeStateInteractive(ignorePrompts, true); + return _doStateChange(oldStarted, onlyIfNeeded, true); case ServerResultType.serverDownloadRequiredError: if (isRetry) { return false; } - var result = await _downloadServerInteractive(); + var result = await downloadServerInteractive(false); if (!result) { return false; } - return changeStateInteractive(ignorePrompts, true); + return _doStateChange(oldStarted, onlyIfNeeded, true); case ServerResultType.unknownError: showDialog( context: appKey.currentContext!, @@ -106,6 +114,7 @@ extension ServerControllerDialog on ServerController { ) ); return false; + case ServerResultType.ignoreStart: case ServerResultType.started: return true; case ServerResultType.canStart: @@ -116,7 +125,7 @@ extension ServerControllerDialog on ServerController { Future _checkLocalServerInteractive(bool ignorePrompts) async { try { - var future = pingSelf(); + var future = pingSelf(port.text); if(!ignorePrompts) { await showDialog( context: appKey.currentContext!, @@ -138,22 +147,6 @@ extension ServerControllerDialog on ServerController { } } - Future _downloadServerInteractive() async { - var download = compute(downloadServer, true); - return await showDialog( - context: appKey.currentContext!, - builder: (context) => - FutureBuilderDialog( - future: download, - loadingMessage: "Downloading server...", - loadedBody: FutureBuilderDialog.ofMessage( - "The server was downloaded successfully"), - errorMessageBuilder: ( - message) => "Cannot download server: $message" - ) - ) ?? download.isCompleted(); - } - Future _showPortTakenError() async { showDialog( context: appKey.currentContext!, @@ -186,6 +179,10 @@ extension ServerControllerDialog on ServerController { } void _showPingErrorDialog() { + if(!started.value){ + return; + } + showDialog( context: appKey.currentContext!, builder: (context) => @@ -196,6 +193,10 @@ extension ServerControllerDialog on ServerController { } void _showCannotStopError() { + if(!started.value){ + return; + } + showDialog( context: appKey.currentContext!, builder: (context) => @@ -205,12 +206,12 @@ extension ServerControllerDialog on ServerController { ); } - void _showUnexpectedError() { + void showUnexpectedError() { showDialog( context: appKey.currentContext!, builder: (context) => const InfoDialog( - text: "The lawin terminated died unexpectedly" + text: "The lawin server died unexpectedly" ) ); } @@ -226,4 +227,21 @@ extension ServerControllerDialog on ServerController { void _showMissingHostError() { showMessage("Missing the host name for lawin server"); } +} + +Future downloadServerInteractive(bool closeAutomatically) async { + var download = compute(downloadServer, true); + return await showDialog( + context: appKey.currentContext!, + builder: (context) => + FutureBuilderDialog( + future: download, + loadingMessage: "Downloading server...", + loadedBody: FutureBuilderDialog.ofMessage( + "The server was downloaded successfully"), + errorMessageBuilder: ( + message) => "Cannot download server: $message", + closeAutomatically: closeAutomatically + ) + ) ?? download.isCompleted(); } \ No newline at end of file diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 33dbc3b..b425f8c 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -5,12 +5,14 @@ import 'package:path/path.dart' as path; class FortniteVersion { String name; Directory location; + bool memoryFix; FortniteVersion.fromJson(json) : name = json["name"], - location = Directory(json["location"]); + location = Directory(json["location"]), + memoryFix = json["memory_fix"] ?? false; - FortniteVersion({required this.name, required this.location}); + FortniteVersion({required this.name, required this.location, required this.memoryFix}); static File? findExecutable(Directory directory, String name) { try{ diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index c94cf92..f114806 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -1,9 +1,10 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; +import 'package:reboot_launcher/src/util/os.dart'; import 'package:url_launcher/url_launcher.dart'; -const String _discordLink = "https://discord.gg/NJU4QjxSMF"; - class InfoPage extends StatelessWidget { const InfoPage({Key? key}) : super(key: key); @@ -42,8 +43,8 @@ class InfoPage extends StatelessWidget { Button _createDiscordButton() { return Button( - child: const Text("Join the discord"), - onPressed: () => launchUrl(Uri.parse(_discordLink))); + child: const Text("Open file directory"), + onPressed: () => launchUrl(Directory(safeBinariesDirectory).uri)); } CircleAvatar _createAutiesAvatar() { @@ -55,7 +56,7 @@ class InfoPage extends StatelessWidget { Align _createVersionInfo() { return const Align( alignment: Alignment.bottomRight, - child: Text("Version 4.0${kDebugMode ? '-DEBUG' : ''}") + child: Text("Version 4.4${kDebugMode ? '-DEBUG' : ''}") ); } } \ No newline at end of file diff --git a/lib/src/page/launcher_page.dart b/lib/src/page/launcher_page.dart index 1863748..03ef1be 100644 --- a/lib/src/page/launcher_page.dart +++ b/lib/src/page/launcher_page.dart @@ -11,11 +11,9 @@ import 'package:reboot_launcher/src/widget/home/game_type_selector.dart'; import 'package:reboot_launcher/src/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/widget/home/username_box.dart'; import 'package:reboot_launcher/src/widget/home/version_selector.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../controller/settings_controller.dart'; import '../util/reboot.dart'; -import '../widget/shared/warning_info.dart'; class LauncherPage extends StatefulWidget { const LauncherPage( @@ -33,10 +31,9 @@ class _LauncherPageState extends State { @override void initState() { - if(_gameController.updater == null && _settingsController.autoUpdate.value){ + if(_gameController.updater == null){ _gameController.updater = compute(downloadRebootDll, _updateTime) - ..then((value) => _updateTime = value) - ..onError(_saveError); + ..then((value) => _updateTime = value); _buildController.cancelledDownload .listen((value) => value ? _onCancelWarning() : {}); } @@ -54,13 +51,6 @@ class _LauncherPageState extends State { storage.write("last_update", updateTime); } - Future _saveError(Object? error, StackTrace stackTrace) async { - var errorFile = await loadBinary("error.txt", true); - errorFile.writeAsString( - "Error: $error\nStacktrace: $stackTrace", mode: FileMode.write); - throw Exception("Cannot update reboot.dll"); - } - void _onCancelWarning() { WidgetsBinding.instance.addPostFrameCallback((_) { if(!mounted) { @@ -78,7 +68,7 @@ class _LauncherPageState extends State { return Padding( padding: const EdgeInsets.all(12.0), child: FutureBuilder( - future: _gameController.updater, + future: _gameController.updater ?? Future.value(true), builder: (context, snapshot) { if (!snapshot.hasData && !snapshot.hasError) { return Row( @@ -114,11 +104,12 @@ class _LauncherPageState extends State { } Widget _createUpdateError(AsyncSnapshot snapshot) { - return WarningInfo( - text: "Cannot update Reboot DLL", - icon: FluentIcons.info, - severity: InfoBarSeverity.warning, - onPressed: () => loadBinary("error.txt", true).then((file) => launchUrl(file.uri)) + return const SizedBox( + width: double.infinity, + child: InfoBar( + title: Text("Cannot update dll"), + severity: InfoBarSeverity.warning + ), ); } } diff --git a/lib/src/page/server_page.dart b/lib/src/page/server_page.dart index d03dcdf..4c06a0c 100644 --- a/lib/src/page/server_page.dart +++ b/lib/src/page/server_page.dart @@ -5,7 +5,6 @@ import 'package:reboot_launcher/src/widget/server/host_input.dart'; import 'package:reboot_launcher/src/widget/server/server_type_selector.dart'; import 'package:reboot_launcher/src/widget/server/port_input.dart'; import 'package:reboot_launcher/src/widget/server/server_button.dart'; -import 'package:reboot_launcher/src/widget/shared/warning_info.dart'; class ServerPage extends StatelessWidget { final ServerController _serverController = Get.find(); @@ -21,10 +20,13 @@ class ServerPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if(_serverController.warning.value) - WarningInfo( - text: "The lawin server handles authentication and parties, not game hosting", - icon: FluentIcons.accept, - onPressed: () => _serverController.warning.value = false + SizedBox( + width: double.infinity, + child: InfoBar( + title: const Text("The lawin server handles authentication and parties, not game hosting"), + severity: InfoBarSeverity.info, + onClose: () => _serverController.warning.value = false + ), ), HostInput(), PortInput(), diff --git a/lib/src/page/settings_page.dart b/lib/src/page/settings_page.dart index 954c6ab..ba15874 100644 --- a/lib/src/page/settings_page.dart +++ b/lib/src/page/settings_page.dart @@ -1,13 +1,18 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; +import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/widget/shared/smart_switch.dart'; import '../util/checks.dart'; -import '../widget/os/file_selector.dart'; +import '../widget/shared/file_selector.dart'; +import '../widget/shared/smart_input.dart'; class SettingsPage extends StatelessWidget { + final ServerController _serverController = Get.find(); final SettingsController _settingsController = Get.find(); SettingsPage({Key? key}) : super(key: key); @@ -17,48 +22,57 @@ class SettingsPage extends StatelessWidget { return Padding( padding: const EdgeInsets.all(12.0), child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FileSelector( - label: "Reboot DLL", - placeholder: "Type the path to the reboot dll", - controller: _settingsController.rebootDll, - windowTitle: "Select a dll", - folder: false, - extension: "dll", - validator: checkDll, - validatorMode: AutovalidateMode.always - ), - - FileSelector( - label: "Console DLL", - placeholder: "Type the path to the console dll", - controller: _settingsController.consoleDll, - windowTitle: "Select a dll", - folder: false, - extension: "dll", - validator: checkDll, - validatorMode: AutovalidateMode.always - ), - - FileSelector( - label: "Cranium DLL", - placeholder: "Type the path to the cranium dll", - controller: _settingsController.craniumDll, - windowTitle: "Select a dll", - folder: false, - extension: "dll", - validator: checkDll, - validatorMode: AutovalidateMode.always - ), - - SmartSwitch( - value: _settingsController.autoUpdate, - label: "Update DLLs" - ) - ] - ), + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: "The hostname of the server that hosts the multiplayer matches", + child: Obx(() => SmartInput( + label: "Matchmaking Host", + placeholder: + "Type the hostname of the server that hosts the multiplayer matches", + controller: _settingsController.matchmakingIp, + validatorMode: AutovalidateMode.always, + validator: checkMatchmaking, + enabled: _serverController.type() == ServerType.embedded + )) + ), + Tooltip( + message: "The dll that is injected when a server is launched", + child: FileSelector( + label: "Reboot DLL", + placeholder: "Type the path to the reboot dll", + controller: _settingsController.rebootDll, + windowTitle: "Select a dll", + folder: false, + extension: "dll", + validator: checkDll, + validatorMode: AutovalidateMode.always), + ), + Tooltip( + message: "The dll that is injected when a client is launched", + child: FileSelector( + label: "Console DLL", + placeholder: "Type the path to the console dll", + controller: _settingsController.consoleDll, + windowTitle: "Select a dll", + folder: false, + extension: "dll", + validator: checkDll, + validatorMode: AutovalidateMode.always), + ), + Tooltip( + message: "The dll that is injected to make the game work", + child: FileSelector( + label: "Cranium DLL", + placeholder: "Type the path to the cranium dll", + controller: _settingsController.craniumDll, + windowTitle: "Select a dll", + folder: false, + extension: "dll", + validator: checkDll, + validatorMode: AutovalidateMode.always)) + ]), ); } } diff --git a/lib/src/util/checks.dart b/lib/src/util/checks.dart index acb7307..789f6df 100644 --- a/lib/src/util/checks.dart +++ b/lib/src/util/checks.dart @@ -52,5 +52,13 @@ String? checkDll(String? text) { return "This file is not a dll"; } + return null; +} + +String? checkMatchmaking(String? text) { + if (text == null || text.isEmpty) { + return "Empty hostname"; + } + return null; } \ No newline at end of file diff --git a/lib/src/util/os.dart b/lib/src/util/os.dart index 16bcd93..14709c7 100644 --- a/lib/src/util/os.dart +++ b/lib/src/util/os.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:file_picker/file_picker.dart'; -import 'package:path/path.dart' as path; - const int appBarSize = 2; final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); @@ -16,22 +13,6 @@ bool get isWin11 { return intBuild != null && intBuild > 22000; } -Future openFolderPicker(String title) async => - await FilePicker.platform.getDirectoryPath(dialogTitle: title); - -Future openFilePicker(String extension) async { - var result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowMultiple: false, - allowedExtensions: [extension] - ); - if(result == null || result.files.isEmpty){ - return null; - } - - return result.files.first.path; -} - Future loadBinary(String binary, bool safe) async{ var safeBinary = File("$safeBinariesDirectory\\$binary"); if(await safeBinary.exists()){ diff --git a/lib/src/util/patcher.dart b/lib/src/util/patcher.dart index a3c940e..94e4d61 100644 --- a/lib/src/util/patcher.dart +++ b/lib/src/util/patcher.dart @@ -1,40 +1,58 @@ import 'dart:io'; import 'dart:typed_data'; -final Uint8List _original = Uint8List.fromList([ +final Uint8List _originalHeadless = Uint8List.fromList([ 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0 ]); -final Uint8List _patched = Uint8List.fromList([ +final Uint8List _patchedHeadless = Uint8List.fromList([ 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 ]); -Future patch(File file) async { - if(_original.length != _patched.length){ - throw Exception("Cannot mutate length of binary file"); - } +final Uint8List _originalMatchmaking = Uint8List.fromList([ + 63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61 +]); - var read = await file.readAsBytes(); - var length = await file.length(); - var offset = 0; - var counter = 0; - while(offset < length){ - if(read[offset] == _original[counter]){ - counter++; - }else { - counter = 0; +final Uint8List _patchedMatchmaking = Uint8List.fromList([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +]); + +Future patchHeadless(File file) async => + _patch(file, _originalHeadless, _patchedHeadless); + +Future patchMatchmaking(File file) async => + await _patch(file, _originalMatchmaking, _patchedMatchmaking); + +Future _patch(File file, Uint8List original, Uint8List patched) async { + try { + if(original.length != patched.length){ + throw Exception("Cannot mutate length of binary file"); } - offset++; - if(counter == _original.length){ - for(var index = 0; index < _patched.length; index++){ - read[offset - counter + index] = _patched[index]; + var read = await file.readAsBytes(); + var length = await file.length(); + var offset = 0; + var counter = 0; + while(offset < length){ + if(read[offset] == original[counter]){ + counter++; + }else { + counter = 0; } - await file.writeAsBytes(read, mode: FileMode.write); - return true; - } - } + offset++; + if(counter == original.length){ + for(var index = 0; index < patched.length; index++){ + read[offset - counter + index] = patched[index]; + } - return false; + await file.writeAsBytes(read, mode: FileMode.write); + return true; + } + } + + return false; + }catch(_){ + return false; + } } \ No newline at end of file diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index a30542e..3cbeb81 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -28,12 +28,12 @@ Future downloadRebootDll(int? lastUpdateMs) async { var outputDir = await tempDirectory.createTemp("reboot"); await extractFileToDisk(tempZip.path, outputDir.path); - + var rebootDll = outputDir.listSync() .firstWhere((element) => path.extension(element.path) == ".dll"); if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) { outputDir.delete(recursive: true); - return lastUpdateMs ?? now.millisecondsSinceEpoch; + return now.millisecondsSinceEpoch; } await rebootDll.rename(oldRebootDll.path); diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index fe19bf5..872a55b 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -8,11 +8,13 @@ import 'package:reboot_launcher/src/util/os.dart'; import 'package:http/http.dart' as http; import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:shelf/shelf_io.dart'; -import 'package:path/path.dart' as path; -final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe"); +final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Lawin.exe"); +final serverConfig = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Config\\config.ini"); +final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt"); + const String _serverUrl = - "https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip"; + "https://cdn.discordapp.com/attachments/1031262639457828910/1034506676843327549/lawin.zip"; Future downloadServer(ignored) async { var response = await http.get(Uri.parse(_serverUrl)); @@ -63,7 +65,7 @@ List createRebootArgs(String username, bool headless) { return args; } -Future pingSelf() async => ping("127.0.0.1", "3551"); +Future pingSelf(String port) async => ping("127.0.0.1", port); Future ping(String host, String port, [bool https=false]) async { var hostName = _getHostName(host); @@ -72,14 +74,15 @@ Future ping(String host, String port, [bool https=false]) async { var uri = Uri( scheme: declaredScheme ?? (https ? "https" : "http"), host: hostName, - port: int.parse(port) + port: int.parse(port), + path: "unknown" ); var client = HttpClient() ..connectionTimeout = const Duration(seconds: 5); var request = await client.getUrl(uri); var response = await request.close(); var body = utf8.decode(await response.single); - return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null; + return body.contains("epicgames") || body.contains("lawinserver") ? uri : null; }catch(_){ return https || declaredScheme != null ? null : await ping(host, port, true); } @@ -89,7 +92,7 @@ String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFir String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; -Future checkServerPreconditions(String host, String port, ServerType type) async { +Future checkServerPreconditions(String host, String port, ServerType type, bool needsFreePort) async { host = host.trim(); if(host.isEmpty){ return ServerResult( @@ -113,6 +116,13 @@ Future checkServerPreconditions(String host, String port, ServerTy if(type == ServerType.embedded || type == ServerType.remote){ var free = await isLawinPortFree(); if (!free) { + if(!needsFreePort) { + return ServerResult( + uri: pingSelf(port), + type: ServerResultType.ignoreStart + ); + } + return ServerResult( type: ServerResultType.portTakenError ); @@ -131,21 +141,42 @@ Future checkServerPreconditions(String host, String port, ServerTy ); } -Future startEmbeddedServer() async { - return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path); +Future startEmbeddedServer() async { + await resetServerLog(); + try { + var process = await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path); + process.outLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append)); + process.errLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append)); + return process; + } on ProcessException { + return null; + } } Future startRemoteServer(Uri uri) async { return await serve(proxyHandler(uri), "127.0.0.1", 3551); } +Future resetServerLog() async { + try { + if(await serverLogFile.exists()) { + await serverLogFile.delete(); + } + + await serverLogFile.create(); + }catch(_){ + // Ignored + } +} + class ServerResult { final Future? uri; + final int? pid; final Object? error; final StackTrace? stackTrace; final ServerResultType type; - ServerResult({this.uri, this.error, this.stackTrace, required this.type}); + ServerResult({this.uri, this.pid, this.error, this.stackTrace, required this.type}); } enum ServerResultType { @@ -156,7 +187,8 @@ enum ServerResultType { portTakenError, serverDownloadRequiredError, canStart, + ignoreStart, started, unknownError, - stopped + stopped, } \ No newline at end of file diff --git a/lib/src/widget/home/launch_button.dart b/lib/src/widget/home/launch_button.dart index 2298fb8..cce53f4 100644 --- a/lib/src/widget/home/launch_button.dart +++ b/lib/src/widget/home/launch_button.dart @@ -18,7 +18,6 @@ import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/reboot.dart'; import 'package:reboot_launcher/src/util/server.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; import 'package:path/path.dart' as path; @@ -55,10 +54,10 @@ class _LaunchButtonState extends State { child: SizedBox( width: double.infinity, child: Obx(() => Tooltip( - message: _gameController.started.value ? "Close the running Fortnite instance" : "Launch a new Fortnite instance", + message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance", child: Button( onPressed: _onPressed, - child: Text(_gameController.started.value ? "Close" : "Launch") + child: Text(_gameController.started() ? "Close" : "Launch") ), )), ), @@ -66,41 +65,38 @@ class _LaunchButtonState extends State { } void _onPressed() async { + if (_gameController.started()) { + _onStop(); + return; + } + + _gameController.started.value = true; if (_gameController.username.text.isEmpty) { showMessage("Missing in-game username"); - _updateServerState(false); + _gameController.started.value = false; return; } if (_gameController.selectedVersionObs.value == null) { showMessage("No version is selected"); - _updateServerState(false); - return; - } - - if (_gameController.started.value) { - _onStop(); + _gameController.started.value = false; return; } try { - _updateServerState(true); var version = _gameController.selectedVersionObs.value!; - var hosting = _gameController.type.value == GameType.headlessServer; + var gamePath = version.executable?.path; + if(gamePath == null){ + _onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null); + _onStop(); + return; + } + if (version.launcher != null) { _gameController.launcherProcess = await Process.start(version.launcher!.path, []); Win32Process(_gameController.launcherProcess!.pid).suspend(); } - if(hosting){ - await patch(version.executable!); - } - - if(!mounted){ - _onStop(); - return; - } - var result = await _serverController.changeStateInteractive(true); if(!result){ _onStop(); @@ -111,19 +107,16 @@ class _LaunchButtonState extends State { await _logFile!.delete(); } - var gamePath = version.executable?.path; - if(gamePath == null){ - _onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null); - _onStop(); - return; - } - _gameController.gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, hosting)) + await patch(version.executable!); + + var headlessHosting = _gameController.type() == GameType.headlessServer; + var arguments = createRebootArgs(_gameController.username.text, headlessHosting); + _gameController.gameProcess = await Process.start(gamePath, arguments) ..exitCode.then((_) => _onEnd()) - ..outLines.forEach(_onGameOutput); - await _injectOrShowError(Injectable.cranium); - - if(hosting){ + ..outLines.forEach((line) => _onGameOutput(line, version.memoryFix)) + ..errLines.forEach((line) => _onGameOutput(line, version.memoryFix)); + if(headlessHosting){ await _showServerLaunchingWarning(); } } catch (exception, stacktrace) { @@ -133,12 +126,15 @@ class _LaunchButtonState extends State { } } - Future _updateServerState(bool value) async { - if (_gameController.started.value == value) { - return; + Future patch(File file) async { + switch(_gameController.type()){ + case GameType.client: + return await compute(patchMatchmaking, file); + case GameType.server: + return false; + case GameType.headlessServer: + return await compute(patchHeadless, file); } - - _gameController.started(value); } void _onEnd() { @@ -170,15 +166,12 @@ class _LaunchButtonState extends State { var result = await showDialog( context: context, - builder: (context) => InfoDialog.ofOnly( - text: "Launching headless reboot server...", - button: DialogButton( - type: ButtonType.only, - onTap: () { - Navigator.of(context).pop(false); - _onStop(); - } - ) + builder: (context) => ProgressDialog( + text: "Launching headless server...", + onStop: () { + Navigator.of(context).pop(false); + _onStop(); + } ) ); @@ -189,7 +182,11 @@ class _LaunchButtonState extends State { _onStop(); } - void _onGameOutput(String line) { + void _onGameOutput(String line, bool memoryFix) { + if(kDebugMode){ + print(line); + } + if(_logFile != null){ _logFile!.writeAsString("$line\n", mode: FileMode.append); } @@ -220,13 +217,22 @@ class _LaunchButtonState extends State { return; } - if(line.contains("Region")){ + if(line.contains("Platform has ")){ + _injectOrShowError(Injectable.cranium); + return; + } + + if(line.contains("Login: Completing Sign-in")){ if(_gameController.type.value == GameType.client){ _injectOrShowError(Injectable.console); }else { _injectOrShowError(Injectable.reboot) .then((value) => _closeDialogIfOpen(true)); } + + if(memoryFix){ + _injectOrShowError(Injectable.memoryFix); + } } } @@ -242,7 +248,7 @@ class _LaunchButtonState extends State { } void _onStop() { - _updateServerState(false); + _gameController.started.value = false; _gameController.kill(); } @@ -253,7 +259,7 @@ class _LaunchButtonState extends State { } try { - var dllPath = _getDllPath(injectable); + var dllPath = await _getDllPath(injectable); if(!dllPath.existsSync()) { await _downloadMissingDll(injectable); if(!dllPath.existsSync()){ @@ -284,7 +290,7 @@ class _LaunchButtonState extends State { }); } - File _getDllPath(Injectable injectable){ + Future _getDllPath(Injectable injectable) async { switch(injectable){ case Injectable.reboot: return File(_settingsController.rebootDll.text); @@ -292,6 +298,8 @@ class _LaunchButtonState extends State { return File(_settingsController.consoleDll.text); case Injectable.cranium: return File(_settingsController.craniumDll.text); + case Injectable.memoryFix: + return await loadBinary("fix.dll", true); } } @@ -308,5 +316,6 @@ class _LaunchButtonState extends State { enum Injectable { console, cranium, - reboot + reboot, + memoryFix } diff --git a/lib/src/widget/home/version_selector.dart b/lib/src/widget/home/version_selector.dart index 2f30810..9fe211c 100644 --- a/lib/src/widget/home/version_selector.dart +++ b/lib/src/widget/home/version_selector.dart @@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:get/get.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/dialog/snackbar.dart'; import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/dialog/add_local_version.dart'; import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart'; @@ -119,7 +120,7 @@ class _VersionSelectorState extends State { context: context, offset: offset, builder: (context) => MenuFlyout( - items: ContextualOption.values + items: ContextualOption.getValues(version.memoryFix) .map((entry) => _createOption(context, entry)) .toList() ) @@ -172,8 +173,26 @@ class _VersionSelectorState extends State { } break; + case ContextualOption.enableMemoryFix: + if(!mounted){ + return; + } - case null: + version.memoryFix = true; + Navigator.of(context).pop(); + showMessage("Enabled memory fix"); + break; + case ContextualOption.disableMemoryFix: + if(!mounted){ + return; + } + + version.memoryFix = false; + Navigator.of(context).pop(); + showMessage("Disabled memory fix"); + break; + + default: break; } } @@ -280,11 +299,21 @@ class _VersionSelectorState extends State { enum ContextualOption { openExplorer, rename, + enableMemoryFix, + disableMemoryFix, delete; + static List getValues(bool memoryFix){ + return memoryFix + ? [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.disableMemoryFix, ContextualOption.delete] + : [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.enableMemoryFix, ContextualOption.delete]; + } + String get name { return this == ContextualOption.openExplorer ? "Open in explorer" : this == ContextualOption.rename ? "Rename" + : this == ContextualOption.enableMemoryFix ? "Enable memory leak fix" + : this == ContextualOption.disableMemoryFix ? "Disable memory leak fix" : "Delete"; } } diff --git a/lib/src/widget/os/file_selector.dart b/lib/src/widget/os/file_selector.dart deleted file mode 100644 index 057284d..0000000 --- a/lib/src/widget/os/file_selector.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:reboot_launcher/src/dialog/snackbar.dart'; - -import '../../util/os.dart'; - -class FileSelector extends StatefulWidget { - final String label; - final String placeholder; - final String windowTitle; - final bool allowNavigator; - final TextEditingController controller; - final String? Function(String?) validator; - final AutovalidateMode? validatorMode; - final String? extension; - final bool folder; - - const FileSelector( - {required this.label, - required this.placeholder, - required this.windowTitle, - required this.controller, - required this.validator, - required this.folder, - this.extension, - this.validatorMode, - this.allowNavigator = true, - Key? key}) - : assert(folder || extension != null, "Missing extension for file selector"), - super(key: key); - - @override - State createState() => _FileSelectorState(); -} - -class _FileSelectorState extends State { - bool _selecting = false; - - @override - Widget build(BuildContext context) { - return InfoLabel( - label: widget.label, - child: Row( - children: [ - Expanded( - child: TextFormBox( - controller: widget.controller, - placeholder: widget.placeholder, - validator: widget.validator, - autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction - ) - ), - if (widget.allowNavigator) const SizedBox(width: 8.0), - if (widget.allowNavigator) - Tooltip( - message: "Select a ${widget.folder ? 'folder' : 'file'}", - child: Button( - onPressed: _onPressed, - child: const Icon(FluentIcons.open_folder_horizontal) - ), - ) - ], - ) - ); - } - - void _onPressed() { - if(_selecting){ - showMessage("Folder selector is already opened"); - return; - } - - _selecting = true; - if(widget.folder) { - compute(openFolderPicker, widget.windowTitle) - .then((value) => widget.controller.text = value ?? widget.controller.text) - .then((_) => _selecting = false); - return; - } - - compute(openFilePicker, widget.extension!) - .then((value) => widget.controller.text = value ?? widget.controller.text) - .then((_) => _selecting = false); - } -} \ No newline at end of file diff --git a/lib/src/widget/shared/smart_input.dart b/lib/src/widget/shared/smart_input.dart index 0efc0e8..5d83629 100644 --- a/lib/src/widget/shared/smart_input.dart +++ b/lib/src/widget/shared/smart_input.dart @@ -8,6 +8,8 @@ class SmartInput extends StatelessWidget { final bool enabled; final VoidCallback? onTap; final bool readOnly; + final AutovalidateMode validatorMode; + final String? Function(String?)? validator; const SmartInput( {Key? key, @@ -17,12 +19,14 @@ class SmartInput extends StatelessWidget { this.onTap, this.enabled = true, this.readOnly = false, - this.type = TextInputType.text}) + this.type = TextInputType.text, + this.validatorMode = AutovalidateMode.disabled, + this.validator}) : super(key: key); @override Widget build(BuildContext context) { - return TextBox( + return TextFormBox( enabled: enabled, controller: controller, header: label, @@ -30,6 +34,8 @@ class SmartInput extends StatelessWidget { placeholder: placeholder, onTap: onTap, readOnly: readOnly, + autovalidateMode: validatorMode, + validator: validator ); } } diff --git a/lib/src/widget/shared/warning_info.dart b/lib/src/widget/shared/warning_info.dart deleted file mode 100644 index 6dc957a..0000000 --- a/lib/src/widget/shared/warning_info.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:fluent_ui/fluent_ui.dart'; - -class WarningInfo extends StatelessWidget { - final String text; - final VoidCallback onPressed; - final IconData icon; - final InfoBarSeverity severity; - - const WarningInfo( - {Key? key, - required this.text, - required this.icon, - required this.onPressed, - this.severity = InfoBarSeverity.info}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return InfoBar( - severity: severity, - title: Text(text), - action: IconButton( - icon: Icon(icon), - onPressed: onPressed - ) - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index a9b7bb8..b0a84b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "4.0.0" +version: "4.4.0" publish_to: 'none' @@ -36,6 +36,7 @@ dependencies: win32: 3.0.0 clipboard: ^0.1.3 sync: ^0.3.0 + ini: ^2.1.0 dependency_overrides: win32: ^3.0.0 @@ -46,6 +47,7 @@ dev_dependencies: flutter_lints: ^2.0.1 msix: ^3.6.3 + hex: ^0.2.0 flutter: uses-material-design: true @@ -58,7 +60,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 4.0.0.0 + msix_version: 4.4.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64 diff --git a/release/release.bat b/release/release.bat index 7e08534..b8f0640 100644 --- a/release/release.bat +++ b/release/release.bat @@ -1,3 +1,3 @@ flutter_distributor package --platform windows --targets exe flutter pub run msix:create -dart compile exe ./../lib/cli.dart --output ./../dist/cli/reboot.exe +dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe