diff --git a/assets/binaries/port.bat b/assets/binaries/port.bat deleted file mode 100644 index c77488b..0000000 --- a/assets/binaries/port.bat +++ /dev/null @@ -1 +0,0 @@ -netstat -ano|find ":3551" \ No newline at end of file diff --git a/lib/cli.dart b/lib/cli.dart new file mode 100644 index 0000000..0ef1a6c --- /dev/null +++ b/lib/cli.dart @@ -0,0 +1,382 @@ +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/model/fortnite_version.dart'; +import 'package:reboot_launcher/src/model/game_type.dart'; +import 'package:reboot_launcher/src/util/binary.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_standalone.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; + +const String _craniumDownload = "https://cdn.discordapp.com/attachments/1001161930599317524/1027684488718860309/cranium.dll"; +const String _consoleDownload = "https://cdn.discordapp.com/attachments/1001161930599317524/1027684489184432188/console.dll"; +const String _injectorDownload = "https://cdn.discordapp.com/attachments/1001161930599317524/1027686593697435799/injector.exe"; + +Process? _gameProcess; + +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 CLI Tool"); + stdout.writeln("Wrote by Auties00"); + stdout.writeln("Version 3.10"); + + var gameJson = await _getControllerJson("game1"); + var serverJson = await _getControllerJson("server1"); + var versions = _getVersions(gameJson); + var parser = ArgParser() + ..addCommand("list")..addCommand("launch") + ..addOption("version", defaultsTo: gameJson["version"]) + ..addOption("username") + ..addOption("server-type", allowed: ["embedded", "remote"], defaultsTo: serverJson["embedded"] ?? true ? "embedded" : "remote") + ..addOption("server-host", defaultsTo: serverJson["host"]) + ..addOption("server-port", defaultsTo: serverJson["port"]) + ..addOption("type", allowed: ["client", "server", "headless_server"], defaultsTo: _getDefaultType(gameJson)) + ..addFlag("update", defaultsTo: true, negatable: true) + ..addFlag("log", defaultsTo: false); + var result = parser.parse(args); + if (result.command?.name == "list") { + stdout.writeln("Versions list: "); + versions.map((entry) => "${entry.location.path}(${entry.name})") + .forEach((element) => stdout.writeln(element)); + return; + } + + var type = _getType(result); + var username = result["username"]; + username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"]; + + var dummyVersion = _createVersion(gameJson["version"], result["version"], versions); + await _updateDLLs(); + if(result["update"]) { + stdout.writeln("Updating DLL..."); + await downloadRebootDll(0); + } + + stdout.writeln("Launching game(type: ${type.name})..."); + await _startLauncherProcess(dummyVersion); + await _startEacProcess(dummyVersion); + if (result["type"] == "headless_server") { + await patchExe(dummyVersion.executable!); + } + + var started = await _startServerIfNeeded(result); + if(!started){ + stderr.writeln("Cannot start server!"); + return; + } + + await _startGameProcess(dummyVersion, type != GameType.client, result); + await _injectOrShowError("cranium.dll"); +} + +GameType _getType(ArgResults result) { + var type = result["type"]; + switch(type){ + case "client": + return GameType.client; + + case "server": + return GameType.server; + + case "headless_server": + return GameType.headlessServer; + + default: + throw Exception("Unknown game type: $result. Use --type only with client, server or headless_server"); + } +} + +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); + } + + var injectorExe = await loadBinary("injector.exe", true); + if(!injectorExe.existsSync()){ + var response = await http.get(Uri.parse(_injectorDownload)); + if(response.statusCode != 200){ + throw Exception("Cannot download injector"); + } + + await injectorExe.writeAsBytes(response.bodyBytes); + } +} + +List _getVersions(Map gameJson) { + Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]"); + return iterable.map((entry) => FortniteVersion.fromJson(entry)) + .toList(); +} + +Future _startGameProcess(FortniteVersion dummyVersion, bool host, ArgResults result) async { + var gamePath = dummyVersion.executable?.path; + if (gamePath == null) { + throw Exception("${dummyVersion.location + .path} no longer contains a Fortnite executable. Did you delete it?"); + } + + var username = result["username"]; + if (username == null) { + username = "Reboot${host ? 'Host' : 'Player'}"; + stdout.writeln("No username was specified, using $username by default. Use --username to specify one"); + } + + var verbose = result["log"]; + _gameProcess = await Process.start(gamePath, createRebootArgs(username, result["type"] == "headless_server")) + ..exitCode.then((_) => _onClose()) + ..outLines.forEach((line) => _onGameOutput(line, host, verbose)); +} + +void _onClose() { + stdout.writeln("The game was closed"); + exit(0); +} + +Future _startEacProcess(FortniteVersion dummyVersion) async { + if (dummyVersion.eacExecutable == null) { + return; + } + + var process = await Process.start(dummyVersion.eacExecutable!.path, []); + Win32Process(process.pid).suspend(); +} + +Future _startLauncherProcess(FortniteVersion dummyVersion) async { + if (dummyVersion.launcher == null) { + return; + } + + var process = await Process.start(dummyVersion.launcher!.path, []); + Win32Process(process.pid).suspend(); +} + +Future _startServerIfNeeded(ArgResults result) async { + stdout.writeln("Starting lawin server..."); + if (!await isLawinPortFree()) { + stdout.writeln("A lawin server is already active"); + return true; + } + + if (result["server-type"] == "embedded") { + stdout.writeln("Starting an embedded server..."); + return await _changeEmbeddedServerState(); + } + + var host = result["server-host"]; + var port = result["server-port"]; + stdout.writeln("Starting a reverse proxy to $host:$port"); + return await _changeReverseProxyState(host, port) != null; +} + +Future _changeEmbeddedServerState() async { + var nodeProcess = await Process.run("where", ["node"]); + if(nodeProcess.exitCode != 0) { + throw Exception("Missing node, cannot start embedded server"); + } + + if(!serverLocation.existsSync()) { + await downloadServer(false); + } + + var serverRunner = File("${serverLocation.path}/start.bat"); + if (!(await serverRunner.exists())) { + return false; + } + + var nodeModules = Directory("${serverLocation.path}/node_modules"); + if (!(await nodeModules.exists())) { + await Process.run("${serverLocation.path}/install_packages.bat", [], + workingDirectory: serverLocation.path); + } + + await Process.start(serverRunner.path, [], workingDirectory: serverLocation.path); + 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) { + if(versionName != null){ + try { + return versions.firstWhere((element) => versionName == element.name); + }catch(_){ + throw Exception("Cannot find version $versionName"); + } + } + + if (versionPath == null) { + throw Exception( + "Specify a version using --version or open the launcher GUI and select it manually"); + } + + return FortniteVersion(name: "dummy", location: Directory(versionPath)); +} + +void _onGameOutput(String line, bool host, bool verbose) { + if(verbose) { + stdout.writeln(line); + } + + if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { + return; + } + + if(line.contains("port 3551 failed: Connection refused")){ + stderr.writeln("Connection refused from lawin server"); + return; + } + + if(line.contains("HTTP 400 response from ")){ + stderr.writeln("Connection refused from lawin server"); + return; + } + + if(line.contains("Network failure when attempting to check platform restrictions")){ + stderr.writeln("Expired token, please reopen the game"); + return; + } + + if (line.contains("Game Engine Initialized") && !host) { + _injectOrShowError("console.dll"); + return; + } + + if(line.contains("Region") && host){ + _injectOrShowError("reboot.dll"); + } +} + +Future _injectOrShowError(String binary) async { + if (_gameProcess == null) { + return; + } + + try { + stdout.writeln("Injecting $binary..."); + var dll = await loadBinary(binary, true); + var success = await injectDll(_gameProcess!.pid, dll.path, true); + if (success) { + return; + } + + _onInjectError(binary); + } catch (exception) { + _onInjectError(binary); + } +} + +void _onInjectError(String binary) { + stderr.writeln(injectLogFile.readAsStringSync()); + throw Exception("Cannot inject binary: $binary"); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7791b7c..468ff74 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart' import 'package:fluent_ui/fluent_ui.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:reboot_launcher/cli.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; @@ -14,9 +15,14 @@ import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/os.dart'; import 'package:system_theme/system_theme.dart'; -void main() async { +void main(List args) async { await Directory(safeBinariesDirectory) .create(recursive: true); + if(args.isNotEmpty){ + handleCLI(args); + return; + } + WidgetsFlutterBinding.ensureInitialized(); await SystemTheme.accentColor.load(); await GetStorage.init("game"); @@ -34,6 +40,7 @@ void main() async { appWindow.title = "Reboot Launcher"; appWindow.show(); }); + runApp(const RebootApplication()); } diff --git a/lib/src/controller/server_controller.dart b/lib/src/controller/server_controller.dart index ab6d550..954835c 100644 --- a/lib/src/controller/server_controller.dart +++ b/lib/src/controller/server_controller.dart @@ -6,6 +6,8 @@ import 'package:get_storage/get_storage.dart'; import 'package:reboot_launcher/src/util/binary.dart'; import 'package:reboot_launcher/src/util/server.dart'; +import '../util/server_standalone.dart'; + class ServerController extends GetxController { late final TextEditingController host; late final TextEditingController port; diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 86f1042..e120146 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:get/get.dart'; + import 'package:path/path.dart' as path; class FortniteVersion { @@ -16,11 +16,7 @@ class FortniteVersion { static File? findExecutable(Directory directory, String name) { try{ var result = directory.listSync(recursive: true) - .firstWhereOrNull((element) => path.basename(element.path) == name); - if(result == null){ - return null; - } - + .firstWhere((element) => path.basename(element.path) == name); return File(result.path); }catch(_){ return null; diff --git a/lib/src/util/injector.dart b/lib/src/util/injector.dart index 5b24863..1a67440 100644 --- a/lib/src/util/injector.dart +++ b/lib/src/util/injector.dart @@ -7,8 +7,12 @@ File injectLogFile = File("${Platform.environment["Temp"]}/server.txt"); // This can be done easily with win32 apis but for some reason it doesn't work on all machines // Update: it was a missing permission error, it could be refactored now -Future injectDll(int pid, String dll) async { - var shell = Shell(workingDirectory: internalBinariesDirectory); +Future injectDll(int pid, String dll, [bool useSafeBinariesHome = false]) async { + var shell = Shell( + commandVerbose: false, + commentVerbose: false, + workingDirectory: useSafeBinariesHome ? safeBinariesDirectory : internalBinariesDirectory + ); var process = await shell.run("./injector.exe -p $pid --inject \"$dll\""); var success = process.outText.contains("Successfully injected module"); if (!success) { diff --git a/lib/src/util/reboot.dart b/lib/src/util/reboot.dart index d0f62b2..8d3865f 100644 --- a/lib/src/util/reboot.dart +++ b/lib/src/util/reboot.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; import 'package:crypto/crypto.dart'; -import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; import 'package:reboot_launcher/src/util/binary.dart'; @@ -31,17 +30,13 @@ Future downloadRebootDll(int? lastUpdateMs) async { await extractFileToDisk(tempZip.path, outputDir.path); var rebootDll = outputDir.listSync() - .firstWhereOrNull((element) => path.extension(element.path) == ".dll"); - if(rebootDll == null){ - throw Exception("Missing reboot dll"); - } - + .firstWhere((element) => path.extension(element.path) == ".dll"); if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) { - outputDir.delete(); + outputDir.delete(recursive: true); return lastUpdateMs ?? now.millisecondsSinceEpoch; } await rebootDll.rename(oldRebootDll.path); - outputDir.delete(); + outputDir.delete(recursive: true); return now.millisecondsSinceEpoch; -} +} \ No newline at end of file diff --git a/lib/src/util/server.dart b/lib/src/util/server.dart index 435a86d..7375382 100644 --- a/lib/src/util/server.dart +++ b/lib/src/util/server.dart @@ -1,52 +1,13 @@ import 'dart:io'; +import 'dart:math'; -import 'package:archive/archive_io.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:process_run/shell.dart'; import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/server_standalone.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; import 'package:shelf_proxy/shelf_proxy.dart'; -final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin"); -const String _serverUrl = - "https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip"; -const String _portableServerUrl = - "https://cdn.discordapp.com/attachments/998020695223193673/1019999251994005504/LawinServer.exe"; - -Future downloadServer(bool portable) async { - if(!portable){ - var response = await http.get(Uri.parse(_serverUrl)); - var tempZip = File("${Platform.environment["Temp"]}/lawin.zip"); - await tempZip.writeAsBytes(response.bodyBytes); - await extractFileToDisk(tempZip.path, serverLocation.parent.path); - var result = Directory("${serverLocation.parent.path}/LawinServer-main"); - await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}"); - await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path); - await updateEngineConfig(); - return true; - } - - var response = await http.get(Uri.parse(_portableServerUrl)); - var server = await loadBinary("LawinServer.exe", true); - await server.writeAsBytes(response.bodyBytes); - return true; -} - -Future updateEngineConfig() async { - var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini"); - var patchedEngine = await loadBinary("DefaultEngine.ini", true); - await engine.writeAsString(await patchedEngine.readAsString()); -} - -Future isLawinPortFree() async { - var portBat = await loadBinary("port.bat", false); - var process = await Process.run(portBat.path, []); - return !process.outText.contains(" LISTENING "); // Goofy way, best we got -} - Future changeReverseProxyState(BuildContext context, String host, String port, HttpServer? server) async { if(server != null){ try{ @@ -92,7 +53,7 @@ Future changeReverseProxyState(BuildContext context, String host, S } Future _showReverseProxyCheck(BuildContext context, String host, String port) async { - var future = _pingServer(host, port); + var future = ping(host, port); return await showDialog( context: context, builder: (context) => ContentDialog( @@ -138,30 +99,6 @@ Future _showReverseProxyCheck(BuildContext context, String host, String po ); } -Future _pingServer(String host, String port, [bool https=false]) async { - var hostName = _getHostName(host); - var declaredScheme = _getScheme(host); - try{ - var uri = Uri( - scheme: declaredScheme ?? (https ? "https" : "http"), - host: hostName, - port: int.parse(port) - ); - var client = HttpClient() - ..connectionTimeout = const Duration(seconds: 5); - var request = await client.getUrl(uri); - var response = await request.close(); - return response.statusCode == 200 ? uri : null; - }catch(_){ - return https || declaredScheme != null ? null : await _pingServer(host, port, true); - } -} - -String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", ""); - -String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; - - void _showStartProxyError(BuildContext context, Object error) { showDialog( context: context, diff --git a/lib/src/util/server_standalone.dart b/lib/src/util/server_standalone.dart new file mode 100644 index 0000000..d431d18 --- /dev/null +++ b/lib/src/util/server_standalone.dart @@ -0,0 +1,90 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +import 'package:process_run/shell.dart'; +import 'package:reboot_launcher/src/util/binary.dart'; + +final serverLocation = Directory("${Platform.environment["UserProfile"]}/.reboot_launcher/lawin"); +const String _serverUrl = + "https://github.com/Lawin0129/LawinServer/archive/refs/heads/main.zip"; +const String _portableServerUrl = + "https://cdn.discordapp.com/attachments/998020695223193673/1019999251994005504/LawinServer.exe"; + +Future downloadServer(bool portable) async { + if(!portable){ + var response = await http.get(Uri.parse(_serverUrl)); + var tempZip = File("${Platform.environment["Temp"]}/lawin.zip"); + await tempZip.writeAsBytes(response.bodyBytes); + await extractFileToDisk(tempZip.path, serverLocation.parent.path); + var result = Directory("${serverLocation.parent.path}/LawinServer-main"); + await result.rename("${serverLocation.parent.path}/${path.basename(serverLocation.path)}"); + await Process.run("${serverLocation.path}/install_packages.bat", [], workingDirectory: serverLocation.path); + await updateEngineConfig(); + return true; + } + + var response = await http.get(Uri.parse(_portableServerUrl)); + var server = await loadBinary("LawinServer.exe", true); + await server.writeAsBytes(response.bodyBytes); + return true; +} + +Future updateEngineConfig() async { + var engine = File("${serverLocation.path}/CloudStorage/DefaultEngine.ini"); + var patchedEngine = await loadBinary("DefaultEngine.ini", true); + await engine.writeAsString(await patchedEngine.readAsString()); +} + +Future isLawinPortFree() async { + return ServerSocket.bind("localhost", 3551) + .then((socket) => socket.close()) + .then((_) => true) + .onError((error, _) => false); +} + +List createRebootArgs(String username, bool headless) { + var args = [ + "-epicapp=Fortnite", + "-epicenv=Prod", + "-epiclocale=en-us", + "-epicportal", + "-skippatchcheck", + "-fromfl=eac", + "-fltoken=3db3ba5dcbd2e16703f3978d", + "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", + "-AUTH_LOGIN=$username@projectreboot.dev", + "-AUTH_PASSWORD=Rebooted", + "-AUTH_TYPE=epic" + ]; + + if(headless){ + args.addAll(["-nullrhi", "-nosplash", "-nosound"]); + } + + return args; +} + +Future ping(String host, String port, [bool https=false]) async { + var hostName = _getHostName(host); + var declaredScheme = _getScheme(host); + try{ + var uri = Uri( + scheme: declaredScheme ?? (https ? "https" : "http"), + host: hostName, + port: int.parse(port) + ); + var client = HttpClient() + ..connectionTimeout = const Duration(seconds: 5); + var request = await client.getUrl(uri); + var response = await request.close(); + return response.statusCode == 200 ? uri : null; + }catch(_){ + return https || declaredScheme != null ? null : await ping(host, port, true); + } +} + +String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", ""); + +String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; \ No newline at end of file diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index 90080f1..7cc776a 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -15,7 +15,7 @@ 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 '../util/os.dart'; +import '../util/server_standalone.dart'; class LaunchButton extends StatefulWidget { const LaunchButton( @@ -111,7 +111,7 @@ class _LaunchButtonState extends State { return; } - _gameController.gameProcess = await Process.start(gamePath, _createProcessArguments()) + _gameController.gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, hosting)) ..exitCode.then((_) => _onEnd()) ..outLines.forEach(_onGameOutput); await _injectOrShowError("cranium.dll"); @@ -210,6 +210,31 @@ class _LaunchButtonState extends State { ); } + Future _showTokenError() async { + if(!mounted){ + return; + } + + showDialog( + context: context, + builder: (context) => ContentDialog( + content: const SizedBox( + width: double.infinity, + child: Text("A token error occurred, restart the game and try again", textAlign: TextAlign.center) + ), + actions: [ + SizedBox( + width: double.infinity, + child: Button( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ) + ) + ], + ) + ); + } + Future _showUnsupportedHeadless() async { if(!mounted){ return; @@ -300,6 +325,13 @@ class _LaunchButtonState extends State { return; } + if(line.contains("Network failure when attempting to check platform restrictions")){ + _fail = true; + _closeDialogIfOpen(false); + _showTokenError(); + return; + } + if (line.contains("Game Engine Initialized") && _gameController.type.value == GameType.client) { _injectOrShowError("console.dll"); return; @@ -365,26 +397,4 @@ class _LaunchButtonState extends State { showSnackbar(context, Snackbar(content: Text("Cannot inject $binary"))); launchUrl(injectLogFile.uri); } - - List _createProcessArguments() { - var args = [ - "-epicapp=Fortnite", - "-epicenv=Prod", - "-epiclocale=en-us", - "-epicportal", - "-skippatchcheck", - "-fromfl=eac", - "-fltoken=3db3ba5dcbd2e16703f3978d", - "-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", - "-AUTH_LOGIN=${_gameController.username.text}@projectreboot.dev", - "-AUTH_PASSWORD=Rebooted", - "-AUTH_TYPE=epic" - ]; - - if(_gameController.type.value == GameType.headlessServer){ - args.addAll(["-nullrhi", "-nosplash", "-nosound"]); - } - - return args; - } } diff --git a/pubspec.yaml b/pubspec.yaml index 58b0ecd..22ba70c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,8 @@ dependencies: get_storage: ^2.0.3 window_manager: ^0.2.7 shelf_proxy: ^1.0.2 + args: ^2.3.1 + github: ^9.4.0 dependency_overrides: win32: ^3.0.0 diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 6b7deb6..85c23db 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -7,24 +7,25 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); #include "flutter_window.h" #include "utils.h" +#include +#include +#include +#include +#include int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + std::vector command_line_arguments = GetCommandLineArguments(); + if (!command_line_arguments.empty() || (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent())) { CreateAndAttachConsole(); } + // Initialize COM, so that it is available for use in the library and/or // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); FlutterWindow window(project); @@ -33,6 +34,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, if (!window.CreateAndShow(L"reboot_launcher", origin, size)) { return EXIT_FAILURE; } + window.SetQuitOnClose(true); ::MSG msg; @@ -42,5 +44,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } ::CoUninitialize(); + std::cout << "Done" << std::endl; return EXIT_SUCCESS; }