diff --git a/assets/binaries/console.dll b/assets/binaries/console.dll index ecb5dad..788eee4 100644 Binary files a/assets/binaries/console.dll and b/assets/binaries/console.dll differ diff --git a/lib/src/controller/game_controller.dart b/lib/src/controller/game_controller.dart index a4a17fe..948a37f 100644 --- a/lib/src/controller/game_controller.dart +++ b/lib/src/controller/game_controller.dart @@ -36,15 +36,16 @@ class GameController extends GetxController { _selectedVersion = Rxn(decodedSelectedVersion); host = RxBool(_storage.read("host") ?? false); + host.listen((value) { + _storage.write("host", value); + username.text = _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""; + }); username = TextEditingController(text: _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""); username.addListener(() async { await _storage.write("${host.value ? 'host' : 'game'}_username", username.text); }); - host.listen((value) => _storage.write("host", value)); - host.listen((value) => username.text = _storage.read("${host.value ? 'host' : 'game'}_username") ?? ""); - started = RxBool(false); } diff --git a/lib/src/model/fortnite_version.dart b/lib/src/model/fortnite_version.dart index 19b7f5f..d8c91e1 100644 --- a/lib/src/model/fortnite_version.dart +++ b/lib/src/model/fortnite_version.dart @@ -12,33 +12,29 @@ class FortniteVersion { FortniteVersion({required this.name, required this.location}); - static File findExecutable(Directory directory, String name) { - if(path.basename(directory.path) == "FortniteGame"){ - return File("$directory/Binaries/Win64/$name"); - } - + static File? findExecutable(Directory directory, String name) { try{ - var gameDirectory = directory.listSync(recursive: true) - .firstWhereOrNull((element) => path.basename(element.path) == "FortniteGame"); - if(gameDirectory == null){ - return File("${directory.path}/Binaries/Win64/$name"); + var result = directory.listSync(recursive: true) + .firstWhereOrNull((element) => path.basename(element.path) == name); + if(result == null){ + return null; } - return File("${gameDirectory.path}/Binaries/Win64/$name"); + return File(result.path); }catch(_){ - return File("${directory.path}/Binaries/Win64/$name"); + return null; } } - File get executable { + File? get executable { return findExecutable(location, "FortniteClient-Win64-Shipping.exe"); } - File get launcher { + File? get launcher { return findExecutable(location, "FortniteLauncher.exe"); } - File get eacExecutable { + File? get eacExecutable { return findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe"); } diff --git a/lib/src/page/info_page.dart b/lib/src/page/info_page.dart index eab5eb4..1d79417 100644 --- a/lib/src/page/info_page.dart +++ b/lib/src/page/info_page.dart @@ -31,7 +31,7 @@ class InfoPage extends StatelessWidget { ), const Expanded( child: Align( - alignment: Alignment.bottomLeft, child: Text("Version 3.6${kDebugMode ? '-DEBUG' : ''}"))) + alignment: Alignment.bottomLeft, child: Text("Version 3.7${kDebugMode ? '-DEBUG' : ''}"))) ], ); } diff --git a/lib/src/util/injector.dart b/lib/src/util/injector.dart index d0b1676..5b24863 100644 --- a/lib/src/util/injector.dart +++ b/lib/src/util/injector.dart @@ -12,7 +12,7 @@ Future injectDll(int pid, String dll) async { var process = await shell.run("./injector.exe -p $pid --inject \"$dll\""); var success = process.outText.contains("Successfully injected module"); if (!success) { - injectLogFile.writeAsString(process.outText, mode: FileMode.append); + injectLogFile.writeAsString(process.outText); } return success; diff --git a/lib/src/util/patcher.dart b/lib/src/util/patcher.dart new file mode 100644 index 0000000..812c916 --- /dev/null +++ b/lib/src/util/patcher.dart @@ -0,0 +1,40 @@ +import 'dart:io'; +import 'dart:typed_data'; + +final Uint8List _original = Uint8List.fromList([ + 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0 +]); + +final Uint8List _patched = Uint8List.fromList([ + 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 +]); + +Future patchExe(File file) async { + if(_original.length != _patched.length){ + throw Exception("Cannot mutate length of binary file"); + } + + var read = await file.readAsBytes(); + var length = await file.length(); + var offset = 0; + var counter = 0; + while(offset < length){ + if(read[offset] == _original[counter]){ + counter++; + }else { + counter = 0; + } + + offset++; + if(counter == _original.length){ + for(var index = 0; index < _patched.length; index++){ + read[offset - counter + index] = _patched[index]; + } + + await file.writeAsBytes(read, mode: FileMode.write); + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/lib/src/widget/add_local_version.dart b/lib/src/widget/add_local_version.dart index a34bea6..701c50d 100644 --- a/lib/src/widget/add_local_version.dart +++ b/lib/src/widget/add_local_version.dart @@ -98,7 +98,7 @@ class AddLocalVersion extends StatelessWidget { return "Directory doesn't exist"; } - if (!FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe").existsSync()) { + if (FortniteVersion.findExecutable(directory, "FortniteClient-Win64-Shipping.exe") == null) { return "Invalid game path"; } diff --git a/lib/src/widget/launch_button.dart b/lib/src/widget/launch_button.dart index a30b7ec..246e3e4 100644 --- a/lib/src/widget/launch_button.dart +++ b/lib/src/widget/launch_button.dart @@ -8,6 +8,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/server_controller.dart'; import 'package:reboot_launcher/src/util/injector.dart'; import 'package:reboot_launcher/src/util/binary.dart'; +import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:win32_suspend_process/win32_suspend_process.dart'; @@ -35,7 +36,7 @@ class _LaunchButtonState extends State { child: Obx(() => Tooltip( message: _gameController.started.value ? "Close the running Fortnite instance" : "Launch a new Fortnite instance", child: Button( - onPressed: () => _onPressed(context), + onPressed: _onPressed, child: Text(_gameController.started.value ? "Close" : "Launch") ), )), @@ -43,7 +44,7 @@ class _LaunchButtonState extends State { ); } - void _onPressed(BuildContext context) async { + void _onPressed() async { if (_gameController.username.text.isEmpty) { showSnackbar( context, const Snackbar(content: Text("Please type a username"))); @@ -84,38 +85,103 @@ class _LaunchButtonState extends State { try { _updateServerState(true); var version = _gameController.selectedVersionObs.value!; - if (await version.launcher.exists()) { - _gameController.launcherProcess = await Process.start(version.launcher.path, []); + var hosting = _gameController.host.value; + if (version.launcher != null) { + _gameController.launcherProcess = await Process.start(version.launcher!.path, []); Win32Process(_gameController.launcherProcess!.pid).suspend(); } - if (await version.eacExecutable.exists()) { - _gameController.eacProcess = await Process.start(version.eacExecutable.path, []); + if (version.eacExecutable != null) { + _gameController.eacProcess = await Process.start(version.eacExecutable!.path, []); Win32Process(_gameController.eacProcess!.pid).suspend(); } - _gameController.gameProcess = await Process.start(version.executable.path, _createProcessArguments()) - ..exitCode.then((_) => _onStop()) + if(hosting){ + await patchExe(version.executable!); + } + + _gameController.gameProcess = await Process.start(version.executable!.path, _createProcessArguments()) + ..exitCode.then((_) => _onEnd()) ..outLines.forEach(_onGameOutput); - _injectOrShowError("cranium.dll"); + await _injectOrShowError("cranium.dll"); + + if(hosting){ + _showServerLaunchingWarning(); + } } catch (exception) { - _updateServerState(false); + _closeDialogIfOpen(); _onError(exception); } } - void _onGameOutput(line) { + void _onEnd() { + _closeDialogIfOpen(); + _onStop(); + } + + void _closeDialogIfOpen() { + if(!mounted){ + return; + } + + var route = ModalRoute.of(context); + if(route != null && !route.isCurrent){ + Navigator.of(context).pop(false); + } + } + + void _showServerLaunchingWarning() async { + var result = await showDialog( + context: context, + builder: (context) => ContentDialog( + content: const InfoLabel( + label: "Launching reboot server...", + child: SizedBox( + width: double.infinity, + child: ProgressBar() + ) + ), + actions: [ + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () { + Navigator.of(context).pop(false); + _onStop(); + }, + style: ButtonStyle( + backgroundColor: ButtonState.all(Colors.red)), + child: const Text('Cancel'), + ) + ) + ], + ) + ); + + if(result != null && result){ + return; + } + + _onStop(); + } + + void _onGameOutput(String line) { if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) { _onStop(); return; } - if (line.contains("[UFortUIManagerWidget_NUI::SetUIState]") && line.contains("FrontEnd")) { - _injectOrShowError(_gameController.host.value ? "reboot.dll" : "console.dll"); + if (line.contains("Game Engine Initialized") && !_gameController.host.value) { + _injectOrShowError("console.dll"); + } + + if(line.contains("added to UI Party led ") && _gameController.host.value){ + _injectOrShowError("reboot.dll") + .then((value) => Navigator.of(context).pop(true)); } } - Future _onError(exception) { + Future _onError(Object exception) { return showDialog( context: context, builder: (context) => ContentDialog( @@ -127,7 +193,7 @@ class _LaunchButtonState extends State { SizedBox( width: double.infinity, child: FilledButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.of(context).pop(true), style: ButtonStyle( backgroundColor: ButtonState.all(Colors.red)), child: const Text('Close'), @@ -141,7 +207,7 @@ class _LaunchButtonState extends State { _gameController.kill(); } - void _injectOrShowError(String binary) async { + Future _injectOrShowError(String binary) async { var gameProcess = _gameController.gameProcess; if (gameProcess == null) { return; @@ -166,7 +232,7 @@ class _LaunchButtonState extends State { } List _createProcessArguments() { - return [ + var args = [ "-epicapp=Fortnite", "-epicenv=Prod", "-epiclocale=en-us", @@ -179,5 +245,11 @@ class _LaunchButtonState extends State { "-AUTH_PASSWORD=Rebooted", "-AUTH_TYPE=epic" ]; + + if(_gameController.host.value){ + args.addAll(["-log", "-nullrhi", "-nosplash", "-nosound", "-unattended"]); + } + + return args; } } diff --git a/lib/src/widget/smart_switch.dart b/lib/src/widget/smart_switch.dart index d1581da..4dde298 100644 --- a/lib/src/widget/smart_switch.dart +++ b/lib/src/widget/smart_switch.dart @@ -50,11 +50,11 @@ class _SmartSwitchState extends State { double get _uncheckedOpacity => widget.enabled ? 0.8 : 0.5; - void _onChanged(checked) { + void _onChanged(bool checked) { if (!widget.enabled) { return; } - setState(() => widget.value(checked)); + setState(() => widget.value.value = checked); } } diff --git a/lib/src/widget/version_selector.dart b/lib/src/widget/version_selector.dart index 06146fd..cf19472 100644 --- a/lib/src/widget/version_selector.dart +++ b/lib/src/widget/version_selector.dart @@ -15,6 +15,7 @@ import 'package:reboot_launcher/src/model/fortnite_version.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/widget/scan_local_version.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../controller/build_controller.dart'; @@ -142,7 +143,7 @@ class VersionSelector extends StatelessWidget { switch (result) { case 0: Navigator.of(context).pop(); - Process.run("explorer.exe", [version.location.path]); + launchUrl(version.location.uri); break; case 1: diff --git a/lib/test.dart b/lib/test.dart new file mode 100644 index 0000000..18b8cab --- /dev/null +++ b/lib/test.dart @@ -0,0 +1,73 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:hex/hex.dart'; + +const String _original = "2d0069006e007600690074006500730065007300730069006f006e0020002d0069006e007600690074006500660072006f006d0020002d00700061007200740079005f006a006f0069006e0069006e0066006f005f0074006f006b0065006e0020002d007200650070006c0061007900"; +const String _patched = "2d006c006f00670020002d006e006f00730070006c0061007300680020002d006e006f0073006f0075006e00640020002d006e0075006c006c0072006800690020002d007500730065006f006c0064006900740065006d00630061007200640073002000200020002000200020002000"; +final Uint8List _originalBinary = Uint8List.fromList([ + 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0 +]); + +final Uint8List _patchedBinary = Uint8List.fromList([ + 45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 +]); + +Future patchExeHex(File file) async { + Future replaceBinary(File file, String original, String replacement) async { + var read = await file.readAsBytes(); + var hex = HEX.encode(read); + var fixed = hex.replaceAll(original, replacement); + return fixed; + } + + return await replaceBinary(file, _original, _patched); +} + +Future patchExeBinary(File file) async { + Future replaceBinary(File file, Uint8List original, Uint8List replacement) async { + if(original.length != replacement.length){ + throw Exception("Cannot mutate length of binary file"); + } + + var read = await file.readAsBytes(); + var length = await file.length(); + var offset = 0; + var counter = 0; + while(offset < length){ + if(read[offset] == original[counter]){ + counter++; + }else { + counter = 0; + } + + offset++; + if(counter == original.length){ + for(var index = 0; index < replacement.length; index++){ + read[offset - counter + index] = replacement[index]; + } + + return HEX.encode(read); + } + } + + throw Exception("No match"); + } + + return await replaceBinary(file, _originalBinary, _patchedBinary); +} + +void main() async { + var file = File("D:\\Fortnite73\\FortniteGame\\Binaries\\Win64\\FortniteClient-Win64-Shipping.exe"); + var hexed = await patchExeHex(file); + var binary = await patchExeBinary(file); + var offset = 0; + while(offset < hexed.length){ + if(hexed[offset] != binary[offset]){ + print("Difference ${hexed[offset]} != ${binary[offset]} at $offset"); + } + + offset++; + } + print(hexed == binary); +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index f2b31e8..7314386 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: reboot_launcher description: Launcher for project reboot -version: "3.6.0" +version: "3.7.0" publish_to: 'none' @@ -29,6 +29,7 @@ dependencies: get: ^4.6.5 get_storage: ^2.0.3 window_manager: ^0.2.7 + hex: ^0.2.0 dev_dependencies: flutter_test: @@ -48,7 +49,7 @@ msix_config: display_name: Reboot Launcher publisher_display_name: Auties00 identity_name: 31868Auties00.RebootLauncher - msix_version: 3.6.0.0 + msix_version: 3.7.0.0 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 logo_path: ./assets/icons/reboot.ico architecture: x64