diff --git a/common/lib/src/constant/game.dart b/common/lib/src/constant/game.dart index 4e6c283..c84c875 100644 --- a/common/lib/src/constant/game.dart +++ b/common/lib/src/constant/game.dart @@ -1,4 +1,5 @@ const String kDefaultPlayerName = "Player"; +const String kDefaultHostName = "Host"; const String kDefaultGameServerHost = "127.0.0.1"; const String kDefaultGameServerPort = "7777"; const String kInitializedLine = "Game Engine Initialized"; diff --git a/common/lib/src/model/dll.dart b/common/lib/src/model/dll.dart index b54398f..8830eee 100644 --- a/common/lib/src/model/dll.dart +++ b/common/lib/src/model/dll.dart @@ -1,6 +1,9 @@ enum InjectableDll { console, - sinum, + starfall, reboot, - memory +} + +extension InjectableDllVersionAware on InjectableDll { + bool get isVersionDependent => this == InjectableDll.reboot; } diff --git a/common/lib/src/model/fortnite_build.dart b/common/lib/src/model/fortnite_build.dart index 50248d6..3371c59 100644 --- a/common/lib/src/model/fortnite_build.dart +++ b/common/lib/src/model/fortnite_build.dart @@ -17,13 +17,15 @@ class FortniteBuild { class FortniteBuildDownloadProgress { final double progress; - final int? minutesLeft; + final int? timeLeft; final bool extracting; + final int speed; FortniteBuildDownloadProgress({ required this.progress, required this.extracting, - this.minutesLeft, + required this.timeLeft, + required this.speed }); } diff --git a/common/lib/src/model/game_instance.dart b/common/lib/src/model/game_instance.dart index 709b88b..6bb11d6 100644 --- a/common/lib/src/model/game_instance.dart +++ b/common/lib/src/model/game_instance.dart @@ -1,10 +1,11 @@ import 'dart:io'; import 'package:reboot_common/common.dart'; +import 'package:version/version.dart'; class GameInstance { - final String versionName; + final Version version; final int gamePid; final int? launcherPid; final int? eacPid; @@ -17,7 +18,7 @@ class GameInstance { GameInstance? child; GameInstance({ - required this.versionName, + required this.version, required this.gamePid, required this.launcherPid, required this.eacPid, diff --git a/common/lib/src/util/build.dart b/common/lib/src/util/build.dart index 390c137..f3afc33 100644 --- a/common/lib/src/util/build.dart +++ b/common/lib/src/util/build.dart @@ -3,165 +3,243 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; import 'package:reboot_common/src/extension/types.dart'; +import 'package:uuid/uuid.dart'; import 'package:version/version.dart'; +import 'package:http/http.dart' as http; + const String kStopBuildDownloadSignal = "kill"; -final Dio _dio = _buildDioInstance(); -Dio _buildDioInstance() { - final dio = Dio(); - final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter; - httpClientAdapter.createHttpClient = () { - final client = HttpClient(); - client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; - return client; - }; - return dio; -} - -final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md"; +final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json"); +final int _ariaPort = 6800; +final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc'); +final Duration _ariaMaxSpawnTime = const Duration(seconds: 10); +final String _ariaSecret = "RebootLauncher"; final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); -const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download"; -const String _unavailableError = "The build downloader is not available right now"; -const String _genericError = "The build downloader is not working correctly"; -const int _maxErrors = 100; Future> fetchBuilds(ignored) async { - final response = await _dio.get( - _archiveSourceUrl, - options: Options( - responseType: ResponseType.plain - ) - ); + final response = await http.get(_archiveSourceUrl); if (response.statusCode != 200) { return []; } - var results = []; - for (final line in response.data?.split("\n") ?? []) { - if (!line.startsWith("|")) { - continue; - } - - var parts = line.substring(1, line.length - 1).split("|"); - if (parts.isEmpty) { - continue; - } - - var versionName = parts.first.trim(); - final separator = versionName.indexOf("-"); - if(separator != -1) { - versionName = versionName.substring(0, separator); - } - - final link = parts.last.trim(); - try { - results.add(FortniteBuild( - version: Version.parse(versionName), - link: link, - available: link.endsWith(".zip") || link.endsWith(".rar") - )); - } on FormatException { - // Ignore - } - } - - return results; + return jsonDecode(response.body) + .map((entry) { + try { + final fileUrl = entry as String; + final fileName = Uri.parse(fileUrl).pathSegments.last; + final fileNameWithoutExtension = path.basenameWithoutExtension(fileName); + return FortniteBuild( + version: Version.parse(fileNameWithoutExtension), + link: entry, + available: true + ); + }catch(_) { + return null; + } + }) + .whereType() + .toList(); } - Future downloadArchiveBuild(FortniteBuildDownloadOptions options) async { + final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1); + final outputFile = File("${options.destination.path}\\.build\\$fileName"); try { final stopped = _setupLifecycle(options); - final outputDir = Directory("${options.destination.path}\\.build"); - await outputDir.create(recursive: true); - final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1); - final extension = path.extension(fileName); - final tempFile = File("${outputDir.path}\\$fileName"); - if(await tempFile.exists()) { - await tempFile.delete(recursive: true); - } + await outputFile.parent.create(recursive: true); - final startTime = DateTime.now().millisecondsSinceEpoch; - final response = _downloadArchive(options, stopped, tempFile, startTime); - await Future.any([stopped.future, response]); - if(!stopped.isCompleted) { - await _extractArchive(stopped, extension, tempFile, options); - } + final downloadItemCompleter = Completer(); - delete(outputDir); - }catch(error) { - _onError(error, options); - } -} + await _startAriaServer(); + final downloadId = await _startAriaDownload(options, outputFile); + Timer.periodic(const Duration(seconds: 5), (Timer timer) async { + try { + final statusRequestId = Uuid().toString().replaceAll("-", ""); + final statusRequest = { + "jsonrcp": "2.0", + "id": statusRequestId, + "method": "aria2.tellStatus", + "params": [ + "token:${_ariaSecret}", + downloadId + ] + }; + final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest)); + final statusResponseJson = jsonDecode(statusResponse.body) as Map?; + if(statusResponseJson == null) { + downloadItemCompleter.completeError("Invalid download status (invalid JSON)"); + timer.cancel(); + return; + } -Future _downloadArchive(FortniteBuildDownloadOptions options, Completer stopped, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async { - var received = byteStart ?? 0; - try { - await _dio.download( - options.build.link, - tempFile.path, - onReceiveProgress: (data, length) { - if(stopped.isCompleted) { - throw StateError("Download interrupted"); + final result = statusResponseJson["result"]; + final files = result["files"] as List?; + if(files == null || files.isEmpty) { + downloadItemCompleter.completeError("Download aborted"); + timer.cancel(); + return; + } + + final error = result["errorCode"]; + if(error != null) { + final errorCode = int.tryParse(error); + if(errorCode == 0) { + final path = File(files[0]["path"]); + downloadItemCompleter.complete(path); + }else if(errorCode == 3) { + downloadItemCompleter.completeError("This build is not available yet"); + }else { + final errorMessage = result["errorMessage"]; + downloadItemCompleter.completeError("$errorMessage (error code $errorCode)"); } - received = data; - final percentage = (received / length) * 100; - _onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options); - }, - deleteOnError: false, - options: Options( - validateStatus: (statusCode) { - if(statusCode == 200) { - return true; - } + timer.cancel(); + return; + } - if(statusCode == 403 || statusCode == 503) { - throw _deniedConnectionError; - } + final speed = int.parse(result["downloadSpeed"] ?? "0"); + final completedLength = int.parse(files[0]["completedLength"] ?? "0"); + final totalLength = int.parse(files[0]["length"] ?? "0"); - if(statusCode == 404) { - throw _unavailableError; - } + final percentage = completedLength * 100 / totalLength; + final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round(); + _onProgress( + options.port, + percentage, + speed, + minutesLeft, + false + ); + }catch(error) { + throw "Invalid download status (${error})"; + } + }); - throw _genericError; - }, - headers: byteStart == null || byteStart <= 0 ? { - "Cookie": "_c_t_c=1" - } : { - "Cookie": "_c_t_c=1", - "Range": "bytes=${byteStart}-" - }, - ) - ); + await Future.any([stopped.future, downloadItemCompleter.future]); + if(!stopped.isCompleted) { + final extension = path.extension(fileName); + await _extractArchive(stopped, extension, await downloadItemCompleter.future, options); + }else { + await _stopAriaDownload(downloadId); + } }catch(error) { - if(stopped.isCompleted) { - return; - } - - if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) { - _onError(error, options); - return; - } - - await _downloadArchive(options, stopped, tempFile, startTime, received, errorsCount + 1); + _onError(error, options); + }finally { + delete(outputFile); } } +Future _startAriaServer() async { + final running = await _isAriaRunning(); + if(running) { + await killProcessByPort(_ariaPort); + } + + final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe"); + if(!aria2c.existsSync()) { + throw "Missing aria2c.exe"; + } + + await startProcess( + executable: aria2c, + args: [ + "--max-connection-per-server=${Platform.numberOfProcessors}", + "--split=${Platform.numberOfProcessors}", + "--enable-rpc", + "--rpc-listen-all=true", + "--rpc-allow-origin-all", + "--rpc-secret=$_ariaSecret", + "--rpc-listen-port=$_ariaPort" + ], + window: false + ); + for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) { + if(await _isAriaRunning()) { + return; + } + await Future.delayed(const Duration(seconds: 1)); + } + throw "cannot start download server (timeout exceeded)"; +} + +Future _isAriaRunning() async { + try { + final statusRequestId = Uuid().toString().replaceAll("-", ""); + final statusRequest = { + "jsonrcp": "2.0", + "id": statusRequestId, + "method": "aria2.getVersion", + "params": [ + "token:${_ariaSecret}" + ] + }; + await http.post(_ariaEndpoint, body: jsonEncode(statusRequest)); + return true; + }catch(_) { + return false; + } +} + +Future _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async { + http.Response? addDownloadResponse; + try { + final addDownloadRequestId = Uuid().toString().replaceAll("-", ""); + final addDownloadRequest = { + "jsonrcp": "2.0", + "id": addDownloadRequestId, + "method": "aria2.addUri", + "params": [ + "token:${_ariaSecret}", + [options.build.link], + { + "dir": outputFile.parent.path, + "out": path.basename(outputFile.path) + } + ] + }; + addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest)); + final addDownloadResponseJson = jsonDecode(addDownloadResponse.body); + final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null; + if(downloadId == null) { + throw "Start failed (${addDownloadResponse.body})"; + } + + return downloadId; + }catch(error) { + throw "Start failed (${addDownloadResponse?.body ?? error})"; + } +} + +Future _stopAriaDownload(String downloadId) async { + try { + final addDownloadRequestId = Uuid().toString().replaceAll("-", ""); + final addDownloadRequest = { + "jsonrcp": "2.0", + "id": addDownloadRequestId, + "method": "aria2.forceRemove", + "params": [ + "token:${_ariaSecret}", + downloadId + ] + }; + await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest)); + }catch(error) { + throw "Stop failed (${error})"; + } +} + + Future _extractArchive(Completer stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { - final startTime = DateTime.now().millisecondsSinceEpoch; Process? process; switch (extension.toLowerCase()) { case ".zip": final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe"); if(!sevenZip.existsSync()) { - throw "Corrupted installation: missing 7zip.exe"; + throw "Missing 7zip.exe"; } process = await startProcess( @@ -176,10 +254,15 @@ Future _extractArchive(Completer stopped, String extension, File ); var completed = false; process.stdOutput.listen((data) { - final now = DateTime.now().millisecondsSinceEpoch; if(data.toLowerCase().contains("everything is ok")) { completed = true; - _onProgress(startTime, now, 100, true, options); + _onProgress( + options.port, + 100, + 0, + -1, + true + ); process?.kill(ProcessSignal.sigabrt); return; } @@ -190,7 +273,13 @@ Future _extractArchive(Completer stopped, String extension, File } final percentage = int.parse(element.substring(0, element.length - 1)).toDouble(); - _onProgress(startTime, now, percentage, true, options); + _onProgress( + options.port, + percentage, + 0, + -1, + true + ); }); process.stdError.listen((data) { if(!data.isBlank) { @@ -206,7 +295,7 @@ Future _extractArchive(Completer stopped, String extension, File case ".rar": final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); if(!winrar.existsSync()) { - throw "Corrupted installation: missing winrar.exe"; + throw "Missing winrar.exe"; } process = await startProcess( @@ -221,11 +310,16 @@ Future _extractArchive(Completer stopped, String extension, File ); var completed = false; process.stdOutput.listen((data) { - final now = DateTime.now().millisecondsSinceEpoch; data = data.replaceAll("\r", "").replaceAll("\b", "").trim(); if(data == "All OK") { completed = true; - _onProgress(startTime, now, 100, true, options); + _onProgress( + options.port, + 100, + 0, + -1, + true + ); process?.kill(ProcessSignal.sigabrt); return; } @@ -236,7 +330,13 @@ Future _extractArchive(Completer stopped, String extension, File } final percentage = int.parse(element).toDouble(); - _onProgress(startTime, now, percentage, true, options); + _onProgress( + options.port, + percentage, + 0, + -1, + true + ); }); process.stdError.listen((data) { if(!data.isBlank) { @@ -257,21 +357,22 @@ Future _extractArchive(Completer stopped, String extension, File process.kill(ProcessSignal.sigabrt); } -void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) { +void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) { if(percentage == 0) { - options.port.send(FortniteBuildDownloadProgress( + port.send(FortniteBuildDownloadProgress( progress: percentage, - extracting: extracting + extracting: extracting, + timeLeft: null, + speed: speed )); return; } - final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now; - final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round(); - options.port.send(FortniteBuildDownloadProgress( + port.send(FortniteBuildDownloadProgress( progress: percentage, extracting: extracting, - minutesLeft: minutesLeft + timeLeft: minutesLeft, + speed: speed )); } @@ -291,4 +392,5 @@ Completer _setupLifecycle(FortniteBuildDownloadOptions options) { }); options.port.send(lifecyclePort.sendPort); return stopped; -} \ No newline at end of file +} + diff --git a/common/lib/src/util/dll.dart b/common/lib/src/util/dll.dart index 4579a99..ffe6253 100644 --- a/common/lib/src/util/dll.dart +++ b/common/lib/src/util/dll.dart @@ -9,9 +9,9 @@ bool _watcher = false; final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll"); final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll"); const String kRebootBelowS20DownloadUrl = - "http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip"; + "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip"; const String kRebootAboveS20DownloadUrl = - "http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/aboveS20/Release.zip"; + "https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip"; Future hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async { final lastUpdate = await _getLastUpdate(lastUpdateMs); diff --git a/common/lib/src/util/process.dart b/common/lib/src/util/process.dart index 7f90f9b..d7e526a 100644 --- a/common/lib/src/util/process.dart +++ b/common/lib/src/util/process.dart @@ -8,10 +8,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:ffi/ffi.dart'; -import 'package:path/path.dart' as path; import 'package:reboot_common/common.dart'; -import 'package:reboot_common/src/util/log.dart'; -import 'package:sync/semaphore.dart'; import 'package:win32/win32.dart'; final _ntdll = DynamicLibrary.open('ntdll.dll'); @@ -98,8 +95,8 @@ Future startElevatedProcess({required String executable, required String a var shellInput = calloc(); shellInput.ref.lpFile = executable.toNativeUtf16(); shellInput.ref.lpParameters = args.toNativeUtf16(); - shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE; - shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; + shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE; + shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED; shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.cbSize = sizeOf(); return ShellExecuteEx(shellInput) == 1; @@ -154,47 +151,36 @@ final _NtSuspendProcess = _ntdll.lookupFunction('NtSuspendProcess'); bool suspend(int pid) { - final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); - final result = _NtSuspendProcess(processHandle); - CloseHandle(processHandle); - return result == 0; + final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid); + try { + return _NtSuspendProcess(processHandle) == 0; + } finally { + CloseHandle(processHandle); + } } bool resume(int pid) { - final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); - final result = _NtResumeProcess(processHandle); - CloseHandle(processHandle); - return result == 0; + final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid); + try { + return _NtResumeProcess(processHandle) == 0; + } finally { + CloseHandle(processHandle); + } } -void _watchProcess(int pid) { - final processHandle = OpenProcess(SYNCHRONIZE, FALSE, 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); } -} - -Future watchProcess(int pid) async { - var completer = Completer(); - var exitPort = ReceivePort(); - exitPort.listen((_) { - if(!completer.isCompleted) { - completer.complete(true); - } - }); - var errorPort = ReceivePort(); - errorPort.listen((_) => completer.complete(false)); - await Isolate.spawn( - _watchProcess, - pid, - onExit: exitPort.sendPort, - onError: errorPort.sendPort, - errorsAreFatal: true - ); - return await completer.future; -} +}); List createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) { log("[PROCESS] Generating reboot args"); diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 8b527ff..5cb2d84 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -7,7 +7,6 @@ environment: sdk: ">=3.0.0 <=4.0.0" dependencies: - dio: ^5.7.0 win32: ^5.5.4 ffi: ^2.1.3 path: ^1.9.0 @@ -17,7 +16,7 @@ dependencies: ini: ^2.1.0 shelf_proxy: ^1.0.2 sync: ^0.3.0 - uuid: ^3.0.6 + uuid: ^4.5.1 shelf_web_socket: ^2.0.0 version: ^3.0.2 diff --git a/gui/assets/build/aria2c.exe b/gui/assets/build/aria2c.exe new file mode 100644 index 0000000..5004e10 Binary files /dev/null and b/gui/assets/build/aria2c.exe differ diff --git a/gui/assets/build/stop.bat b/gui/assets/build/stop.bat deleted file mode 100644 index 8c1961b..0000000 --- a/gui/assets/build/stop.bat +++ /dev/null @@ -1,2 +0,0 @@ -taskkill /f /im winrar.exe -taskkill /f /im tar.exe \ No newline at end of file diff --git a/gui/dependencies/dlls/cobalt.dll b/gui/dependencies/dlls/cobalt.dll deleted file mode 100644 index c29ba0c..0000000 Binary files a/gui/dependencies/dlls/cobalt.dll and /dev/null differ diff --git a/gui/dependencies/dlls/memory.dll b/gui/dependencies/dlls/memory.dll deleted file mode 100644 index c29e42e..0000000 Binary files a/gui/dependencies/dlls/memory.dll and /dev/null differ diff --git a/gui/lib/l10n/reboot_en.arb b/gui/lib/l10n/reboot_en.arb index 365d1b6..cf0360f 100644 --- a/gui/lib/l10n/reboot_en.arb +++ b/gui/lib/l10n/reboot_en.arb @@ -76,7 +76,7 @@ "playGameServerCustomContent": "Enter IP", "settingsName": "Settings", "settingsClientName": "Internal files", - "settingsClientDescription": "Configure the internal files used by the launcher for Fortnite", + "settingsClientDescription": "Configure the internal files used by the launcher", "settingsClientOptionsName": "Options", "settingsClientOptionsDescription": "Configure additional options for Fortnite", "settingsClientConsoleName": "Unreal engine patcher", @@ -94,20 +94,19 @@ "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", "settingsServerOptionsName": "Options", "settingsServerOptionsSubtitle": "Configure additional options for the game server", - "settingsServerTypeName": "Type", + "settingsServerTypeName": "Game server type", "settingsServerTypeDescription": "The type of game server to inject", "settingsServerTypeEmbeddedName": "Embedded", "settingsServerTypeCustomName": "Custom", - "settingsOldServerFileName": "Game server (Before Fortnite Season 20)", - "settingsNewServerFileName": "Game server (After Fortnite Season 20)", + "settingsOldServerFileName": "Game server", "settingsServerFileDescription": "The file injected to create the game server", "settingsServerPortName": "Port", "settingsServerPortDescription": "The port the launcher expects the game server to be hosted on", - "settingsServerOldMirrorName": "Update mirror (Before Fortnite Season 20)", - "settingsServerNewMirrorName": "Update mirror (After Fortnite Season 20)", + "settingsServerOldMirrorName": "Update mirror (Before season 20)", + "settingsServerNewMirrorName": "Update mirror (Season 20 and above)", "settingsServerMirrorDescription": "The URL used to update the game server dll", "settingsServerMirrorPlaceholder": "mirror", - "settingsServerTimerName": "Update timer", + "settingsServerTimerName": "Game server updater", "settingsServerTimerSubtitle": "Determines when the game server should be updated", "settingsUtilsName": "Launcher", "settingsUtilsSubtitle": "This section contains settings related to the launcher", @@ -217,6 +216,8 @@ "downloadedVersion": "The download was completed successfully!", "download": "Download", "downloading": "Downloading...", + "allocatingSpace": "Allocating disk space...", + "startingDownload": "Starting download...", "extracting": "Extracting...", "buildProgress": "{progress}%", "buildInstallationDirectory": "Installation directory", @@ -326,6 +327,8 @@ "backendErrorMessage": "The backend reported an unexpected error", "welcomeTitle": "Welcome to Reboot Launcher", "welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab", + "hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.", + "hostAccountAction": "I understand", "welcomeAction": "Take the tour", "startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.", "startOnboardingActionLabel": "Let's do it", diff --git a/gui/lib/main.dart b/gui/lib/main.dart index bf29107..6dfd16b 100644 --- a/gui/lib/main.dart +++ b/gui/lib/main.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; @@ -154,23 +152,24 @@ Future _initUrlHandler() async { } } -void _initWindow() => doWhenWindowReady(() async { +Future _initWindow() async { try { await SystemTheme.accentColor.load(); await windowManager.ensureInitialized(); await Window.initialize(); var settingsController = Get.find(); var size = Size(settingsController.width, settingsController.height); - appWindow.size = size; + await windowManager.setSize(size); var offsetX = settingsController.offsetX; var offsetY = settingsController.offsetY; - if(offsetX != null && offsetY != null){ - appWindow.position = Offset( + if(offsetX != null && offsetY != null) { + final position = Offset( offsetX, offsetY ); + await windowManager.setPosition(position); }else { - appWindow.alignment = Alignment.center; + await windowManager.setAlignment(Alignment.center); } if(isWin11) { @@ -183,9 +182,9 @@ void _initWindow() => doWhenWindowReady(() async { }catch(error, stackTrace) { onError(error, stackTrace, false); }finally { - appWindow.show(); + windowManager.show(); } -}); +} Future> _initStorage() async { final errors = []; diff --git a/gui/lib/src/controller/backend_controller.dart b/gui/lib/src/controller/backend_controller.dart index 48e53cf..88ae028 100644 --- a/gui/lib/src/controller/backend_controller.dart +++ b/gui/lib/src/controller/backend_controller.dart @@ -10,7 +10,7 @@ import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/src/util/keyboard.dart'; class BackendController extends GetxController { - static const String storageName = "backend_storage"; + static const String storageName = "v2_backend_storage"; static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); late final GetStorage? _storage; diff --git a/gui/lib/src/controller/dll_controller.dart b/gui/lib/src/controller/dll_controller.dart index d6f588c..9865f5b 100644 --- a/gui/lib/src/controller/dll_controller.dart +++ b/gui/lib/src/controller/dll_controller.dart @@ -9,16 +9,16 @@ import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:version/version.dart'; class DllController extends GetxController { - static const String storageName = "dll_storage"; + static const String storageName = "v2_dll_storage"; late final GetStorage? _storage; late final String originalDll; late final TextEditingController gameServerDll; late final TextEditingController unrealEngineConsoleDll; late final TextEditingController backendDll; - late final TextEditingController memoryLeakDll; late final TextEditingController gameServerPort; late final Rx timer; late final TextEditingController beforeS20Mirror; @@ -33,8 +33,7 @@ class DllController extends GetxController { _storage = appWithNoStorage ? null : GetStorage(storageName); gameServerDll = _createController("game_server", InjectableDll.reboot); unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console); - backendDll = _createController("backend", InjectableDll.cobalt); - memoryLeakDll = _createController("memory_leak", InjectableDll.memory); + backendDll = _createController("backend", InjectableDll.starfall); gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort); gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text)); final timerIndex = _storage?.read("timer"); @@ -60,8 +59,7 @@ class DllController extends GetxController { void resetGame() { gameServerDll.text = getDefaultDllPath(InjectableDll.reboot); unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console); - backendDll.text = getDefaultDllPath(InjectableDll.cobalt); - memoryLeakDll.text = getDefaultDllPath(InjectableDll.memory); + backendDll.text = getDefaultDllPath(InjectableDll.starfall); } void resetServer() { @@ -155,27 +153,21 @@ class DllController extends GetxController { } } - (File, bool) getInjectableData(InjectableDll dll) { + (File, bool) getInjectableData(Version version, InjectableDll dll) { final defaultPath = canonicalize(getDefaultDllPath(dll)); switch(dll){ case InjectableDll.reboot: if(customGameServer.value) { - final file = File(gameServerDll.text); - if(file.existsSync()) { - return (file, true); - } + return (File(gameServerDll.text), true); } - return (rebootBeforeS20DllFile, false); + return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false); case InjectableDll.console: final ue4ConsoleFile = File(unrealEngineConsoleDll.text); return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath); - case InjectableDll.cobalt: + case InjectableDll.starfall: final backendFile = File(backendDll.text); return (backendFile, canonicalize(backendFile.path) != defaultPath); - case InjectableDll.memory: - final memoryLeakFile = File(memoryLeakDll.text); - return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath); } } @@ -187,7 +179,7 @@ class DllController extends GetxController { log("[DLL] File name: $fileName"); InfoBarEntry? entry; try { - if (fileName == "reboot.dll") { + if (fileName.contains("reboot")) { log("[DLL] Downloading reboot.dll..."); return await updateGameServerDll( silent: silent diff --git a/gui/lib/src/controller/game_controller.dart b/gui/lib/src/controller/game_controller.dart index dc922d3..2a9350f 100644 --- a/gui/lib/src/controller/game_controller.dart +++ b/gui/lib/src/controller/game_controller.dart @@ -8,7 +8,7 @@ import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/main.dart'; class GameController extends GetxController { - static const String storageName = "game_storage"; + static const String storageName = "v2_game_storage"; late final GetStorage? _storage; late final TextEditingController username; diff --git a/gui/lib/src/controller/hosting_controller.dart b/gui/lib/src/controller/hosting_controller.dart index ca63d4e..b377887 100644 --- a/gui/lib/src/controller/hosting_controller.dart +++ b/gui/lib/src/controller/hosting_controller.dart @@ -12,10 +12,12 @@ import 'package:sync/semaphore.dart'; import 'package:uuid/uuid.dart'; class HostingController extends GetxController { - static const String storageName = "hosting_storage"; + static const String storageName = "v2_hosting_storage"; late final GetStorage? _storage; late final String uuid; + late final TextEditingController accountUsername; + late final TextEditingController accountPassword; late final TextEditingController name; late final FocusNode nameFocusNode; late final TextEditingController description; @@ -37,6 +39,10 @@ class HostingController extends GetxController { _storage = appWithNoStorage ? null : GetStorage(storageName); uuid = _storage?.read("uuid") ?? const Uuid().v4(); _storage?.write("uuid", uuid); + accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName); + accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text)); + accountPassword = TextEditingController(text: _storage?.read("account_password") ?? ""); + accountPassword.addListener(() => _storage?.write("account_password", password.text)); name = TextEditingController(text: _storage?.read("name")); name.addListener(() => _storage?.write("name", name.text)); description = TextEditingController(text: _storage?.read("description")); @@ -152,6 +158,8 @@ class HostingController extends GetxController { } void reset() { + accountUsername.text = kDefaultHostName; + accountPassword.text = ""; name.text = ""; description.text = ""; showPassword.value = false; diff --git a/gui/lib/src/controller/settings_controller.dart b/gui/lib/src/controller/settings_controller.dart index 5a2d4f7..955899f 100644 --- a/gui/lib/src/controller/settings_controller.dart +++ b/gui/lib/src/controller/settings_controller.dart @@ -13,7 +13,7 @@ import 'package:version/version.dart'; import 'package:yaml/yaml.dart'; class SettingsController extends GetxController { - static const String storageName = "settings_storage"; + static const String storageName = "v2_settings_storage"; late final GetStorage? _storage; late final RxString language; diff --git a/gui/lib/src/messenger/implementation/onboard.dart b/gui/lib/src/messenger/implementation/onboard.dart index 77425e7..aee3536 100644 --- a/gui/lib/src/messenger/implementation/onboard.dart +++ b/gui/lib/src/messenger/implementation/onboard.dart @@ -17,6 +17,7 @@ import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart'; void startOnboarding() { + final gameController = Get.find(); final settingsController = Get.find(); settingsController.firstRun.value = false; profileOverlayKey.currentState!.showOverlay( @@ -27,7 +28,7 @@ void startOnboarding() { label: translations.startOnboardingActionLabel, onTap: () async { onClose(); - await showProfileForm(context); + await showProfileForm(context, gameController.username, gameController.password); _promptPlayPage(); } ) @@ -78,6 +79,22 @@ void _promptServerBrowserPage() { context: context, label: translations.promptServerBrowserPageActionLabel, onTap: () { + onClose(); + _promptHostAccount(); + } + ) + ); +} + +void _promptHostAccount() { + pageIndex.value = RebootPageType.host.index; + profileOverlayKey.currentState!.showOverlay( + text: translations.hostAccountText, + offset: Offset(27.5, 17.5), + actionBuilder: (context, onClose) => _buildActionButton( + context: context, + label: translations.hostAccountAction, + onTap: () async { onClose(); _promptHostPage(); } @@ -86,7 +103,6 @@ void _promptServerBrowserPage() { } void _promptHostPage() { - pageIndex.value = RebootPageType.host.index; pageOverlayTargetKey.currentState!.showOverlay( text: translations.promptHostPageText, actionBuilder: (context, onClose) => _buildActionButton( diff --git a/gui/lib/src/messenger/implementation/profile.dart b/gui/lib/src/messenger/implementation/profile.dart index 9e2847f..d9ce207 100644 --- a/gui/lib/src/messenger/implementation/profile.dart +++ b/gui/lib/src/messenger/implementation/profile.dart @@ -6,13 +6,11 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -final GameController _gameController = Get.find(); - -Future showProfileForm(BuildContext context) async{ +Future showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{ final showPassword = RxBool(false); - final oldUsername = _gameController.username.text; + final oldUsername = username.text; final showPasswordTrailing = RxBool(oldUsername.isNotEmpty); - final oldPassword = _gameController.password.text; + final oldPassword = password.text; final result = await showRebootDialog( builder: (context) => Obx(() => FormDialog( content: Column( @@ -25,17 +23,17 @@ Future showProfileForm(BuildContext context) async{ child: TextFormBox( placeholder: translations.usernameOrEmailPlaceholder, validator: (text) { - if(_gameController.password.text.isEmpty) { + if(password.text.isEmpty) { return null; } - if(EmailValidator.validate(_gameController.username.text)) { + if(EmailValidator.validate(username.text)) { return null; } return translations.invalidEmail; }, - controller: _gameController.username, + controller: username, autovalidateMode: AutovalidateMode.always, enableSuggestions: true, autofocus: true, @@ -47,7 +45,7 @@ Future showProfileForm(BuildContext context) async{ label: translations.password, child: TextFormBox( placeholder: translations.passwordPlaceholder, - controller: _gameController.password, + controller: password, autovalidateMode: AutovalidateMode.always, obscureText: !showPassword.value, enableSuggestions: false, @@ -87,7 +85,7 @@ Future showProfileForm(BuildContext context) async{ return true; } - _gameController.username.text = oldUsername; - _gameController.password.text = oldPassword; + username.text = oldUsername; + password.text = oldPassword; return false; } diff --git a/gui/lib/src/messenger/implementation/version.dart b/gui/lib/src/messenger/implementation/version.dart index 4f09453..0deb5e2 100644 --- a/gui/lib/src/messenger/implementation/version.dart +++ b/gui/lib/src/messenger/implementation/version.dart @@ -33,16 +33,15 @@ class _AddVersionDialogState extends State { final Rxn _build = Rxn(); final RxnInt _timeLeft = RxnInt(); final Rxn _progress = Rxn(); + final RxInt _speed = RxInt(0); late DiskSpace _diskSpace; late Future> _fetchFuture; late Future _diskFuture; - Isolate? _isolate; SendPort? _downloadPort; Object? _error; StackTrace? _stackTrace; - bool _selecting = false; @override void initState() { @@ -61,9 +60,8 @@ class _AddVersionDialogState extends State { } void _cancelDownload() { - Process.run('${assetsDirectory.path}\\build\\stop.bat', []); _downloadPort?.send(kStopBuildDownloadSignal); - _isolate?.kill(priority: Isolate.immediate); + WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } @override @@ -158,7 +156,7 @@ class _AddVersionDialogState extends State { final communicationPort = ReceivePort(); communicationPort.listen((message) { if(message is FortniteBuildDownloadProgress) { - _onProgress(build, message.progress, message.minutesLeft, message.extracting); + _onProgress(build, message); }else if(message is SendPort) { _downloadPort = message; }else { @@ -172,7 +170,7 @@ class _AddVersionDialogState extends State { ); final errorPort = ReceivePort(); errorPort.listen((message) => _onDownloadError(message, null)); - _isolate = await Isolate.spawn( + await Isolate.spawn( downloadArchiveBuild, options, onError: errorPort.sendPort, @@ -212,23 +210,24 @@ class _AddVersionDialogState extends State { _stackTrace = stackTrace; } - void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) { + void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) { if (!mounted) { return; } - if(progress >= 100 && extracting) { + if(message.progress >= 100 && message.extracting) { _onDownloadComplete(build); return; } - _status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading; - if(progress >= 0) { - WindowsTaskbar.setProgress(progress.round(), 100); + _status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading; + if(message.progress >= 0) { + WindowsTaskbar.setProgress(message.progress.round(), 100); } - _timeLeft.value = timeLeft; - _progress.value = progress; + _timeLeft.value = message.timeLeft; + _progress.value = message.progress; + _speed.value = message.speed; } Widget get _progressBody { @@ -239,31 +238,33 @@ class _AddVersionDialogState extends State { Align( alignment: Alignment.centerLeft, child: Text( - _status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting, + _statusText, style: FluentTheme.maybeOf(context)?.typography.body, textAlign: TextAlign.start, ), ), - const SizedBox( - height: 8.0, - ), + if(_progress.value != null && !_isAllocatingDiskSpace) + const SizedBox( + height: 8.0, + ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - translations.buildProgress((_progress.value ?? 0).round()), - style: FluentTheme.maybeOf(context)?.typography.body, - ), - - if(timeLeft != null) + if(_progress.value != null && !_isAllocatingDiskSpace) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Text( - translations.timeLeft(timeLeft), + translations.buildProgress((_progress.value ?? 0).round()), style: FluentTheme.maybeOf(context)?.typography.body, - ) - ], - ), + ), + + if(timeLeft != null) + Text( + translations.timeLeft(timeLeft), + style: FluentTheme.maybeOf(context)?.typography.body, + ) + ], + ), const SizedBox( height: 8.0, @@ -271,7 +272,7 @@ class _AddVersionDialogState extends State { SizedBox( width: double.infinity, - child: ProgressBar(value: (_progress.value ?? 0).toDouble()) + child: ProgressBar(value: _isAllocatingDiskSpace ? null : _progress.value?.toDouble()) ), const SizedBox( @@ -281,6 +282,24 @@ class _AddVersionDialogState extends State { ); } + String get _statusText { + if (_status.value != _DownloadStatus.downloading) { + return translations.extracting; + } + + if (_progress.value == null) { + return translations.startingDownload; + } + + if (_speed.value == 0) { + return translations.allocatingSpace; + } + + return translations.downloading; + } + + bool get _isAllocatingDiskSpace => _status.value == _DownloadStatus.downloading && _speed.value == 0; + Widget _buildFormBody(List builds) { return Column( mainAxisSize: MainAxisSize.min, diff --git a/gui/lib/src/page/implementation/home_page.dart b/gui/lib/src/page/implementation/home_page.dart index e7d9da5..b518512 100644 --- a/gui/lib/src/page/implementation/home_page.dart +++ b/gui/lib/src/page/implementation/home_page.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:ui'; import 'package:app_links/app_links.dart'; -import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show MaterialPage; @@ -27,6 +26,7 @@ import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/widget/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/profile_tile.dart'; import 'package:reboot_launcher/src/widget/title_bar.dart'; +import 'package:version/version.dart'; import 'package:window_manager/window_manager.dart'; final GlobalKey profileOverlayKey = GlobalKey(); @@ -58,7 +58,6 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void initState() { super.initState(); - windowManager.setPreventClose(true); windowManager.addListener(this); _syncPageViewWithNavigator(); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -111,7 +110,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA return; } - var result = await pingGameServer(address); + final result = await pingGameServer(address); if(result) { return; } @@ -135,13 +134,12 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA dllsDirectory.createSync(recursive: true); } + final dummy = Version.parse("1"); + final dummyS20 = Version.parse("20"); for(final injectable in InjectableDll.values) { - final (file, custom) = _dllController.getInjectableData(injectable); - if(!custom) { - _dllController.downloadCriticalDllInteractive( - file.path, - silent: true - ); + _downloadDll(dummy, injectable); + if(injectable.isVersionDependent) { + _downloadDll(dummyS20, injectable); } } @@ -150,12 +148,22 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA })); } + void _downloadDll(Version version, InjectableDll injectable) { + final (file, custom) = _dllController.getInjectableData(version, injectable); + if(!custom) { + _dllController.downloadCriticalDllInteractive( + file.path, + silent: false + ); + } + } + @override void onWindowClose() async { try { await _hostingController.discardServer(); - }finally { - exit(0); // Force closing + }catch(error) { + log("[HOSTING] Cannot discard server: $error"); } } @@ -220,14 +228,18 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA @override void onWindowResized() { - _settingsController.saveWindowSize(appWindow.size); _focused.value = true; + windowManager.getSize().then((size) { + _settingsController.saveWindowSize(size); + }); } @override void onWindowMoved() { - _settingsController.saveWindowOffset(appWindow.position); _focused.value = true; + windowManager.getPosition().then((position) { + _settingsController.saveWindowOffset(position); + }); } @override @@ -449,9 +461,12 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileWidget( - overlayKey: profileOverlayKey - ), + Obx(() { + pageIndex.value; + return ProfileWidget( + overlayKey: profileOverlayKey + ); + }), _autoSuggestBox, const SizedBox(height: 12.0), _buildNavigationTrail() @@ -554,7 +569,7 @@ class _HomePageState extends State with WindowListener, AutomaticKeepA ); GestureDetector get _draggableArea => GestureDetector( - onDoubleTap: appWindow.maximizeOrRestore, + onDoubleTap: windowManager.maximizeOrRestore, onHorizontalDragStart: (_) => windowManager.startDragging(), onVerticalDragStart: (_) => windowManager.startDragging() ); diff --git a/gui/lib/src/page/implementation/host_page.dart b/gui/lib/src/page/implementation/host_page.dart index 44aea3c..df5962e 100644 --- a/gui/lib/src/page/implementation/host_page.dart +++ b/gui/lib/src/page/implementation/host_page.dart @@ -53,7 +53,6 @@ class HostPage extends RebootPage { class _HostingPageState extends RebootPageState { final GameController _gameController = Get.find(); final HostingController _hostingController = Get.find(); - final SettingsController _settingsController = Get.find(); final DllController _dllController = Get.find(); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); @@ -85,7 +84,6 @@ class _HostingPageState extends RebootPageState { key: hostVersionOverlayTargetKey ), _options, - _internalFiles, _share, _resetDefaults ]; @@ -270,220 +268,6 @@ class _HostingPageState extends RebootPageState { ], ); - SettingTile get _internalFiles => SettingTile( - icon: Icon( - FluentIcons.archive_settings_24_regular - ), - title: Text(translations.settingsServerName), - subtitle: Text(translations.settingsServerSubtitle), - children: [ - _internalFilesServerType, - _internalFilesUpdateTimer, - _internalFilesOldServerSource, - _internalFilesNewServerSource, - ], - ); - - Widget get _internalFilesServerType => SettingTile( - icon: Icon( - FluentIcons.games_24_regular - ), - title: Text(translations.settingsServerTypeName), - subtitle: Text(translations.settingsServerTypeDescription), - contentWidth: SettingTile.kDefaultContentWidth + 30, - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), - items: { - false: translations.settingsServerTypeEmbeddedName, - true: translations.settingsServerTypeCustomName - }.entries.map((entry) => MenuFlyoutItem( - text: Text(entry.value), - onPressed: () { - final oldValue = _dllController.customGameServer.value; - if(oldValue == entry.key) { - return; - } - - _dllController.customGameServer.value = entry.key; - _dllController.infoBarEntry?.close(); - if(!entry.key) { - _dllController.updateGameServerDll( - force: true - ); - } - } - )).toList() - )) - ); - - Widget get _internalFilesOldServerSource => Obx(() { - if(!_dllController.customGameServer.value) { - return SettingTile( - icon: Icon( - FluentIcons.globe_24_regular - ), - title: Text(translations.settingsServerOldMirrorName), - subtitle: Text(translations.settingsServerMirrorDescription), - contentWidth: SettingTile.kDefaultContentWidth + 30, - content: Row( - children: [ - Expanded( - child: TextFormBox( - placeholder: translations.settingsServerMirrorPlaceholder, - controller: _dllController.beforeS20Mirror, - onChanged: (value) { - if(Uri.tryParse(value) != null) { - _dllController.updateGameServerDll(force: true); - } - }, - ), - ), - const SizedBox(width: 8.0), - Button( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero) - ), - onPressed: () => _dllController.updateGameServerDll(force: true), - child: SizedBox.square( - dimension: 30, - child: Icon( - FluentIcons.arrow_download_24_regular - ), - ) - ), - const SizedBox(width: 8.0), - Button( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero) - ), - onPressed: () { - _dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl; - _dllController.updateGameServerDll(force: true); - }, - child: SizedBox.square( - dimension: 30, - child: Icon( - FluentIcons.arrow_reset_24_regular - ), - ) - ) - ], - ) - ); - }else { - return createFileSetting( - title: translations.settingsOldServerFileName, - description: translations.settingsServerFileDescription, - controller: _dllController.gameServerDll, - onReset: () { - final path = _dllController.getDefaultDllPath(InjectableDll.reboot); - _dllController.gameServerDll.text = path; - _dllController.downloadCriticalDllInteractive(path); - } - ); - } - }); - - Widget get _internalFilesNewServerSource => Obx(() { - if(!_dllController.customGameServer.value) { - return SettingTile( - icon: Icon( - FluentIcons.globe_24_regular - ), - title: Text(translations.settingsServerNewMirrorName), - subtitle: Text(translations.settingsServerMirrorDescription), - contentWidth: SettingTile.kDefaultContentWidth + 30, - content: Row( - children: [ - Expanded( - child: TextFormBox( - placeholder: translations.settingsServerMirrorPlaceholder, - controller: _dllController.aboveS20Mirror, - onChanged: (value) { - if(Uri.tryParse(value) != null) { - _dllController.updateGameServerDll(force: true); - } - }, - ), - ), - const SizedBox(width: 8.0), - Button( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero) - ), - onPressed: () => _dllController.updateGameServerDll(force: true), - child: SizedBox.square( - dimension: 30, - child: Icon( - FluentIcons.arrow_download_24_regular - ), - ) - ), - const SizedBox(width: 8.0), - Button( - style: ButtonStyle( - padding: WidgetStateProperty.all(EdgeInsets.zero) - ), - onPressed: () { - _dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl; - _dllController.updateGameServerDll(force: true); - }, - child: SizedBox.square( - dimension: 30, - child: Icon( - FluentIcons.arrow_reset_24_regular - ), - ) - ) - ], - ) - ); - }else { - return createFileSetting( - title: translations.settingsNewServerFileName, - description: translations.settingsServerFileDescription, - controller: _dllController.gameServerDll, - onReset: () { - final path = _dllController.getDefaultDllPath(InjectableDll.reboot); - _dllController.gameServerDll.text = path; - _dllController.downloadCriticalDllInteractive(path); - } - ); - } - }); - - Widget get _internalFilesUpdateTimer => Obx(() { - if(_dllController.customGameServer.value) { - return const SizedBox.shrink(); - } - - return SettingTile( - icon: Icon( - FluentIcons.timer_24_regular - ), - title: Text(translations.settingsServerTimerName), - subtitle: Text(translations.settingsServerTimerSubtitle), - contentWidth: SettingTile.kDefaultContentWidth + 30, - content: Obx(() => DropDownButton( - onOpen: () => inDialog = true, - onClose: () => inDialog = false, - leading: Text(_dllController.timer.value.text), - items: UpdateTimer.values.map((entry) => MenuFlyoutItem( - text: Text(entry.text), - onPressed: () { - _dllController.timer.value = entry; - _dllController.infoBarEntry?.close(); - _dllController.updateGameServerDll( - force: true - ); - } - )).toList() - )) - ); - }); - SettingTile get _share => SettingTile( icon: Icon( FluentIcons.link_24_regular @@ -554,8 +338,8 @@ class _HostingPageState extends RebootPageState { try { _hostingController.publishServer( - _gameController.username.text, - _hostingController.instance.value!.versionName + _hostingController.accountUsername.text, + _hostingController.instance.value!.version.toString() ); } catch(error) { _showCannotUpdateGameServer(error); @@ -589,14 +373,4 @@ class _HostingPageState extends RebootPageState { severity: InfoBarSeverity.success, duration: infoBarLongDuration ); -} - -extension _UpdateTimerExtension on UpdateTimer { - String get text { - if (this == UpdateTimer.never) { - return translations.updateGameServerDllNever; - } - - return translations.updateGameServerDllEvery(name); - } } \ No newline at end of file diff --git a/gui/lib/src/page/implementation/play_page.dart b/gui/lib/src/page/implementation/play_page.dart index 9a501f3..4f68074 100644 --- a/gui/lib/src/page/implementation/play_page.dart +++ b/gui/lib/src/page/implementation/play_page.dart @@ -1,16 +1,12 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:get/get.dart'; -import 'package:reboot_common/common.dart'; -import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/util/translations.dart'; -import 'package:reboot_launcher/src/widget/file_setting_tile.dart'; import 'package:reboot_launcher/src/widget/game_start_button.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/version_selector_tile.dart'; @@ -38,7 +34,6 @@ class PlayPage extends RebootPage { class _PlayPageState extends RebootPageState { final GameController _gameController = Get.find(); - final DllController _dllController = Get.find(); @override Widget? get button => LaunchButton( @@ -53,50 +48,9 @@ class _PlayPageState extends RebootPageState { key: gameVersionOverlayTargetKey ), _options, - _internalFiles, _resetDefaults ]; - SettingTile get _internalFiles => SettingTile( - icon: Icon( - FluentIcons.archive_settings_24_regular - ), - title: Text(translations.settingsClientName), - subtitle: Text(translations.settingsClientDescription), - children: [ - createFileSetting( - title: translations.settingsClientConsoleName, - description: translations.settingsClientConsoleDescription, - controller: _dllController.unrealEngineConsoleDll, - onReset: () { - final path = _dllController.getDefaultDllPath(InjectableDll.console); - _dllController.unrealEngineConsoleDll.text = path; - _dllController.downloadCriticalDllInteractive(path, force: true); - } - ), - createFileSetting( - title: translations.settingsClientAuthName, - description: translations.settingsClientAuthDescription, - controller: _dllController.backendDll, - onReset: () { - final path = _dllController.getDefaultDllPath(InjectableDll.cobalt); - _dllController.backendDll.text = path; - _dllController.downloadCriticalDllInteractive(path, force: true); - } - ), - createFileSetting( - title: translations.settingsClientMemoryName, - description: translations.settingsClientMemoryDescription, - controller: _dllController.memoryLeakDll, - onReset: () { - final path = _dllController.getDefaultDllPath(InjectableDll.memory); - _dllController.memoryLeakDll.text = path; - _dllController.downloadCriticalDllInteractive(path, force: true); - } - ), - ], - ); - SettingTile get _options => SettingTile( icon: Icon( FluentIcons.options_24_regular @@ -127,7 +81,6 @@ class _PlayPageState extends RebootPageState { content: Button( onPressed: () => showResetDialog(() { _gameController.reset(); - _dllController.resetGame(); }), child: Text(translations.gameResetDefaultsContent), ) diff --git a/gui/lib/src/page/implementation/settings_page.dart b/gui/lib/src/page/implementation/settings_page.dart index ae01871..6c14773 100644 --- a/gui/lib/src/page/implementation/settings_page.dart +++ b/gui/lib/src/page/implementation/settings_page.dart @@ -4,11 +4,13 @@ import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; +import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/util/translations.dart'; +import 'package:reboot_launcher/src/widget/file_setting_tile.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -33,6 +35,7 @@ class SettingsPage extends RebootPage { class _SettingsPageState extends RebootPageState { final SettingsController _settingsController = Get.find(); + final DllController _dllController = Get.find(); @override Widget? get button => null; @@ -41,9 +44,235 @@ class _SettingsPageState extends RebootPageState { List get settings => [ _language, _theme, + _internalFiles, _installationDirectory, ]; + SettingTile get _internalFiles => SettingTile( + icon: Icon( + FluentIcons.archive_settings_24_regular + ), + title: Text(translations.settingsClientName), + subtitle: Text(translations.settingsClientDescription), + children: [ + createFileSetting( + title: translations.settingsClientConsoleName, + description: translations.settingsClientConsoleDescription, + controller: _dllController.unrealEngineConsoleDll, + onReset: () { + final path = _dllController.getDefaultDllPath(InjectableDll.console); + _dllController.unrealEngineConsoleDll.text = path; + _dllController.downloadCriticalDllInteractive(path, force: true); + } + ), + createFileSetting( + title: translations.settingsClientAuthName, + description: translations.settingsClientAuthDescription, + controller: _dllController.backendDll, + onReset: () { + final path = _dllController.getDefaultDllPath(InjectableDll.starfall); + _dllController.backendDll.text = path; + _dllController.downloadCriticalDllInteractive(path, force: true); + } + ), + _internalFilesServerType, + _internalFilesUpdateTimer, + _internalFilesServerSource, + _internalFilesNewServerSource, + ], + ); + + Widget get _internalFilesServerType => SettingTile( + icon: Icon( + FluentIcons.games_24_regular + ), + title: Text(translations.settingsServerTypeName), + subtitle: Text(translations.settingsServerTypeDescription), + contentWidth: SettingTile.kDefaultContentWidth + 30, + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), + items: { + false: translations.settingsServerTypeEmbeddedName, + true: translations.settingsServerTypeCustomName + }.entries.map((entry) => MenuFlyoutItem( + text: Text(entry.value), + onPressed: () { + final oldValue = _dllController.customGameServer.value; + if(oldValue == entry.key) { + return; + } + + _dllController.customGameServer.value = entry.key; + _dllController.infoBarEntry?.close(); + if(!entry.key) { + _dllController.updateGameServerDll( + force: true + ); + } + } + )).toList() + )) + ); + + Widget get _internalFilesServerSource => Obx(() { + if(!_dllController.customGameServer.value) { + return SettingTile( + icon: Icon( + FluentIcons.globe_24_regular + ), + title: Text(translations.settingsServerOldMirrorName), + subtitle: Text(translations.settingsServerMirrorDescription), + contentWidth: SettingTile.kDefaultContentWidth + 30, + content: Row( + children: [ + Expanded( + child: TextFormBox( + placeholder: translations.settingsServerMirrorPlaceholder, + controller: _dllController.beforeS20Mirror, + onChanged: (value) { + if(Uri.tryParse(value) != null) { + _dllController.updateGameServerDll(force: true); + } + }, + ), + ), + const SizedBox(width: 8.0), + Button( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero) + ), + onPressed: () => _dllController.updateGameServerDll(force: true), + child: SizedBox.square( + dimension: 30, + child: Icon( + FluentIcons.arrow_download_24_regular + ), + ) + ), + const SizedBox(width: 8.0), + Button( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero) + ), + onPressed: () { + _dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl; + _dllController.updateGameServerDll(force: true); + }, + child: SizedBox.square( + dimension: 30, + child: Icon( + FluentIcons.arrow_reset_24_regular + ), + ) + ) + ], + ) + ); + }else { + return createFileSetting( + title: translations.settingsOldServerFileName, + description: translations.settingsServerFileDescription, + controller: _dllController.gameServerDll, + onReset: () { + final path = _dllController.getDefaultDllPath(InjectableDll.reboot); + _dllController.gameServerDll.text = path; + _dllController.downloadCriticalDllInteractive(path); + } + ); + } + }); + + Widget get _internalFilesNewServerSource => Obx(() { + if(!_dllController.customGameServer.value) { + return SettingTile( + icon: Icon( + FluentIcons.globe_24_regular + ), + title: Text(translations.settingsServerNewMirrorName), + subtitle: Text(translations.settingsServerMirrorDescription), + contentWidth: SettingTile.kDefaultContentWidth + 30, + content: Row( + children: [ + Expanded( + child: TextFormBox( + placeholder: translations.settingsServerMirrorPlaceholder, + controller: _dllController.aboveS20Mirror, + onChanged: (value) { + if(Uri.tryParse(value) != null) { + _dllController.updateGameServerDll(force: true); + } + }, + ), + ), + const SizedBox(width: 8.0), + Button( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero) + ), + onPressed: () => _dllController.updateGameServerDll(force: true), + child: SizedBox.square( + dimension: 30, + child: Icon( + FluentIcons.arrow_download_24_regular + ), + ) + ), + const SizedBox(width: 8.0), + Button( + style: ButtonStyle( + padding: WidgetStateProperty.all(EdgeInsets.zero) + ), + onPressed: () { + _dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl; + _dllController.updateGameServerDll(force: true); + }, + child: SizedBox.square( + dimension: 30, + child: Icon( + FluentIcons.arrow_reset_24_regular + ), + ) + ) + ], + ) + ); + }else { + return const SizedBox(); + } + }); + + Widget get _internalFilesUpdateTimer => Obx(() { + if(_dllController.customGameServer.value) { + return const SizedBox.shrink(); + } + + return SettingTile( + icon: Icon( + FluentIcons.timer_24_regular + ), + title: Text(translations.settingsServerTimerName), + subtitle: Text(translations.settingsServerTimerSubtitle), + contentWidth: SettingTile.kDefaultContentWidth + 30, + content: Obx(() => DropDownButton( + onOpen: () => inDialog = true, + onClose: () => inDialog = false, + leading: Text(_dllController.timer.value.text), + items: UpdateTimer.values.map((entry) => MenuFlyoutItem( + text: Text(entry.text), + onPressed: () { + _dllController.timer.value = entry; + _dllController.infoBarEntry?.close(); + _dllController.updateGameServerDll( + force: true + ); + } + )).toList() + )) + ); + }); + SettingTile get _language => SettingTile( icon: Icon( FluentIcons.local_language_24_regular @@ -111,4 +340,14 @@ extension _ThemeModeExtension on ThemeMode { return translations.light; } } +} + +extension _UpdateTimerExtension on UpdateTimer { + String get text { + if (this == UpdateTimer.never) { + return translations.updateGameServerDllNever; + } + + return translations.updateGameServerDllEvery(name); + } } \ No newline at end of file diff --git a/gui/lib/src/util/matchmaker.dart b/gui/lib/src/util/matchmaker.dart index 23349ae..b722c9d 100644 --- a/gui/lib/src/util/matchmaker.dart +++ b/gui/lib/src/util/matchmaker.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -5,39 +6,26 @@ import 'package:reboot_common/common.dart'; const Duration _timeout = Duration(seconds: 5); -Future pingGameServer(String address, {Duration? timeout}) async { - Future ping(String hostname, int port) async { - log("[MATCHMAKER] Pinging $hostname:$port"); - RawDatagramSocket? socket; - try { - socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); - await for (final event in socket) { - log("[MATCHMAKER] Event: $event"); - switch(event) { - case RawSocketEvent.read: - log("[MATCHMAKER] Success"); - return true; - case RawSocketEvent.write: - log("[MATCHMAKER] Sending data"); - final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA=="); - socket.send(dataToSend, InternetAddress(hostname), port); - case RawSocketEvent.readClosed: - case RawSocketEvent.closed: - return false; - } - } - - return false; - }catch(error) { - log("[MATCHMAKER] Error: $error"); - return false; - }finally { - socket?.close(); - } - } - +Completer pingGameServerOrTimeout(String address, Duration timeout) { + final completer = Completer(); final start = DateTime.now(); - var firstTime = true; + (() async { + while (!completer.isCompleted && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds) { + final result = await pingGameServer(address); + if(result) { + completer.complete(true); + }else { + await Future.delayed(_timeout); + } + } + if(!completer.isCompleted) { + completer.complete(false); + } + })(); + return completer; +} + +Future pingGameServer(String address) async { final split = address.split(":"); var hostname = split[0]; if(isLocalHost(hostname)) { @@ -45,19 +33,37 @@ Future pingGameServer(String address, {Duration? timeout}) async { } final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort); - while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) { - final result = await ping(hostname, port) - .timeout(_timeout, onTimeout: () => false); - if(result) { - return true; + return await _ping(hostname, port) + .timeout(_timeout, onTimeout: () => false); +} + + +Future _ping(String hostname, int port) async { + log("[MATCHMAKER] Pinging $hostname:$port"); + RawDatagramSocket? socket; + try { + socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0); + await for (final event in socket) { + log("[MATCHMAKER] Event: $event"); + switch(event) { + case RawSocketEvent.read: + log("[MATCHMAKER] Success"); + return true; + case RawSocketEvent.write: + log("[MATCHMAKER] Sending data"); + final dataToSend = base64Decode("AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABA=="); + socket.send(dataToSend, InternetAddress(hostname), port); + case RawSocketEvent.readClosed: + case RawSocketEvent.closed: + return false; + } } - if(firstTime) { - firstTime = false; - }else { - await Future.delayed(_timeout); - } + return false; + }catch(error) { + log("[MATCHMAKER] Error: $error"); + return false; + }finally { + socket?.close(); } - - return false; } \ No newline at end of file diff --git a/gui/lib/src/util/os.dart b/gui/lib/src/util/os.dart index fc22ab9..11bbb4a 100644 --- a/gui/lib/src/util/os.dart +++ b/gui/lib/src/util/os.dart @@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/scheduler.dart'; import 'package:win32/win32.dart'; +import 'package:window_manager/window_manager.dart'; final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))'); @@ -486,4 +487,8 @@ int _convertToHString(String string) { free(stringPtr); free(hString); } +} + +extension WindowManagerExtension on WindowManager { + Future maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize(); } \ No newline at end of file diff --git a/gui/lib/src/widget/file_setting_tile.dart b/gui/lib/src/widget/file_setting_tile.dart index 5da3d46..4591ec4 100644 --- a/gui/lib/src/widget/file_setting_tile.dart +++ b/gui/lib/src/widget/file_setting_tile.dart @@ -13,6 +13,7 @@ import 'package:reboot_launcher/src/widget/setting_tile.dart'; const double _kButtonDimensions = 30; const double _kButtonSpacing = 8; +// FIXME: If the user clicks on the reset button, the text field checker won't be called SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) { final obx = RxString(controller.text); controller.addListener(() => obx.value = controller.text); diff --git a/gui/lib/src/widget/game_start_button.dart b/gui/lib/src/widget/game_start_button.dart index 130c7e3..70534e8 100644 --- a/gui/lib/src/widget/game_start_button.dart +++ b/gui/lib/src/widget/game_start_button.dart @@ -12,7 +12,6 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/dll_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart'; -import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/implementation/server.dart'; @@ -22,6 +21,7 @@ import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/translations.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import 'package:version/version.dart'; class LaunchButton extends StatefulWidget { final bool host; @@ -41,12 +41,11 @@ class _LaunchButtonState extends State { final HostingController _hostingController = Get.find(); final BackendController _backendController = Get.find(); final DllController _dllController = Get.find(); - final SettingsController _settingsController = Get.find(); InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameServerInfoBar; CancelableOperation? _operation; - CancelableOperation? _pingOperation; + Completer? _pingOperation; IVirtualDesktop? _virtualDesktop; @override @@ -95,7 +94,7 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Set started"); log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); for (final injectable in InjectableDll.values) { - if(await _getDllFileOrStop(injectable, host) == null) { + if(await _getDllFileOrStop(version.content, injectable, host) == null) { return; } } @@ -230,7 +229,7 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); final instance = GameInstance( - versionName: version.content.toString(), + version: version.content, gamePid: gameProcess, launcherPid: launcherProcess, eacPid: eacProcess, @@ -243,7 +242,7 @@ class _LaunchButtonState extends State { }else{ _gameController.instance.value = instance; } - await _injectOrShowError(InjectableDll.sinum, host); + await _injectOrShowError(InjectableDll.starfall, host); log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance"); return instance; } @@ -251,8 +250,8 @@ class _LaunchButtonState extends State { Future _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async { log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); final gameArgs = createRebootArgs( - _gameController.username.text, - _gameController.password.text, + host ? _hostingController.accountUsername.text : _gameController.username.text, + host ? _hostingController.accountPassword.text :_gameController.password.text, host, hostType, false, @@ -399,7 +398,6 @@ class _LaunchButtonState extends State { if(instance != null && !instance.launched) { instance.launched = true; instance.tokenError = false; - await _injectOrShowError(InjectableDll.memory, host); if(!host){ await _injectOrShowError(InjectableDll.console, host); _onGameClientInjected(); @@ -438,11 +436,12 @@ class _LaunchButtonState extends State { duration: null ); final gameServerPort = _dllController.gameServerPort.text; - this._pingOperation = await CancelableOperation.fromFuture(pingGameServer( + final pingOperation = pingGameServerOrTimeout( "127.0.0.1:$gameServerPort", - timeout: const Duration(minutes: 2) - )); - final localPingResult = (await _pingOperation?.value) ?? false; + const Duration(minutes: 2) + ); + this._pingOperation = pingOperation; + final localPingResult = await pingOperation.future; _gameServerInfoBar?.close(); if (!localPingResult) { showRebootInfoBar( @@ -464,8 +463,8 @@ class _LaunchButtonState extends State { } await _hostingController.publishServer( - _gameController.username.text, - _hostingController.instance.value!.versionName, + _hostingController.accountUsername.text, + _hostingController.instance.value!.version.toString(), ); showRebootInfoBar( translations.gameServerStarted, @@ -485,18 +484,17 @@ class _LaunchButtonState extends State { duration: null ); final publicIp = await Ipify.ipv4(); - this._pingOperation = CancelableOperation.fromFuture(pingGameServer("$publicIp:$gameServerPort")); - final externalResult = (await _pingOperation?.value) ?? false; - if (externalResult) { + final available = await pingGameServer("$publicIp:$gameServerPort"); + if(available) { + _gameServerInfoBar?.close(); return true; } - _gameServerInfoBar?.close(); - this._pingOperation = CancelableOperation.fromFuture(pingGameServer( + final pingOperation = pingGameServerOrTimeout( "$publicIp:$gameServerPort", - timeout: const Duration(days: 365) - )); - final future = await _pingOperation?.value ?? false; + const Duration(days: 365) + ); + this._pingOperation = pingOperation; _gameServerInfoBar = showRebootInfoBar( translations.checkGameServerFixMessage(gameServerPort), action: Button( @@ -507,7 +505,9 @@ class _LaunchButtonState extends State { duration: null, loading: true ); - return await future; + final result = await pingOperation.future; + _gameServerInfoBar?.close(); + return result; }finally { _gameServerInfoBar?.close(); } @@ -515,8 +515,13 @@ class _LaunchButtonState extends State { Future _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { if(host == null) { - await _pingOperation?.cancel(); - _pingOperation = null; + try { + _pingOperation?.complete(false); + }catch(_) { + // Ignore: might be running, don't bother checking + } finally { + _pingOperation = null; + } await _operation?.cancel(); _operation = null; _backendController.cancelInteractive(); @@ -524,9 +529,6 @@ class _LaunchButtonState extends State { host = host ?? widget.host; final instance = host ? _hostingController.instance.value : _gameController.instance.value; - if(instance == null) { - return; - } if(host){ _hostingController.instance.value = null; @@ -550,11 +552,11 @@ class _LaunchButtonState extends State { } if(reason == _StopReason.normal) { - instance.launched = true; + instance?.launched = true; } - instance.kill(); - final child = instance.child; + instance?.kill(); + final child = instance?.child; if(child != null) { await _onStop( reason: reason, @@ -591,7 +593,7 @@ class _LaunchButtonState extends State { ); break; case _StopReason.exitCode: - if(!instance.launched) { + if(instance != null && !instance.launched) { showRebootInfoBar( translations.corruptedVersionError, severity: InfoBarSeverity.error, @@ -601,9 +603,9 @@ class _LaunchButtonState extends State { break; case _StopReason.corruptedVersionError: showRebootInfoBar( - translations.corruptedVersionError, - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, + translations.corruptedVersionError, + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, action: Button( onPressed: () => launchUrl(launcherLogFile.uri), child: Text(translations.openLog), @@ -627,13 +629,13 @@ class _LaunchButtonState extends State { case _StopReason.tokenError: _backendController.stop(); showRebootInfoBar( - translations.tokenError(instance.injectedDlls.map((element) => element.name).join(", ")), - severity: InfoBarSeverity.error, - duration: infoBarLongDuration, - action: Button( - onPressed: () => launchUrl(launcherLogFile.uri), - child: Text(translations.openLog), - ) + translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")), + severity: InfoBarSeverity.error, + duration: infoBarLongDuration, + action: Button( + onPressed: () => launchUrl(launcherLogFile.uri), + child: Text(translations.openLog), + ) ); break; case _StopReason.crash: @@ -663,7 +665,7 @@ class _LaunchButtonState extends State { try { final gameProcess = instance.gamePid; log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess"); - final dllPath = await _getDllFileOrStop(injectable, hosting); + final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting); log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath"); if(dllPath == null) { log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist"); @@ -690,9 +692,9 @@ class _LaunchButtonState extends State { } } - Future _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { + Future _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async { log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); - final (file, customDll) = _dllController.getInjectableData(injectable); + final (file, customDll) = _dllController.getInjectableData(version, injectable); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); if(await file.exists()) { log("[${host ? 'HOST' : 'GAME'}] Path exists"); @@ -712,7 +714,7 @@ class _LaunchButtonState extends State { log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); await _dllController.downloadCriticalDllInteractive(file.path, force: true); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); - return _getDllFileOrStop(injectable, host, true); + return _getDllFileOrStop(version, injectable, host, true); } InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( @@ -723,32 +725,32 @@ class _LaunchButtonState extends State { InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) { return _gameClientInfoBar = showRebootInfoBar( - linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, - loading: true, - duration: null, - action: Obx(() { - if(_hostingController.started.value || linkedHosting) { - return const SizedBox.shrink(); - } + linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly, + loading: true, + duration: null, + action: Obx(() { + if(_hostingController.started.value || linkedHosting) { + return const SizedBox.shrink(); + } - return Padding( - padding: const EdgeInsets.only( - bottom: 2.0 - ), - child: Button( - onPressed: () async { - _backendController.joinLocalhost(); - if(!_hostingController.started.value) { - _gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true); - _gameClientInfoBar?.close(); - _showLaunchingGameClientWidget(version, hostType, true); - } - }, - child: Text(translations.startGameServer), - ), - ); - }) - ); + return Padding( + padding: const EdgeInsets.only( + bottom: 2.0 + ), + child: Button( + onPressed: () async { + _backendController.joinLocalhost(); + if(!_hostingController.started.value) { + _gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true); + _gameClientInfoBar?.close(); + _showLaunchingGameClientWidget(version, hostType, true); + } + }, + child: Text(translations.startGameServer), + ), + ); + }) + ); } } diff --git a/gui/lib/src/widget/profile_tile.dart b/gui/lib/src/widget/profile_tile.dart index 82a2bf9..6d0b881 100644 --- a/gui/lib/src/widget/profile_tile.dart +++ b/gui/lib/src/widget/profile_tile.dart @@ -2,8 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:reboot_common/common.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; +import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; +import 'package:reboot_launcher/src/page/abstract/page_type.dart'; +import 'package:reboot_launcher/src/page/pages.dart'; class ProfileWidget extends StatefulWidget { final GlobalKey overlayKey; @@ -15,6 +18,7 @@ class ProfileWidget extends StatefulWidget { class _ProfileWidgetState extends State { final GameController _gameController = Get.find(); + final HostingController _hostingController = Get.find(); @override Widget build(BuildContext context) => OverlayTarget( @@ -22,7 +26,7 @@ class _ProfileWidgetState extends State { child: HoverButton( margin: const EdgeInsets.all(8.0), onPressed: () async { - if(await showProfileForm(context)) { + if(await showProfileForm(context, _username, _password)) { setState(() {}); } }, @@ -57,7 +61,7 @@ class _ProfileWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _username, + _usernameLabel, textAlign: TextAlign.start, style: const TextStyle( fontWeight: FontWeight.w600 @@ -65,7 +69,7 @@ class _ProfileWidgetState extends State { maxLines: 1 ), Text( - _email, + _emailLabel, textAlign: TextAlign.start, style: const TextStyle( fontWeight: FontWeight.w100 @@ -81,8 +85,8 @@ class _ProfileWidgetState extends State { ), ); - String get _username { - var username = _gameController.username.text; + String get _usernameLabel { + final username = _username.text; if(username.isEmpty) { return kDefaultPlayerName; } @@ -96,8 +100,8 @@ class _ProfileWidgetState extends State { return result.substring(0, 1).toUpperCase() + result.substring(1); } - String get _email { - var username = _gameController.username.text; + String get _emailLabel { + final username = _username.text; if(username.isEmpty) { return "$kDefaultPlayerName@projectreboot.dev"; } @@ -108,4 +112,7 @@ class _ProfileWidgetState extends State { return "$username@projectreboot.dev".toLowerCase(); } + + TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username; + TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password; } diff --git a/gui/lib/src/widget/title_bar_buttons.dart b/gui/lib/src/widget/title_bar_buttons.dart index ef54333..24eae84 100644 --- a/gui/lib/src/widget/title_bar_buttons.dart +++ b/gui/lib/src/widget/title_bar_buttons.dart @@ -1,5 +1,6 @@ -import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow; import 'package:flutter/material.dart'; +import 'package:reboot_launcher/src/util/os.dart'; +import 'package:window_manager/window_manager.dart'; import 'title_bar_icons.dart'; import 'title_bar_mouse.dart'; @@ -132,7 +133,7 @@ class MinimizeWindowButton extends WindowButton { animate: animate ?? false, iconBuilder: (buttonContext) => MinimizeIcon(color: buttonContext.iconColor), - onPressed: onPressed ?? () => appWindow.minimize()); + onPressed: onPressed ?? () => windowManager.minimize()); } class MaximizeWindowButton extends WindowButton { @@ -148,7 +149,7 @@ class MaximizeWindowButton extends WindowButton { iconBuilder: (buttonContext) => MaximizeIcon(color: buttonContext.iconColor), onPressed: onPressed ?? - () => appWindow.maximizeOrRestore()); + () => windowManager.maximizeOrRestore()); } final _defaultCloseButtonColors = WindowButtonColors( @@ -169,5 +170,5 @@ class CloseWindowButton extends WindowButton { animate: animate ?? false, iconBuilder: (buttonContext) => CloseIcon(color: buttonContext.iconColor), - onPressed: onPressed ?? () => appWindow.close()); + onPressed: onPressed ?? () => windowManager.close()); } diff --git a/gui/pubspec.yaml b/gui/pubspec.yaml index 338b5d6..1356964 100644 --- a/gui/pubspec.yaml +++ b/gui/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Graphical User Interface for Project Reboot -version: "9.2.7" +version: "10.0.0" publish_to: 'none' @@ -28,7 +28,6 @@ dependencies: ref: main # Window management - bitsdojo_window: ^0.1.6 window_manager: ^0.4.2 # Extract zip archives (for example the reboot.zip) diff --git a/gui/windows/flutter/generated_plugin_registrant.cc b/gui/windows/flutter/generated_plugin_registrant.cc index 5b5a819..1498af3 100644 --- a/gui/windows/flutter/generated_plugin_registrant.cc +++ b/gui/windows/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -19,8 +18,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); - BitsdojoWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); FlutterAcrylicPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); LocalNotifierPluginRegisterWithRegistrar( diff --git a/gui/windows/flutter/generated_plugins.cmake b/gui/windows/flutter/generated_plugins.cmake index 0116766..b6a020c 100644 --- a/gui/windows/flutter/generated_plugins.cmake +++ b/gui/windows/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links - bitsdojo_window_windows flutter_acrylic local_notifier screen_retriever diff --git a/gui/windows/runner/main.cpp b/gui/windows/runner/main.cpp index b039523..8042f77 100644 --- a/gui/windows/runner/main.cpp +++ b/gui/windows/runner/main.cpp @@ -1,6 +1,3 @@ -#include -auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); - #include #include diff --git a/gui/windows/runner/win32_window.cpp b/gui/windows/runner/win32_window.cpp index 91119ff..76f3adb 100644 --- a/gui/windows/runner/win32_window.cpp +++ b/gui/windows/runner/win32_window.cpp @@ -121,7 +121,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title, HWND window = CreateWindow( window_class, title.c_str(), - WS_OVERLAPPED | WS_BORDER | WS_THICKFRAME, + WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), @@ -198,6 +198,9 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_NCCALCSIZE: + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam);