checkpoint

This commit is contained in:
Alessandro Autiero
2023-02-24 15:24:24 +01:00
parent 013d15d7ff
commit 63c7cc5c5b
69 changed files with 1148 additions and 644 deletions

View File

@@ -0,0 +1 @@
powershell -inputformat none -outputformat none -NonInteractive -Command Add-MpPreference -ExclusionPath "%UserProfile%/.reboot_launcher"

View File

@@ -0,0 +1 @@
for /f "tokens=5" %%a in ('netstat -aon ^| find ":3551" ^| find "LISTENING"') do taskkill /f /pid %%a

View File

@@ -0,0 +1 @@
for /f "tokens=5" %%a in ('netstat -aon ^| find ":8080" ^| find "LISTENING"') do taskkill /f /pid %%a

View File

@@ -1 +0,0 @@
netstat -ano|find ":3551"

View File

@@ -1,2 +0,0 @@
for /f "tokens=5" %%a in ('netstat -aon ^| find ":3551" ^| find "LISTENING"') do taskkill /f /pid %%a
for /f "tokens=5" %%a in ('netstat -aon ^| find ":8080" ^| find "LISTENING"') do taskkill /f /pid %%a

View File

@@ -1 +0,0 @@
sc query "MongoDB" | findstr /i "STATE"

View File

@@ -0,0 +1,2 @@
net stop winnat
net start winnat

View File

@@ -10,8 +10,5 @@ bCheckOSSForUpdate=false
[XMPP] [XMPP]
bEnableWebsockets=false bEnableWebsockets=false
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp [/Script/Engine.InputSettings]
[OnlineSubsystemMcp.Xmpp Prod] ConsoleKey=F8
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80

View File

@@ -9,12 +9,12 @@ bUploadAthenaStats=false
bUploadAthenaStatsV2=false bUploadAthenaStatsV2=false
[/Script/FortniteGame.FortMatchmakingV2] [/Script/FortniteGame.FortMatchmakingV2]
bCustomKeyEnabled=false bCustomKeyEnabled=true
[/Script/FortniteGame.FortChatManager] [/Script/FortniteGame.FortChatManager]
bShouldRequestGeneralChatRooms=false bShouldRequestGeneralChatRooms=false
bShouldJoinGlobalChat=false bShouldJoinGlobalChat=false
bShouldJoinFounderChat=false bShouldJoinFoaunderChat=false
bIsAthenaGlobalChatEnabled=false bIsAthenaGlobalChatEnabled=false
[/Script/FortniteGame.FortGameInstance] [/Script/FortniteGame.FortGameInstance]
@@ -26,3 +26,6 @@ bBattleRoyaleMatchmakingEnabled=true
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=6)) +FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=6))
+FrontEndPlaylistData=(PlaylistName=Playlist_PlaygroundV2, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=false, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=2, bDisplayAsLimitedTime=false, DisplayPriority=16)) +FrontEndPlaylistData=(PlaylistName=Playlist_PlaygroundV2, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=false, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=2, bDisplayAsLimitedTime=false, DisplayPriority=16))
+FrontEndPlaylistData=(PlaylistName=Playlist_Campaign, PlaylistAccess=(bEnabled=true, bInvisibleWhenEnabled=true)) +FrontEndPlaylistData=(PlaylistName=Playlist_Campaign, PlaylistAccess=(bEnabled=true, bInvisibleWhenEnabled=true))
[/Script/Engine.InputSettings]
ConsoleKey=F8

View File

@@ -15,4 +15,7 @@ bIsOutOfSeasonMode=true
+DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden) +DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden)
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden) +DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden)
+DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden) +DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden)
+DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden) +DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden)
[/Script/Engine.InputSettings]
ConsoleKey=F8

BIN
assets/images/auties.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -62,7 +62,7 @@ void main(List<String> args) async {
if(result["update"]) { if(result["update"]) {
stdout.writeln("Updating reboot dll..."); stdout.writeln("Updating reboot dll...");
try { try {
await downloadRebootDll(0); await downloadRebootDll(rebootDownloadUrl, 0);
}catch(error){ }catch(error){
stderr.writeln("Cannot update reboot dll: $error"); stderr.writeln("Cannot update reboot dll: $error");
} }

View File

@@ -21,8 +21,7 @@ import 'package:window_manager/window_manager.dart';
final GlobalKey appKey = GlobalKey(); final GlobalKey appKey = GlobalKey();
void main() async { void main() async {
await Directory(safeBinariesDirectory) await safeBinariesDirectory.create(recursive: true);
.create(recursive: true);
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load(); await SystemTheme.accentColor.load();
await GetStorage.init("game"); await GetStorage.init("game");

View File

@@ -13,7 +13,7 @@ Future<Map<String, dynamic>> getControllerJson(String name) async {
throw Exception("Missing documents folder"); throw Exception("Missing documents folder");
} }
var file = File("$folder/$name.gs"); var file = File("$folder\\$name.gs");
if(!file.existsSync()){ if(!file.existsSync()){
return HashMap(); return HashMap();
} }

View File

@@ -2,12 +2,12 @@ import 'dart:io';
import 'package:process_run/shell.dart'; import 'package:process_run/shell.dart';
import 'package:reboot_launcher/cli.dart'; import 'package:reboot_launcher/cli.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart';
import '../model/fortnite_version.dart'; import '../model/fortnite_version.dart';
import '../model/game_type.dart'; import '../model/game_type.dart';
import '../util/injector.dart'; import '../util/injector.dart';
import '../util/os.dart'; import '../util/os.dart';
import '../util/process.dart';
import '../util/server.dart'; import '../util/server.dart';
final List<String> _errorStrings = [ final List<String> _errorStrings = [
@@ -41,7 +41,6 @@ Future<void> startGame() async {
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type)) _gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
..exitCode.then((_) => _onClose()) ..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose)); ..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
_injectOrShowError("craniumv2.dll");
} }
@@ -51,7 +50,7 @@ Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
} }
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []); _launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
Win32Process(_launcherProcess!.pid).suspend(); suspend(_launcherProcess!.pid);
} }
Future<void> _startEacProcess(FortniteVersion dummyVersion) async { Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
@@ -60,7 +59,7 @@ Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
} }
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []); _eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
Win32Process(_eacProcess!.pid).suspend(); suspend(_eacProcess!.pid);
} }
void _onGameOutput(String line, String dll, bool hosting, bool verbose) { void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
@@ -85,18 +84,14 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
} }
if(line.contains("Region ")){ if(line.contains("Region ")){
_injectRequiredDLLs(hosting, dll); if(hosting) {
} _injectOrShowError(dll, false);
} }else {
_injectOrShowError("console.dll");
}
void _injectRequiredDLLs(bool host, String rebootDll) { _injectOrShowError("leakv2.dll");
if(host) {
_injectOrShowError(rebootDll, false);
}else {
_injectOrShowError("console.dll");
} }
_injectOrShowError("leakv2.dll");
} }
void _kill() { void _kill() {

View File

@@ -54,6 +54,6 @@ Future<void> downloadRequiredDLLs() async {
var tempZip = File("${tempDirectory.path}/reboot_config.zip"); var tempZip = File("${tempDirectory.path}/reboot_config.zip");
await tempZip.writeAsBytes(response.bodyBytes); await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, "$safeBinariesDirectory\\backend\\cli"); await extractFileToDisk(tempZip.path, "${safeBinariesDirectory.path}\\cli");
} }
} }

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -20,7 +21,9 @@ class GameController extends GetxController {
late final HashMap<GameType, GameInstance> gameInstancesMap; late final HashMap<GameType, GameInstance> gameInstancesMap;
late final RxBool started; late final RxBool started;
late bool updated; late bool updated;
Future? updater; late bool error;
late bool failing;
StreamController<bool>? updater;
GameController() { GameController() {
_storage = GetStorage("game"); _storage = GetStorage("game");
@@ -52,6 +55,10 @@ class GameController extends GetxController {
started = RxBool(false); started = RxBool(false);
updated = false; updated = false;
error = false;
failing = false;
} }
String _readUsername() { String _readUsername() {

View File

@@ -29,18 +29,10 @@ class ServerController extends GetxController {
host.text = _readHost(); host.text = _readHost();
port.text = _readPort(); port.text = _readPort();
_storage.write("type", value.index); _storage.write("type", value.index);
if(!started.value) { if(!started.value) {
return; return;
} }
if(value == ServerType.remote){
remoteServer?.close(force: true);
remoteServer = null;
started.value = false;
return;
}
stop(); stop();
}); });
@@ -76,6 +68,7 @@ class ServerController extends GetxController {
break; break;
case ServerType.remote: case ServerType.remote:
await remoteServer?.close(force: true); await remoteServer?.close(force: true);
remoteServer = null;
break; break;
case ServerType.local: case ServerType.local:
break; break;

View File

@@ -5,15 +5,21 @@ import 'package:reboot_launcher/src/model/tutorial_page.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'dart:ui'; import 'dart:ui';
import '../util/reboot.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {
late final GetStorage _storage; late final GetStorage _storage;
late final String originalDll; late final String originalDll;
late final TextEditingController updateUrl;
late final TextEditingController rebootDll; late final TextEditingController rebootDll;
late final TextEditingController consoleDll; late final TextEditingController consoleDll;
late final TextEditingController authDll; late final TextEditingController authDll;
late final TextEditingController matchmakingIp; late final TextEditingController matchmakingIp;
late final Rx<PaneDisplayMode> displayType; late final Rx<PaneDisplayMode> displayType;
late final RxBool automaticallyStartMatchmaker;
late final RxBool doNotAskAgain; late final RxBool doNotAskAgain;
late final RxBool advancedMode;
late final RxBool autoUpdate;
late Rx<TutorialPage> tutorialPage; late Rx<TutorialPage> tutorialPage;
late double width; late double width;
late double height; late double height;
@@ -24,15 +30,24 @@ class SettingsController extends GetxController {
SettingsController() { SettingsController() {
_storage = GetStorage("settings"); _storage = GetStorage("settings");
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
rebootDll = _createController("reboot", "reboot.dll"); rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.dll"); consoleDll = _createController("console", "console.dll");
authDll = _createController("cranium2", "craniumv2.dll"); authDll = _createController("cranium2", "craniumv2.dll");
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1"); matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
matchmakingIp.addListener(() async { matchmakingIp.addListener(() async {
var text = matchmakingIp.text; var text = matchmakingIp.text;
_storage.write("ip", text); _storage.write("ip", text);
}); });
automaticallyStartMatchmaker = RxBool(_storage.read("start_matchmaker_automatically") ?? false);
automaticallyStartMatchmaker.listen((value) => _storage.write("start_matchmaker_automatically", value));
doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false); doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false);
doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value)); doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value));
@@ -40,6 +55,12 @@ class SettingsController extends GetxController {
height = _storage.read("height") ?? window.physicalSize.height; height = _storage.read("height") ?? window.physicalSize.height;
offsetX = _storage.read("offset_x"); offsetX = _storage.read("offset_x");
offsetY = _storage.read("offset_y"); offsetY = _storage.read("offset_y");
advancedMode = RxBool(_storage.read("advanced") ?? false);
advancedMode.listen((value) async => _storage.write("advanced", value));
autoUpdate = RxBool(_storage.read("auto_update") ?? false);
autoUpdate.listen((value) async => _storage.write("auto_update", value));
displayType = Rx(PaneDisplayMode.top); displayType = Rx(PaneDisplayMode.top);
scrollingDistance = 0.0; scrollingDistance = 0.0;
@@ -50,7 +71,7 @@ class SettingsController extends GetxController {
TextEditingController _createController(String key, String name) { TextEditingController _createController(String key, String name) {
loadBinary(name, true); loadBinary(name, true);
var controller = TextEditingController(text: _storage.read(key) ?? "$safeBinariesDirectory\\$name"); var controller = TextEditingController(text: _storage.read(key) ?? "${safeBinariesDirectory.path}\\$name");
controller.addListener(() => _storage.write(key, controller.text)); controller.addListener(() => _storage.write(key, controller.text));
return controller; return controller;

View File

@@ -229,6 +229,16 @@ class ErrorDialog extends AbstractDialog {
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace}); const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
static DialogButton createCopyErrorButton({required Object error, required StackTrace? stackTrace, required Function() onClick, ButtonType type = ButtonType.primary}) => DialogButton(
text: "Copy error",
type: type,
onTap: () async {
FlutterClipboard.controlC("An error occurred: $error\nStacktrace:\n $stackTrace");
showMessage("Copied error to clipboard");
onClick();
},
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InfoDialog( return InfoDialog(
@@ -239,14 +249,10 @@ class ErrorDialog extends AbstractDialog {
), ),
if(stackTrace != null) if(stackTrace != null)
DialogButton( createCopyErrorButton(
text: "Copy error", error: exception,
type: ButtonType.primary, stackTrace: stackTrace,
onTap: () async { onClick: () => Navigator.pop(context)
FlutterClipboard.controlC("An error occurred: $exception\nStacktrace:\n $stackTrace.toString");
Navigator.of(context).pop();
showMessage("Copied error to clipboard");
},
) )
], ],
); );

View File

@@ -1,6 +1,19 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart'; import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import '../../main.dart';
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
"This means that you cannot currently host this version of the game. "
"For a list of supported versions, check #info in the Discord server. "
"If you are unsure which version works best, use build 7.40. "
"If you are a passionate programmer you can add support by opening a PR on Github. ";
const String _corruptedBuildError = "The build you are currently using is corrupted. "
"This means that some critical files are missing for the game to launch. "
"Download the build again from the launcher or, if it's not available there, from another source. "
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
Future<void> showBrokenError() async { Future<void> showBrokenError() async {
showDialog( showDialog(
@@ -26,8 +39,17 @@ Future<void> showTokenErrorFixable() async {
builder: (context) => const InfoDialog( builder: (context) => const InfoDialog(
text: "A token error occurred. " text: "A token error occurred. "
"The backend server has been automatically restarted to fix the issue. " "The backend server has been automatically restarted to fix the issue. "
"Relaunch your game to check if the issue has been automatically fixed. " "The game has been restarted automatically. "
"Otherwise, open an issue on Discord." )
);
}
Future<void> showTokenErrorCouldNotFix() async {
showDialog(
context: appKey.currentContext!,
builder: (context) => const InfoDialog(
text: "A token error occurred. "
"The game couldn't be recovered, open an issue on Discord."
) )
); );
} }
@@ -42,4 +64,35 @@ Future<void> showTokenErrorUnfixable() async {
"Otherwise, open an issue on Discord." "Otherwise, open an issue on Discord."
) )
); );
}
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
if(error == null) {
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: server ? _unsupportedServerError : _corruptedBuildError
)
);
return;
}
showDialog(
context: appKey.currentContext!,
builder: (context) => ErrorDialog(
exception: error,
stackTrace: stackTrace,
errorMessageBuilder: (exception) => _corruptedBuildError
)
);
}
Future<void> showMissingBuildError(FortniteVersion version) async {
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "${version.location.path} no longer contains a Fortnite executable. "
"This probably means that you deleted it or move it somewhere else."
)
);
} }

View File

@@ -8,41 +8,49 @@ import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart'; import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/embedded/server.dart'; import 'package:reboot_launcher/src/embedded/server.dart';
import 'package:reboot_launcher/src/model/server_type.dart'; import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:sync/semaphore.dart'; import 'package:sync/semaphore.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../main.dart'; import '../../main.dart';
import '../page/home_page.dart';
import '../util/server.dart'; import '../util/server.dart';
extension ServerControllerDialog on ServerController { extension ServerControllerDialog on ServerController {
static Semaphore semaphore = Semaphore(); static Semaphore semaphore = Semaphore();
Future<bool> start({required bool required, required bool askPortKill, bool isRetry = false}) async { Future<bool> restart() async {
await resetWinNat();
return (!started() || await stop()) && await toggle();
}
Future<bool> toggle() async {
try{ try{
semaphore.acquire(); semaphore.acquire();
if (type() == ServerType.local) { if (type() == ServerType.local) {
return _pingSelfInteractive(required); return _pingSelfInteractive();
} }
var oldStarted = started(); var result = await _toggle();
if(oldStarted && required){
return true;
}
started.value = !started.value;
var result = await _startInternal(oldStarted, required, askPortKill, isRetry);
if(!result){ if(!result){
return false; started.value = false;
return false;
} }
return await _pingSelfInteractive(true); var ping = await _pingSelfInteractive();
if(!ping){
started.value = false;
return false;
}
return true;
}finally{ }finally{
semaphore.release(); semaphore.release();
} }
} }
Future<bool> _startInternal(bool oldStarted, bool required, bool askPortKill, bool isRetry) async { Future<bool> _toggle([ServerResultType? lastResultType]) async {
if (oldStarted) { if (started.value) {
var result = await stop(); var result = await stop();
if (!result) { if (!result) {
started.value = true; started.value = true;
@@ -53,23 +61,23 @@ extension ServerControllerDialog on ServerController {
return false; return false;
} }
var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required); started.value = true;
var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions; var conditions = await checkServerPreconditions(host.text, port.text, type.value);
var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions;
if(result.type == ServerResultType.alreadyStarted) { if(result.type == ServerResultType.alreadyStarted) {
started.value = false; started.value = false;
return true; return true;
} }
var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result); var handled = await _handleResultType(result, lastResultType);
if (!handled) { if (!handled) {
started.value = false;
return false; return false;
} }
return handled; return handled;
} }
Future<ServerResult> _startServer(bool closeAutomatically) async { Future<ServerResult> _startServer() async {
try{ try{
switch(type()){ switch(type()){
case ServerType.embedded: case ServerType.embedded:
@@ -79,7 +87,7 @@ extension ServerControllerDialog on ServerController {
embeddedMatchmaker = await startEmbeddedMatchmaker(); embeddedMatchmaker = await startEmbeddedMatchmaker();
break; break;
case ServerType.remote: case ServerType.remote:
var uriResult = await _pingRemoteInteractive(closeAutomatically); var uriResult = await _pingRemoteInteractive();
if(uriResult == null){ if(uriResult == null){
return ServerResult( return ServerResult(
type: ServerResultType.cannotPingServer type: ServerResultType.cannotPingServer
@@ -104,8 +112,9 @@ extension ServerControllerDialog on ServerController {
); );
} }
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, bool askPortKill, ServerResult result) async { Future<bool> _handleResultType(ServerResult result, ServerResultType? lastResultType) async {
switch (result.type) { var newResultType = result.type;
switch (newResultType) {
case ServerResultType.missingHostError: case ServerResultType.missingHostError:
_showMissingHostError(); _showMissingHostError();
return false; return false;
@@ -117,33 +126,43 @@ extension ServerControllerDialog on ServerController {
return false; return false;
case ServerResultType.cannotPingServer: case ServerResultType.cannotPingServer:
return false; return false;
case ServerResultType.portTakenError: case ServerResultType.backendPortTakenError:
if (isRetry) { if (lastResultType == ServerResultType.backendPortTakenError) {
_showPortTakenError(); _showPortTakenError(3551);
return false; return false;
} }
if(askPortKill) { var result = await _showPortTakenDialog(3551);
var result = await _showPortTakenDialog(); if (!result) {
if (!result) { return false;
return false;
}
} }
await freeLawinPort(); await freeLawinPort();
return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true); await stop();
return _toggle(newResultType);
case ServerResultType.matchmakerPortTakenError:
if (lastResultType == ServerResultType.matchmakerPortTakenError) {
_showPortTakenError(8080);
return false;
}
var result = await _showPortTakenDialog(8080);
if (!result) {
return false;
}
await freeMatchmakerPort();
await stop();
return _toggle(newResultType);
case ServerResultType.unknownError: case ServerResultType.unknownError:
showDialog( if(lastResultType == ServerResultType.unknownError) {
context: appKey.currentContext!, _showUnknownError(result);
builder: (context) => return false;
ErrorDialog( }
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace, await resetWinNat();
errorMessageBuilder: ( await stop();
exception) => "Cannot start server: $exception" return _toggle(newResultType);
)
);
return false;
case ServerResultType.alreadyStarted: case ServerResultType.alreadyStarted:
case ServerResultType.canStart: case ServerResultType.canStart:
return true; return true;
@@ -152,16 +171,15 @@ extension ServerControllerDialog on ServerController {
} }
} }
Future<bool> _pingSelfInteractive(bool closeAutomatically) async { Future<bool> _pingSelfInteractive() async {
try { try {
return await showDialog<bool>( var resultFuture = compute(pingSelf, port.text)
.then((value) => value != null);
await showDialog<bool>(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) =>
FutureBuilderDialog( FutureBuilderDialog(
future: Future.wait([ future: _waitFutureOrTime(resultFuture),
compute(pingSelf, port.text),
Future.delayed(const Duration(seconds: 1))
]),
loadingMessage: "Pinging ${type().id} server...", loadingMessage: "Pinging ${type().id} server...",
successfulBody: FutureBuilderDialog.ofMessage( successfulBody: FutureBuilderDialog.ofMessage(
"The ${type().id} server works correctly"), "The ${type().id} server works correctly"),
@@ -169,25 +187,23 @@ extension ServerControllerDialog on ServerController {
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."), "The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
errorMessageBuilder: ( errorMessageBuilder: (
exception) => "An error occurred while pining the ${type().id} server: $exception", exception) => "An error occurred while pining the ${type().id} server: $exception",
closeAutomatically: closeAutomatically closeAutomatically: true
) )
) ?? false; );
return await resultFuture;
} catch (_) { } catch (_) {
return false; return false;
} }
} }
Future<Uri?> _pingRemoteInteractive(bool closeAutomatically) async { Future<Uri?> _pingRemoteInteractive() async {
try { try {
var mainFuture = ping(host.text, port.text); var mainFuture = ping(host.text, port.text);
var result = await showDialog<bool>( await showDialog<bool>(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) =>
FutureBuilderDialog( FutureBuilderDialog(
future: Future.wait([ future: _waitFutureOrTime(mainFuture.then((value) => value != null)),
mainFuture,
Future.delayed(const Duration(seconds: 1))
]),
loadingMessage: "Pinging remote server...", loadingMessage: "Pinging remote server...",
successfulBody: FutureBuilderDialog.ofMessage( successfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port "The server at ${host.text}:${port
@@ -195,32 +211,30 @@ extension ServerControllerDialog on ServerController {
unsuccessfulBody: FutureBuilderDialog.ofMessage( unsuccessfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port "The server at ${host.text}:${port
.text} doesn't work. Check the hostname and/or the port and try again."), .text} doesn't work. Check the hostname and/or the port and try again."),
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception", errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
closeAutomatically: closeAutomatically
) )
) ?? false; ) ?? false;
return result ? await mainFuture : null; return await mainFuture;
} catch (_) { } catch (_) {
return null; return null;
} }
} }
Future<void> _showPortTakenError() async { Future<void> _showPortTakenError(int port) async {
showDialog( showDialog(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) => InfoDialog(
const InfoDialog( text: "Port $port is already in use and the associating process cannot be killed. Kill it manually and try again.",
text: "Port 3551 is already in use and the associating process cannot be killed. Kill it manually and try again.",
) )
); );
} }
Future<bool> _showPortTakenDialog() async { Future<bool> _showPortTakenDialog(int port) async {
return await showDialog<bool>( return await showDialog<bool>(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => builder: (context) =>
InfoDialog( InfoDialog(
text: "Port 3551 is already in use, do you want to kill the associated process?", text: "Port $port is already in use, do you want to kill the associated process?",
buttons: [ buttons: [
DialogButton( DialogButton(
type: ButtonType.secondary, type: ButtonType.secondary,
@@ -286,4 +300,24 @@ extension ServerControllerDialog on ServerController {
void _showMissingHostError() { void _showMissingHostError() {
showMessage("Missing the host name for backend server"); showMessage("Missing the host name for backend server");
} }
}
Future<Object?> _showUnknownError(ServerResult result) {
return showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
)
);
}
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) {
return Future.wait<bool>([
resultFuture,
Future.delayed(const Duration(seconds: 1))
.then((value) => true)
]).then((value) => value.reduce((f, s) => f && s));
} }

View File

@@ -1,6 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart'; import '../../main.dart';
import '../page/home_page.dart';
void showMessage(String text){ void showMessage(String text){
showSnackbar( showSnackbar(

View File

@@ -328,6 +328,6 @@ File _getProfileFile(Context context) {
_profiles.createSync(recursive: true); _profiles.createSync(recursive: true);
} }
return File("${_profiles.path}\\ClientProfile-${parseSeasonBuild(context)}.json"); return File("${_profiles.path}\\ClientProfile.json");
} }

View File

@@ -4,28 +4,19 @@ import 'dart:io';
import 'package:jaguar/jaguar.dart'; import 'package:jaguar/jaguar.dart';
class EmbeddedErrorWriter extends ErrorWriter { class EmbeddedErrorWriter extends ErrorWriter {
static const String _errorName = "errors.com.lawinserver.common.not_found"; static const String _errorName404 = "errors.com.lawinserver.common.not_found";
static const String _errorName500 = "errors.com.lawinserver.common.error";
static const String _errorCode = "1004"; static const String _errorCode = "1004";
@override @override
FutureOr<Response> make404(Context ctx) { FutureOr<Response> make404(Context ctx) {
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}"); stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
ctx.response.headers.set('X-Epic-Error-Name', _errorName); ctx.response.headers.set('X-Epic-Error-Name', _errorName404);
ctx.response.headers.set('X-Epic-Error-Code', _errorCode); ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
return Response.json( return Response.json(
statusCode: 204, statusCode: 204,
{} {
); "errorCode": _errorName404,
}
@override
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
return Response(
statusCode: 500,
body: {
"errorCode": _errorName,
"errorMessage": "Sorry the resource you were trying to find could not be found", "errorMessage": "Sorry the resource you were trying to find could not be found",
"numericErrorCode": _errorCode, "numericErrorCode": _errorCode,
"originatingService": "any", "originatingService": "any",
@@ -33,4 +24,20 @@ class EmbeddedErrorWriter extends ErrorWriter {
} }
); );
} }
@override
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
ctx.response.headers.set('X-Epic-Error-Name', _errorName500);
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
return Response.json(
statusCode: 500,
{
"errorCode": _errorName500,
"errorMessage": "Sorry the resource you were trying to find threw an error",
"numericErrorCode": _errorCode,
"originatingService": "any",
"intent": "prod"
}
);
}
} }

View File

@@ -14,24 +14,11 @@ import "error.dart";
import "lightswitch.dart"; import "lightswitch.dart";
import 'matchmaking.dart'; import 'matchmaking.dart';
bool _loggingCapabilities = false;
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async { Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
var server = _createServer(ipQuery); var server = Jaguar(port: 3551, errorWriter: EmbeddedErrorWriter());
await server.serve(logRequests: true);
return server;
}
Future<Jaguar> startEmbeddedMatchmaker() async {
var server = _createMatchmaker();
server.serve(logRequests: true);
return server;
}
Jaguar _createServer(String Function() ipQuery) {
var server = Jaguar(address: "127.0.0.1", port: 3551, errorWriter: EmbeddedErrorWriter());
// Version // Version
server.getJson("unknown", (context) => Response(body: "lawinserver"));
server.getJson("/fortnite/api/version", getVersion); server.getJson("/fortnite/api/version", getVersion);
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate); server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate); server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
@@ -105,23 +92,7 @@ Jaguar _createServer(String Function() ipQuery) {
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy); server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy); server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
return server; await server.serve(logRequests: true);
}
Jaguar _createMatchmaker(){
var server = Jaguar(address: "127.0.0.1", port: 8080);
WebSocket? ws;
server.wsStream(
"/",
(_, input) => ws = input,
after: [(_) => queueMatchmaking(ws!)]
);
return _addLoggingCapabilities(server);
}
Jaguar _addLoggingCapabilities(Jaguar server) {
if(_loggingCapabilities){
return server;
}
server.log.onRecord.listen((line) { server.log.onRecord.listen((line) {
stdout.writeln(line); stdout.writeln(line);
@@ -133,6 +104,17 @@ Jaguar _addLoggingCapabilities(Jaguar server) {
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append); serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
}); });
_loggingCapabilities = true; return server;
}
Future<Jaguar> startEmbeddedMatchmaker() async {
var server = Jaguar(port: 8080);
WebSocket? ws;
server.wsStream(
"/",
(_, input) => ws = input,
after: [(_) => queueMatchmaking(ws!)]
);
await server.serve(logRequests: true);
return server; return server;
} }

View File

@@ -1,4 +1,4 @@
import 'dart:convert'; import 'package:path/path.dart' as path;
import 'dart:io'; import 'dart:io';
import 'package:jaguar/jaguar.dart'; import 'package:jaguar/jaguar.dart';
@@ -11,23 +11,16 @@ import '../util/os.dart';
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings"); final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
const String _engineName = "DefaultEngine.ini"; List getStorageSettings(Context context) =>
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync(); loadEmbeddedDirectory("config")
.listSync()
.map((e) => File(e.path))
.map(_getStorageSetting)
.toList();
const String _gameName = "DefaultGame.ini"; Map<String, Object> _getStorageSetting(File file){
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync(); var name = path.basename(file.path);
var bytes = file.readAsBytesSync();
const String _runtimeName = "DefaultRuntimeOptions.ini";
final String _runtimeIni = loadEmbedded("config/$_runtimeName").readAsStringSync();
List<Map<String, Object>> getStorageSettings(Context context) => [
_getStorageSetting(_engineName, _engineIni),
_getStorageSetting(_gameName, _gameIni),
_getStorageSetting(_runtimeName, _runtimeIni)
];
Map<String, Object> _getStorageSetting(String name, String source){
var bytes = utf8.encode(source);
return { return {
"uniqueFilename": name, "uniqueFilename": name,
"filename": name, "filename": name,
@@ -43,16 +36,8 @@ Map<String, Object> _getStorageSetting(String name, String source){
} }
Response getStorageSetting(Context context) { Response getStorageSetting(Context context) {
switch(context.pathParams.get("file")){ var file = loadEmbedded("config\\${context.pathParams.get("file")}");
case _engineName: return Response(body: file.readAsStringSync());
return Response(body: _engineIni);
case _gameName:
return Response(body: _gameIni);
case _runtimeName:
return Response(body: _runtimeIni);
default:
return Response();
}
} }
Response getStorageFile(Context context) { Response getStorageFile(Context context) {
@@ -107,5 +92,5 @@ File _getSettingsFile(Context context) {
_settings.createSync(recursive: true); _settings.createSync(recursive: true);
} }
return File("${_settings.path}\\ClientSettings-${parseSeasonBuild(context)}.Sav"); return File("${_settings.path}\\ClientSettings.Sav");
} }

View File

@@ -4,8 +4,10 @@ class GameInstance {
final Process gameProcess; final Process gameProcess;
final Process? launcherProcess; final Process? launcherProcess;
final Process? eacProcess; final Process? eacProcess;
bool tokenError;
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess); GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess)
: tokenError = false;
void kill() { void kill() {
gameProcess.kill(ProcessSignal.sigabrt); gameProcess.kill(ProcessSignal.sigabrt);

View File

@@ -0,0 +1,11 @@
import 'package:version/version.dart';
class RebootDownload {
final int updateTime;
final Object? error;
final StackTrace? stackTrace;
RebootDownload(this.updateTime, [this.error, this.stackTrace]);
bool get hasError => error != null;
}

View File

@@ -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/controller/server_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart'; import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/dialog/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/page/settings_page.dart'; import 'package:reboot_launcher/src/page/settings_page.dart';
import 'package:reboot_launcher/src/page/launcher_page.dart'; import 'package:reboot_launcher/src/page/launcher_page.dart';
import 'package:reboot_launcher/src/page/server_page.dart'; import 'package:reboot_launcher/src/page/server_page.dart';
@@ -17,6 +18,7 @@ import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../controller/settings_controller.dart'; import '../controller/settings_controller.dart';
import '../model/server_type.dart';
import '../model/tutorial_page.dart'; import '../model/tutorial_page.dart';
import 'info_page.dart'; import 'info_page.dart';
@@ -32,7 +34,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
static const double _sectionSize = 100.0; static const double _sectionSize = 100.0;
static const double _defaultPadding = 12.0; static const double _defaultPadding = 12.0;
static const int _headerButtonCount = 3; static const int _headerButtonCount = 3;
static const int _sectionButtonCount = 4;
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
@@ -51,20 +52,36 @@ class _HomePageState extends State<HomePage> with WindowListener {
@override @override
void initState() { void initState() {
windowManager.addListener(this); windowManager.addListener(this);
_searchController.addListener(() { _searchController.addListener(_onSearch);
if (searchValue.isEmpty) { _onEasyMode();
_searchItems.value = null; _settingsController.advancedMode.listen((advanced) {
return; _onEasyMode();
} _index.value = _index.value + (advanced ? 1 : -1);
_searchItems.value = _allItems.whereType<PaneItem>()
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
.toList()
.cast<NavigationPaneItem>();
}); });
super.initState(); super.initState();
} }
void _onSearch() {
if (searchValue.isEmpty) {
_searchItems.value = null;
return;
}
_searchItems.value = _allItems.whereType<PaneItem>()
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
.toList()
.cast<NavigationPaneItem>();
}
void _onEasyMode() {
if(_settingsController.advancedMode.value){
return;
}
_gameController.type.value = GameType.client;
_serverController.type.value = ServerType.embedded;
}
@override @override
void dispose() { void dispose() {
windowManager.removeListener(this); windowManager.removeListener(this);
@@ -119,29 +136,38 @@ class _HomePageState extends State<HomePage> with WindowListener {
} }
@override @override
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>( Widget build(BuildContext context) {
onNotification: (notification) => _calculateSize(), return NotificationListener<SizeChangedLayoutNotification>(
child: SizeChangedLayoutNotifier( onNotification: (notification) {
child: Obx(() => Stack( return _calculateSize();
children: [ },
_createNavigationView(), child: SizeChangedLayoutNotifier(
if(_settingsController.displayType() == PaneDisplayMode.top) child: Obx(_getViewStack)
Align( )
alignment: Alignment.topRight, );
child: WindowTitleBar(focused: _focused()) }
),
if(_settingsController.displayType() == PaneDisplayMode.top)
_createTopDisplayGestures(),
if(_focused() && isWin11)
const WindowBorder()
])
)
)
);
Padding _createTopDisplayGestures() => Padding( Widget _getViewStack() {
padding: const EdgeInsets.only( var view = _createNavigationView();
left: _sectionSize * _sectionButtonCount, return Stack(
children: [
view,
if(_settingsController.displayType() == PaneDisplayMode.top)
Align(
alignment: Alignment.topRight,
child: WindowTitleBar(focused: _focused())
),
if(_settingsController.displayType() == PaneDisplayMode.top)
_createTopDisplayGestures(view.pane?.items.length ?? 0),
if(_focused() && isWin11)
const WindowBorder()
]
);
}
Padding _createTopDisplayGestures(int size) => Padding(
padding: EdgeInsets.only(
left: _sectionSize * size,
right: _headerSize * _headerButtonCount, right: _headerSize * _headerButtonCount,
), ),
child: SizedBox( child: SizedBox(
@@ -205,11 +231,11 @@ class _HomePageState extends State<HomePage> with WindowListener {
); );
} }
RenderObjectWidget _createPage(Widget? body) { Widget _createPage(Widget? body) {
if(_settingsController.displayType() == PaneDisplayMode.top){ if(_settingsController.displayType() == PaneDisplayMode.top){
return Padding( return Padding(
padding: const EdgeInsets.all(_defaultPadding), padding: const EdgeInsets.all(_defaultPadding),
child: body child: body
); );
} }
@@ -267,10 +293,9 @@ class _HomePageState extends State<HomePage> with WindowListener {
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [ List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
if(_settingsController.displayType() != PaneDisplayMode.top) if(_settingsController.displayType() != PaneDisplayMode.top)
PaneItem( PaneItem(
title: const Text("Tutorial"), title: const Text("Settings"),
icon: const Icon(FluentIcons.info), icon: const Icon(FluentIcons.settings),
body: const InfoPage(), body: SettingsPage()
onTap: _onTutorial
) )
]; ];
@@ -281,24 +306,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
body: const LauncherPage() body: const LauncherPage()
), ),
PaneItem( if(_settingsController.advancedMode.value)
title: const Text("Backend"), PaneItem(
icon: const Icon(FluentIcons.server_enviroment), title: const Text("Backend"),
body: ServerPage() icon: const Icon(FluentIcons.server_enviroment),
), body: ServerPage()
),
PaneItem( PaneItem(
title: const Text("Settings"), title: const Text("Tutorial"),
icon: const Icon(FluentIcons.settings), icon: const Icon(FluentIcons.info),
body: SettingsPage() body: const InfoPage(),
onTap: _onTutorial
), ),
if(_settingsController.displayType() == PaneDisplayMode.top) if(_settingsController.displayType() == PaneDisplayMode.top)
PaneItem( PaneItem(
title: const Text("Tutorial"), title: const Text("Settings"),
icon: const Icon(FluentIcons.info), icon: const Icon(FluentIcons.settings),
body: const InfoPage(), body: SettingsPage()
onTap: _onTutorial
) )
]; ];

View File

@@ -14,27 +14,25 @@ class InfoPage extends StatefulWidget {
class _InfoPageState extends State<InfoPage> { class _InfoPageState extends State<InfoPage> {
final List<String> _elseTitles = [ final List<String> _elseTitles = [
"Open the settings tab",
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
"Open the home page", "Open the home page",
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
"Type your username if you haven't already", "Type your username if you haven't already",
"Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button", "Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button",
"As you want to play, select client from the dropdown menu", "As you want to play, select client from the dropdown menu",
"Click launch to open the game", "Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
]; ];
final List<String> _ownTitles = [ final List<String> _ownTitles = [
"Open the settings tab",
"Type 127.0.0.1 as the matchmaking host",
"Open the home page", "Open the home page",
"Type 127.0.0.1 as the matchmaking host\n If you didn't know, 127.0.0.1 is the ip for your local machine",
"Type your username if you haven't already", "Type your username if you haven't already",
"Select the version you want to host\n If necessary, install it using the download button", "Select the version you want to host\n If necessary, install it using the download button\n Check the supported versions in #info in the Discord server\n Fortnite 7.40 is the best one to use usually",
"As you want to host, select Headless Server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead", "As you want to host, select headless server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead\n The difference between the two is that the first doesn't render a fortnite instance\n Both will not allow you to play, only to host\n You will see an infinite loading screen when using the normal server\n If you want to also play continue reading",
"Click launch to start the server and wait until the Reboot GUI shows up", "Click launch to start the server and wait until the Reboot GUI shows up\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
"To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step", "To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step",
"When you want to start the game, click on the 'Start Bus Countdown' button", "When you want to start the game, click on the 'Start Bus Countdown' button\n Before clicking that button, make all of your friends join\n This is because joining mid-game isn't allowed",
"If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window", "If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window\n Remember to keep both the headless server(or server) and client open\n If you want to close the client or server, simply switch between them using the menu\n The launcher will remember what instances you have opened",
"Click launch to open the game", "Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1" "Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
]; ];

View File

@@ -1,19 +1,31 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/build_controller.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/game_controller.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/dialog.dart'; import 'package:reboot_launcher/src/dialog/dialog.dart';
import 'package:reboot_launcher/src/model/reboot_download.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart'; import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
import 'package:reboot_launcher/src/widget/home/launch_button.dart'; import 'package:reboot_launcher/src/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/widget/home/username_box.dart'; import 'package:reboot_launcher/src/widget/home/username_box.dart';
import 'package:reboot_launcher/src/widget/home/version_selector.dart'; import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import 'package:reboot_launcher/src/widget/shared/file_selector.dart';
import '../dialog/dialog_button.dart';
import '../model/server_type.dart';
import '../util/checks.dart';
import '../util/reboot.dart'; import '../util/reboot.dart';
import '../widget/shared/smart_input.dart';
import 'home_page.dart';
class LauncherPage extends StatefulWidget { class LauncherPage extends StatefulWidget {
const LauncherPage( const LauncherPage(
@@ -26,21 +38,91 @@ class LauncherPage extends StatefulWidget {
class _LauncherPageState extends State<LauncherPage> { class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>(); final BuildController _buildController = Get.find<BuildController>();
@override @override
void initState() { void initState() {
if(_gameController.updater == null){ if(_gameController.updater == null) {
_gameController.updater = compute(downloadRebootDll, _updateTime) _startUpdater();
..then((value) => _updateTime = value) _setupBuildWarning();
..then((value) => _gameController.updated = true);
_buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {});
} }
super.initState(); super.initState();
} }
void _setupBuildWarning() {
_buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {});
}
void _startUpdater() {
_gameController.updater = StreamController.broadcast();
downloadRebootDll(_settingsController.updateUrl.text, _updateTime)
..then((result) async {
if(!result.hasError){
_updateTime = result.updateTime;
_gameController.updated = true;
_gameController.failing = false;
_gameController.error = false;
_gameController.updater?.add(true);
return;
}
if(_gameController.failing){
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = true;
_gameController.updater?.add(false);
return;
}
_gameController.failing = true;
showDialog(
context: appKey.currentContext!,
builder: (context) => InfoDialog(
text: "An error occurred while downloading the reboot dll: this usually means that your antivirus flagged it. "
"Do you want to add an exclusion to Windows Defender to fix the issue? "
"If you are using a different antivirus disable it manually as this won't work. ",
buttons: [
ErrorDialog.createCopyErrorButton(
error: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
type: ButtonType.secondary,
onClick: () {
Navigator.pop(context);
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = true;
_gameController.updater?.add(false);
}
),
DialogButton(
text: "Add",
type: ButtonType.primary,
onTap: () async {
Navigator.pop(context);
var binary = await loadBinary("antivirus.bat", true);
var result = await runElevated(binary.path, "");
if(!result) {
_gameController.failing = false;
}
_startUpdater();
}
),
],
)
);
})
..catchError((error, stackTrace) {
_gameController.error = true;
_gameController.updater?.add(false);
return RebootDownload(0, error, stackTrace);
});
}
int? get _updateTime { int? get _updateTime {
var storage = GetStorage("update"); var storage = GetStorage("update");
return storage.read("last_update_v2"); return storage.read("last_update_v2");
@@ -64,63 +146,71 @@ class _LauncherPageState extends State<LauncherPage> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => StreamBuilder<bool>(
return FutureBuilder( stream: _gameController.updater!.stream,
future: _gameController.updater ?? Future.value(true), builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
builder: (context, snapshot) { );
if (!_gameController.updated && !snapshot.hasData && !snapshot.hasError) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
}
return Column( Widget get _homeScreen => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if(snapshot.hasError) if(_gameController.error)
_createUpdateError(snapshot), _updateError,
UsernameBox(), UsernameBox(),
const VersionSelector(), Tooltip(
GameTypeSelector(), message:
const LaunchButton() "The hostname of the server that hosts the multiplayer matches",
], child: Obx(() => SmartInput(
); label: "Matchmaking Host",
} placeholder:
); "Type the hostname of the server that hosts the multiplayer matches",
} controller: _settingsController.matchmakingIp,
validatorMode: AutovalidateMode.always,
validator: checkMatchmaking,
enabled: _serverController.type() == ServerType.embedded)
)
),
const VersionSelector(),
if(_settingsController.advancedMode.value)
GameTypeSelector(),
const LaunchButton()
],
);
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) { Widget get _updateScreen => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
ProgressRing(),
SizedBox(height: 16.0),
Text("Updating Reboot DLL...")
],
),
],
);
Widget get _updateError {
return MouseRegion( return MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
showDialog( _gameController.updated = false;
context: context, _gameController.failing = false;
builder: (context) => ErrorDialog( _gameController.error = false;
exception: snapshot.error!, _gameController.updater?.add(false);
stackTrace: snapshot.stackTrace!, _startUpdater();
errorMessageBuilder: (exception) => "Cannot update Reboot dll: ${snapshot.error}"
)
);
}, },
child: const SizedBox( child: const SizedBox(
width: double.infinity, width: double.infinity,
child: InfoBar( child: InfoBar(
title: Text("Cannot update dll"), title: Text("The Reboot dll wasn't downloaded: disable your antivirus or proxy and click here to try again"
),
severity: InfoBarSeverity.info severity: InfoBarSeverity.info
), )
) ),
), ),
); );
} }

View File

@@ -18,12 +18,17 @@ class ServerPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if(_serverController.warning.value) if(_serverController.warning.value)
SizedBox( GestureDetector(
width: double.infinity, onTap: () => _serverController.warning.value = false,
child: InfoBar( child: const MouseRegion(
title: const Text("The backend server handles authentication and parties, not game hosting"), cursor: SystemMouseCursors.click,
severity: InfoBarSeverity.info, child: SizedBox(
onClose: () => _serverController.warning.value = false width: double.infinity,
child: InfoBar(
title: Text("The backend server handles authentication and parties, not game hosting"),
severity: InfoBarSeverity.info
),
),
), ),
), ),
HostInput(), HostInput(),

View File

@@ -1,87 +1,121 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart'; import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/checks.dart'; import '../util/checks.dart';
import '../widget/setting/url_updater.dart';
import '../widget/shared/file_selector.dart'; import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_input.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
SettingsPage({Key? key}) : super(key: key); SettingsPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) =>
return Column( _settingsController.advancedMode.value ? _advancedSettings : _easySettings;
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, Widget get _advancedSettings => Column(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Tooltip( crossAxisAlignment: CrossAxisAlignment.start,
message: children: [
"The hostname of the server that hosts the multiplayer matches", const RebootUpdaterInput(),
child: Obx(() => SmartInput( _createFileSelector(),
label: "Matchmaking Host", _createConsoleSelector(),
placeholder: _createGameSelector(),
"Type the hostname of the server that hosts the multiplayer matches", _createVersionInfo(),
controller: _settingsController.matchmakingIp, _createAdvancedSwitch()
validatorMode: AutovalidateMode.always, ]
validator: checkMatchmaking, );
enabled: _serverController.type() == ServerType.embedded))),
Tooltip( Widget get _easySettings => SizedBox.expand(
message: "The dll that is injected when a server is launched", child: Column(
child: FileSelector( mainAxisAlignment: MainAxisAlignment.center,
label: "Reboot DLL", children: [
placeholder: "Type the path to the reboot dll", const CircleAvatar(
controller: _settingsController.rebootDll, radius: 48,
windowTitle: "Select a dll", backgroundImage: AssetImage("assets/images/auties.png")),
folder: false, const SizedBox(
extension: "dll", height: 16.0,
validator: checkDll, ),
validatorMode: AutovalidateMode.always), const Text("Made by Auties00"),
), const SizedBox(
Tooltip( height: 4.0,
message: "The dll that is injected when a client is launched", ),
child: FileSelector( _versionText,
label: "Console DLL", const SizedBox(
placeholder: "Type the path to the console dll", height: 8.0,
controller: _settingsController.consoleDll, ),
windowTitle: "Select a dll", Button(
folder: false, child: const Text("Switch to advanced mode"),
extension: "dll", onPressed: () => _settingsController.advancedMode.value = true
validator: checkDll, )
validatorMode: AutovalidateMode.always), ],
), ),
Tooltip( );
message: "The dll that is injected to make the game work",
child: FileSelector( Widget _createAdvancedSwitch() => SmartSwitch(
label: "Cranium DLL", label: "Advanced Mode",
placeholder: value: _settingsController.advancedMode
"Type the path to the dll used for authentication", );
controller: _settingsController.authDll,
windowTitle: "Select a dll", Widget _createVersionInfo() => Column(
folder: false, crossAxisAlignment: CrossAxisAlignment.start,
extension: "dll", children: [
validator: checkDll, const Text("Version Status"),
validatorMode: AutovalidateMode.always)), const SizedBox(height: 6.0),
Column( Button(
crossAxisAlignment: CrossAxisAlignment.start, child: _versionText,
children: [ onPressed: () => launchUrl(safeBinariesDirectory.uri)
const Text("Version Status"), )
const SizedBox(height: 6.0), ],
Button( );
child: const Text("6.0${kDebugMode ? '-DEBUG' : '-RELEASE'}"),
onPressed: () => showMessage("What a nice launcher") Widget _createGameSelector() => Tooltip(
) message: "The dll that is injected to make the game work",
], child: FileSelector(
) label: "Cranium DLL",
]); placeholder:
} "Type the path to the dll used for authentication",
controller: _settingsController.authDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always
)
);
Widget _createConsoleSelector() => Tooltip(
message: "The dll that is injected when a client is launched",
child: FileSelector(
label: "Console DLL",
placeholder: "Type the path to the console dll",
controller: _settingsController.consoleDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
);
Widget _createFileSelector() => Tooltip(
message: "The dll that is injected when a server is launched",
child: FileSelector(
label: "Reboot DLL",
placeholder: "Type the path to the reboot dll",
controller: _settingsController.rebootDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
);
Widget get _versionText => const Text("6.4${kDebugMode ? '-DEBUG' : '-RELEASE'}");
} }

View File

@@ -51,12 +51,19 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
Future<Process> downloadManifestBuild( Future<Process> downloadManifestBuild(
String manifestUrl, String destination, Function(double, String) onProgress) async { String manifestUrl, String destination, Function(double, String) onProgress) async {
var buildExe = await loadBinary("build.exe", false); var log = await loadBinary("download.txt", true);
await log.create();
var buildExe = await loadBinary("build.exe", true);
var process = await Process.start(buildExe.path, [manifestUrl, destination]); var process = await Process.start(buildExe.path, [manifestUrl, destination]);
log.writeAsString("Starting download of: $manifestUrl\n", mode: FileMode.append);
process.errLines process.errLines
.where((message) => message.contains("%")) .where((message) => message.contains("%"))
.forEach((message) => onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1))); .forEach((message) {
log.writeAsString("$message\n", mode: FileMode.append);
onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1));
});
return process; return process;
} }
@@ -104,7 +111,7 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
var shell = Shell( var shell = Shell(
commandVerbose: false, commandVerbose: false,
commentVerbose: false, commentVerbose: false,
workingDirectory: safeBinariesDirectory workingDirectory: safeBinariesDirectory.path
); );
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\""); await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
} finally { } finally {

View File

@@ -14,6 +14,14 @@ String? checkVersion(String? text, List<FortniteVersion> versions) {
return null; return null;
} }
String? checkChangeVersion(String? text) {
if (text == null || text.isEmpty) {
return 'Empty version name';
}
return null;
}
String? checkGameFolder(text) { String? checkGameFolder(text) {
if (text == null || text.isEmpty) { if (text == null || text.isEmpty) {
return 'Empty game path'; return 'Empty game path';

View File

@@ -1,6 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart'; import '../../main.dart';
import '../page/home_page.dart';
import '../dialog/dialog.dart'; import '../dialog/dialog.dart';
void onError(Object? exception, StackTrace? stackTrace, bool framework) { void onError(Object? exception, StackTrace? stackTrace, bool framework) {

View File

@@ -4,6 +4,8 @@ import 'package:win32/win32.dart';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'dart:ffi'; import 'dart:ffi';
import 'package:path/path.dart' as path;
const int appBarSize = 2; const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))'); final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -18,7 +20,7 @@ bool get isWin11 {
} }
Future<File> loadBinary(String binary, bool safe) async{ Future<File> loadBinary(String binary, bool safe) async{
var safeBinary = File("$safeBinariesDirectory\\$binary"); var safeBinary = File("${safeBinariesDirectory.path}\\$binary");
if(await safeBinary.exists()){ if(await safeBinary.exists()){
return safeBinary; return safeBinary;
} }
@@ -35,36 +37,77 @@ Future<File> loadBinary(String binary, bool safe) async{
return safeBinary; return safeBinary;
} }
File _locateInternalBinary(String binary) =>
File("${internalAssetsDirectory.path}\\binaries\\$binary");
Future<void> resetWinNat() async {
var binary = await loadBinary("winnat.bat", true);
await runElevated(binary.path, "");
}
Future<bool> runElevated(String executable, String args) async { Future<bool> runElevated(String executable, String args) async {
var shellInput = calloc<SHELLEXECUTEINFO>(); var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16(); shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16(); shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = SW_SHOWDEFAULT; shellInput.ref.nShow = SW_HIDE;
shellInput.ref.fMask = 0x00000040; shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>(); shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput); var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1; return shellResult == 1;
} }
File _locateInternalBinary(String binary){ Directory get internalAssetsDirectory =>
return File("$internalBinariesDirectory\\$binary"); Directory("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets");
}
String get internalBinariesDirectory =>
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
Directory get tempDirectory => Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}"); Directory("${Platform.environment["Temp"]}");
String get safeBinariesDirectory => Directory get safeBinariesDirectory =>
"${Platform.environment["UserProfile"]}\\.reboot_launcher"; Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
Directory get embeddedBackendDirectory =>
Directory("${safeBinariesDirectory.path}\\backend");
File loadEmbedded(String file) { File loadEmbedded(String file) {
var safeBinary = File("$safeBinariesDirectory\\backend\\cli\\$file"); var safeBinary = File("${embeddedBackendDirectory.path}\\$file");
if(safeBinary.existsSync()){ if(safeBinary.existsSync()){
return safeBinary; return safeBinary;
} }
return File("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\$file"); safeBinary.parent.createSync(recursive: true);
var internal = File("${internalAssetsDirectory.path}\\$file");
if(internal.existsSync()) {
internal.copySync(safeBinary.path);
}
return safeBinary;
}
Directory loadEmbeddedDirectory(String directory) {
var safeBinary = Directory("${embeddedBackendDirectory.path}\\$directory");
safeBinary.parent.createSync(recursive: true);
var internal = Directory("${internalAssetsDirectory.path}\\$directory");
_copyFolder(internal, safeBinary);
return safeBinary;
}
void _copyFolder(Directory dir1, Directory dir2) {
if(!dir1.existsSync()){
return;
}
if (!dir2.existsSync()) {
dir2.createSync(recursive: true);
}
dir1.listSync().forEach((element) {
var newPath = "${dir2.path}/${path.basename(element.path)}";
if (element is File) {
var newFile = File(newPath);
newFile.writeAsBytesSync(element.readAsBytesSync());
} else if (element is Directory) {
_copyFolder(element, Directory(newPath));
}
});
} }

34
lib/src/util/process.dart Normal file
View File

@@ -0,0 +1,34 @@
import 'dart:ffi';
import 'package:win32/src/kernel32.dart';
import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
// ignore: non_constant_identifier_names
int NtResumeProcess(int hWnd) {
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
return function(hWnd);
}
// ignore: non_constant_identifier_names
int NtSuspendProcess(int hWnd) {
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
return function(hWnd);
}
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = NtSuspendProcess(processHandle);
CloseHandle(processHandle);
return (result == 0) ? true : false;
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = NtResumeProcess(processHandle);
CloseHandle(processHandle);
return (result == 0) ? true : false;
}

View File

@@ -4,43 +4,57 @@ import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/model/reboot_download.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
const _rebootUrl = const String rebootDownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip"; "https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async { Future<RebootDownload> downloadRebootDll(String url, int? lastUpdateMs) async {
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null; Directory? outputDir;
File? tempZip;
try {
var now = DateTime.now();
var oldRebootDll = await loadBinary("reboot.dll", true);
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await oldRebootDll.exists();
if (lastUpdate != null &&
now.difference(lastUpdate).inHours <= 24 &&
await oldRebootDll.exists()) {
return RebootDownload(lastUpdateMs!);
}
var response = await http.get(Uri.parse(rebootDownloadUrl));
var tempZip = await loadBinary("reboot.zip", true);
await tempZip.writeAsBytes(response.bodyBytes);
var outputDir = await safeBinariesDirectory.createTemp("reboot_out");
await extractFileToDisk(tempZip.path, outputDir.path);
var rebootDll = File(outputDir
.listSync()
.firstWhere((element) => path.extension(element.path) == ".dll")
.path);
if (!exists ||
sha1.convert(await oldRebootDll.readAsBytes()) !=
sha1.convert(await rebootDll.readAsBytes())) {
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
}
return RebootDownload(now.millisecondsSinceEpoch);
} catch (error, stackTrace) {
return RebootDownload(-1, error, stackTrace);
} finally {
try {
outputDir?.delete(recursive: true);
tempZip?.delete();
} catch (_) {}
}
} }
Future<int> downloadRebootDll(int? lastUpdateMs) async { Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
var now = DateTime.now(); return lastUpdateMs != null
var oldRebootDll = await loadBinary("reboot.dll", true); ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
var lastUpdate = await _getLastUpdate(lastUpdateMs); : null;
var exists = await oldRebootDll.exists(); }
if(lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && await oldRebootDll.exists()){
return lastUpdateMs!;
}
var response = await http.get(Uri.parse(_rebootUrl));
var tempZip = File("${tempDirectory.path}/reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes);
var outputDir = await tempDirectory.createTemp("reboot");
await extractFileToDisk(tempZip.path, outputDir.path);
var rebootDll = File(
outputDir.listSync()
.firstWhere((element) => path.extension(element.path) == ".dll")
.path
);
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
outputDir.delete(recursive: true);
return now.millisecondsSinceEpoch;
}
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
outputDir.delete(recursive: true);
return now.millisecondsSinceEpoch;
}

View File

@@ -8,29 +8,40 @@ import 'package:reboot_launcher/src/util/os.dart';
import 'package:shelf_proxy/shelf_proxy.dart'; import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:shelf/shelf_io.dart'; import 'package:shelf/shelf_io.dart';
import 'package:http/http.dart' as http;
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt"); final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
Future<bool> isLawinPortFree() async { Future<bool> isLawinPortFree() async {
try { return http.get(Uri.parse("http://127.0.0.1:3551/unknown"))
var portBat = await loadBinary("port.bat", true); .timeout(const Duration(milliseconds: 500))
var process = await Process.run(portBat.path, []); .then((value) => false)
return !process.outText.contains(" LISTENING "); .onError((error, stackTrace) => true);
}catch(_){ }
return ServerSocket.bind("127.0.0.1", 3551)
.then((socket) => socket.close()) Future<bool> isMatchmakerPortFree() async {
.then((_) => true) return HttpServer.bind("127.0.0.1", 8080)
.onError((error, _) => false); .then((socket) => socket.close())
} .then((_) => true)
.onError((error, _) => false);
} }
Future<void> freeLawinPort() async { Future<void> freeLawinPort() async {
var releaseBat = await loadBinary("release.bat", false); var releaseBat = await loadBinary("kill_lawin_port.bat", false);
var result = await Process.run(releaseBat.path, []); var result = await Process.run(releaseBat.path, []);
if(!result.outText.contains("Access is denied")){ if(result.exitCode == 1){
return; await runElevated(releaseBat.path, "");
await Future.delayed(const Duration(seconds: 1));
} }
}
await runElevated(releaseBat.path, ""); Future<void> freeMatchmakerPort() async {
var releaseBat = await loadBinary("kill_matchmaker_port.bat", false);
var result = await Process.run(releaseBat.path, []);
if(result.exitCode == 1){
await runElevated(releaseBat.path, "");
await Future.delayed(const Duration(seconds: 1));
}
} }
List<String> createRebootArgs(String username, GameType type) { List<String> createRebootArgs(String username, GameType type) {
@@ -92,7 +103,7 @@ String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFir
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null; String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type, bool needsFreePort) async { Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
host = host.trim(); host = host.trim();
if(host.isEmpty){ if(host.isEmpty){
return ServerResult( return ServerResult(
@@ -113,19 +124,16 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
); );
} }
if(type == ServerType.embedded || type == ServerType.remote){ if(type != ServerType.local && !(await isLawinPortFree())){
var free = await isLawinPortFree(); return ServerResult(
if (!free) { type: ServerResultType.backendPortTakenError
if(!needsFreePort) { );
return ServerResult( }
type: ServerResultType.alreadyStarted
);
}
return ServerResult( if(type == ServerType.embedded && !(await isMatchmakerPortFree())){
type: ServerResultType.portTakenError return ServerResult(
); type: ServerResultType.backendPortTakenError
} );
} }
return ServerResult( return ServerResult(
@@ -151,7 +159,8 @@ enum ServerResultType {
missingPortError, missingPortError,
illegalPortError, illegalPortError,
cannotPingServer, cannotPingServer,
portTakenError, backendPortTakenError,
matchmakerPortTakenError,
canStart, canStart,
alreadyStarted, alreadyStarted,
unknownError, unknownError,

View File

@@ -1,7 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_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/model/game_type.dart'; import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
class GameTypeSelector extends StatelessWidget { class GameTypeSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
@@ -12,33 +14,34 @@ class GameTypeSelector extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Tooltip( return Tooltip(
message: "The type of Fortnite instance to launch", message: "The type of Fortnite instance to launch",
child: InfoLabel( child: _createAdvancedSelector(),
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList()))
),
),
); );
} }
MenuFlyoutItem _createItem(GameType type) { Widget _createAdvancedSelector() => InfoLabel(
return MenuFlyoutItem( label: "Type",
text: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Tooltip( child: Obx(() => DropDownButton(
message: type.message, leading: Text(_gameController.type.value.name),
child: Text(type.name) items: GameType.values
) .map((type) => _createItem(type))
), .toList())
onPressed: () { )
_gameController.type(type); )
_gameController.started.value = _gameController.currentGameInstance != null; );
}
); MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
} text: SizedBox(
width: double.infinity,
child: Tooltip(
message: type.message,
child: Text(type.name)
)
),
onPressed: () {
_gameController.type(type);
_gameController.started.value = _gameController.currentGameInstance != null;
}
);
} }

View File

@@ -18,7 +18,6 @@ import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart'; import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart'; import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart'; import 'package:reboot_launcher/src/util/server.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/../main.dart'; import 'package:reboot_launcher/src/../main.dart';
@@ -26,6 +25,8 @@ import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart'; import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/game_instance.dart'; import 'package:reboot_launcher/src/model/game_instance.dart';
import '../../page/home_page.dart';
import '../../util/process.dart';
import '../shared/smart_check_box.dart'; import '../shared/smart_check_box.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
@@ -39,6 +40,10 @@ class LaunchButton extends StatefulWidget {
class _LaunchButtonState extends State<LaunchButton> { class _LaunchButtonState extends State<LaunchButton> {
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()"; final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
final List<String> _corruptedBuildErrors = [
"when 0 bytes remain",
"Pak chunk signature verification failed!"
];
final List<String> _errorStrings = [ final List<String> _errorStrings = [
"port 3551 failed: Connection refused", "port 3551 failed: Connection refused",
"Unable to login to Fortnite servers", "Unable to login to Fortnite servers",
@@ -69,7 +74,7 @@ class _LaunchButtonState extends State<LaunchButton> {
child: Obx(() => Tooltip( child: Obx(() => Tooltip(
message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance", message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance",
child: Button( child: Button(
onPressed: _onPressed, onPressed: () => _start(_gameController.type()),
child: Text(_gameController.started() ? "Close" : "Launch") child: Text(_gameController.started() ? "Close" : "Launch")
), ),
)), )),
@@ -77,9 +82,9 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
} }
void _onPressed() async { void _start(GameType type) async {
if (_gameController.started()) { if (_gameController.started()) {
_onStop(_gameController.type()); _onStop(type);
return; return;
} }
@@ -87,7 +92,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if (_gameController.username.text.isEmpty) { if (_gameController.username.text.isEmpty) {
if(_serverController.type() != ServerType.local){ if(_serverController.type() != ServerType.local){
showMessage("Missing username"); showMessage("Missing username");
_onStop(_gameController.type()); _onStop(type);
return; return;
} }
@@ -96,25 +101,31 @@ class _LaunchButtonState extends State<LaunchButton> {
if (_gameController.selectedVersionObs.value == null) { if (_gameController.selectedVersionObs.value == null) {
showMessage("No version is selected"); showMessage("No version is selected");
_onStop(_gameController.type()); _onStop(type);
return; return;
} }
for (var element in Injectable.values) {
if(await _getDllPath(element, type) == null) {
return;
}
}
try { try {
_fail = false;
await _resetLogFile(); await _resetLogFile();
var version = _gameController.selectedVersionObs.value!; var version = _gameController.selectedVersionObs.value!;
var gamePath = version.executable?.path; var gamePath = version.executable?.path;
if(gamePath == null){ if(gamePath == null){
_onError("${version.location.path} no longer contains a Fortnite executable, did you delete it?", null); showMissingBuildError(version);
_onStop(_gameController.type()); _onStop(type);
return; return;
} }
var result = await _serverController.start(required: true, askPortKill: false); var result = _serverController.started() || await _serverController.toggle();
if(!result){ if(!result){
showMessage("Cannot launch the game as the backend didn't start up correctly"); _onStop(type);
_onStop(_gameController.type());
return; return;
} }
@@ -122,15 +133,15 @@ class _LaunchButtonState extends State<LaunchButton> {
await compute(patchHeadless, version.executable!); await compute(patchHeadless, version.executable!);
await _startMatchMakingServer(); await _startMatchMakingServer();
await _startGameProcesses(version, _gameController.type()); await _startGameProcesses(version, type);
if(_gameController.type() == GameType.headlessServer){ if(type == GameType.headlessServer){
await _showServerLaunchingWarning(); await _showServerLaunchingWarning();
} }
} catch (exception, stacktrace) { } catch (exception, stacktrace) {
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
_onError(exception, stacktrace); showCorruptedBuildError(type != GameType.client, exception, stacktrace);
_onStop(_gameController.type()); _onStop(type);
} }
} }
@@ -143,7 +154,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
Future<void> _startMatchMakingServer() async { Future<void> _startMatchMakingServer() async {
if(_gameController.type() != GameType.client || _settingsController.doNotAskAgain()){ if(_gameController.type() != GameType.client){
return; return;
} }
@@ -158,57 +169,70 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
var controller = CheckboxController(); var result = await _askToStartMatchMakingServer();
var result = await showDialog<bool>( if(result != true){
context: context,
builder: (context) => ContentDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: double.infinity,
child: Text(
"The matchmaking ip is set to the local machine, but no server is running. "
"If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.",
textAlign: TextAlign.start,
)
),
const SizedBox(height: 12.0),
SmartCheckBox(
controller: controller,
content: const Text("Don't ask again")
)
],
),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Ignore'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Start a server'),
)
],
)
) ?? false;
_settingsController.doNotAskAgain.value = controller.value;
if(!result){
return; return;
} }
var version = _gameController.selectedVersionObs.value!; var version = _gameController.selectedVersionObs.value!;
_startGameProcesses( await _startGameProcesses(
version, version,
GameType.headlessServer GameType.headlessServer
); );
} }
Future<bool> _askToStartMatchMakingServer() async {
if(_settingsController.doNotAskAgain()) {
return _settingsController.automaticallyStartMatchmaker();
}
var controller = CheckboxController();
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
ContentDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: double.infinity,
child: Text(
"The matchmaking ip is set to the local machine, but no server is running. "
"If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.",
textAlign: TextAlign.start,
)
),
const SizedBox(height: 12.0),
SmartCheckBox(
controller: controller,
content: const Text("Don't ask again")
)
],
),
actions: [
Button(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Ignore'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Start a server'),
)
],
)
);
_settingsController.doNotAskAgain.value = controller.value;
if(result != null){
_settingsController.automaticallyStartMatchmaker.value = result;
}
return result ?? false;
}
Future<Process> _createGameProcess(String gamePath, GameType type) async { Future<Process> _createGameProcess(String gamePath, GameType type) async {
var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type)); var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type));
gameProcess gameProcess
@@ -219,20 +243,20 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
Future<void> _resetLogFile() async { Future<void> _resetLogFile() async {
if(_logFile != null && await _logFile!.exists()){ if(_logFile != null && await _logFile!.exists()){
await _logFile!.delete(); await _logFile!.delete();
} }
} }
Future<Process?> _createLauncherProcess(FortniteVersion version) async { Future<Process?> _createLauncherProcess(FortniteVersion version) async {
var launcherFile = version.launcher; var launcherFile = version.launcher;
if (launcherFile == null) { if (launcherFile == null) {
return null; return null;
} }
var launcherProcess = await Process.start(launcherFile.path, []); var launcherProcess = await Process.start(launcherFile.path, []);
Win32Process(launcherProcess.pid).suspend(); suspend(launcherProcess.pid);
return launcherProcess; return launcherProcess;
} }
Future<Process?> _createEacProcess(FortniteVersion version) async { Future<Process?> _createEacProcess(FortniteVersion version) async {
@@ -242,7 +266,7 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
var eacProcess = await Process.start(eacFile.path, []); var eacProcess = await Process.start(eacFile.path, []);
Win32Process(eacProcess.pid).suspend(); suspend(eacProcess.pid);
return eacProcess; return eacProcess;
} }
@@ -268,8 +292,8 @@ class _LaunchButtonState extends State<LaunchButton> {
var result = await showDialog<bool>( var result = await showDialog<bool>(
context: appKey.currentContext!, context: appKey.currentContext!,
builder: (context) => ProgressDialog( builder: (context) => ProgressDialog(
text: "Launching headless server...", text: "Launching headless server...",
onStop: () =>_onEnd(_gameController.type()) onStop: () =>_onEnd(_gameController.type())
) )
) ?? false; ) ?? false;
@@ -290,6 +314,17 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
if(_corruptedBuildErrors.any((element) => line.contains(element))){
if(_fail){
return;
}
_fail = true;
showCorruptedBuildError(type != GameType.client);
_onStop(type);
return;
}
if(_errorStrings.any((element) => line.contains(element))){ if(_errorStrings.any((element) => line.contains(element))){
if(_fail){ if(_fail){
return; return;
@@ -297,7 +332,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_fail = true; _fail = true;
_closeDialogIfOpen(false); _closeDialogIfOpen(false);
_showTokenError(); _showTokenError(type);
return; return;
} }
@@ -310,30 +345,28 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
_injectOrShowError(Injectable.memoryFix, type); _injectOrShowError(Injectable.memoryFix, type);
_gameController.currentGameInstance?.tokenError = false;
} }
} }
Future<void> _showTokenError() async { Future<void> _showTokenError(GameType type) async {
if(_serverController.type() == ServerType.embedded) { if(_serverController.type() != ServerType.embedded) {
showTokenErrorFixable();
await _serverController.start(
required: true,
askPortKill: false
);
} else {
showTokenErrorUnfixable(); showTokenErrorUnfixable();
_gameController.currentGameInstance?.tokenError = true;
return;
} }
}
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async { var tokenError = _gameController.currentGameInstance?.tokenError;
return showDialog( _gameController.currentGameInstance?.tokenError = true;
context: context, await _serverController.restart();
builder: (context) => ErrorDialog( if (tokenError == true) {
exception: exception, showTokenErrorCouldNotFix();
stackTrace: stackTrace, return;
errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception" }
)
); showTokenErrorFixable();
_onStop(type);
_start(type);
} }
void _onStop(GameType type) { void _onStop(GameType type) {
@@ -351,13 +384,9 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
try { try {
var dllPath = await _getDllPath(injectable); var dllPath = await _getDllPath(injectable, type);
if(!dllPath.existsSync()) { if(dllPath == null) {
await _downloadMissingDll(injectable); return;
if(!dllPath.existsSync()){
_onDllFail(dllPath, type);
return;
}
} }
await injectDll(gameProcess.pid, dllPath.path); await injectDll(gameProcess.pid, dllPath.path);
@@ -367,6 +396,34 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<File?> _getDllPath(Injectable injectable, GameType type) async {
Future<File> getPath(Injectable injectable) async {
switch(injectable){
case Injectable.reboot:
return File(_settingsController.rebootDll.text);
case Injectable.console:
return File(_settingsController.consoleDll.text);
case Injectable.cranium:
return File(_settingsController.authDll.text);
case Injectable.memoryFix:
return await loadBinary("leakv2.dll", true);
}
}
var dllPath = await getPath(injectable);
if(dllPath.existsSync()) {
return dllPath;
}
await _downloadMissingDll(injectable);
if(dllPath.existsSync()) {
return dllPath;
}
_onDllFail(dllPath, type);
return null;
}
void _onDllFail(File dllPath, GameType type) { void _onDllFail(File dllPath, GameType type) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(_fail){ if(_fail){
@@ -380,26 +437,13 @@ class _LaunchButtonState extends State<LaunchButton> {
}); });
} }
Future<File> _getDllPath(Injectable injectable) async {
switch(injectable){
case Injectable.reboot:
return File(_settingsController.rebootDll.text);
case Injectable.console:
return File(_settingsController.consoleDll.text);
case Injectable.cranium:
return File(_settingsController.authDll.text);
case Injectable.memoryFix:
return await loadBinary("leakv2.dll", true);
}
}
Future<void> _downloadMissingDll(Injectable injectable) async { Future<void> _downloadMissingDll(Injectable injectable) async {
if(injectable != Injectable.reboot){ if(injectable != Injectable.reboot){
await loadBinary("$injectable.dll", true); await loadBinary("$injectable.dll", true);
return; return;
} }
await downloadRebootDll(0); await downloadRebootDll(rebootDownloadUrl, 0);
} }
} }

View File

@@ -60,42 +60,62 @@ class _VersionSelectorState extends State<VersionSelector> {
Widget _createSelector(BuildContext context) { Widget _createSelector(BuildContext context) {
return Tooltip( return Tooltip(
message: "The version of Fortnite to launch", message: "The version of Fortnite to launch",
child: Obx(() => DropDownButton( child: Obx(() => _createOptionsMenu(
leading: Text(_gameController.selectedVersionObs.value?.name ?? version: _gameController.selectedVersionObs(),
"Select a version"), close: false,
items: _gameController.hasNoVersions child: DropDownButton(
? [_createDefaultVersionItem()] leading: Text(_gameController.selectedVersionObs.value?.name
: _gameController.versions.value ?? "Select a version"),
.map((version) => _createVersionItem(context, version)) items: _createSelectorItems(context)
.toList())) )
))
); );
} }
MenuFlyoutItem _createVersionItem( List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
BuildContext context, FortniteVersion version) { return _gameController.hasNoVersions ? [_createDefaultVersionItem()]
return MenuFlyoutItem( : _gameController.versions.value
text: Listener( .map((version) => _createVersionItem(context, version))
onPointerDown: (event) async { .toList();
if (event.kind != PointerDeviceKind.mouse || }
event.buttons != kSecondaryMouseButton) {
return;
}
await _openMenu(context, version, event.position); MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) {
}, return MenuFlyoutItem(
text: _createOptionsMenu(
version: version,
close: true,
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: Text(version.name) child: Text(version.name)
), ),
), ),
onPressed: () => _gameController.selectedVersion = version); onPressed: () => _gameController.selectedVersion = version
);
}
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) {
return Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse ||
event.buttons != kSecondaryMouseButton) {
return;
}
if(version == null) {
return;
}
await _openMenu(context, version, event.position, close);
},
child: child
);
} }
MenuFlyoutItem _createDefaultVersionItem() { MenuFlyoutItem _createDefaultVersionItem() {
return MenuFlyoutItem( return MenuFlyoutItem(
text: const SizedBox( text: const SizedBox(
width: double.infinity, child: Text("No versions available")), width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
trailing: const Expanded(child: SizedBox()), trailing: const Expanded(child: SizedBox()),
onPressed: () {}); onPressed: () {});
} }
@@ -114,24 +134,25 @@ class _VersionSelectorState extends State<VersionSelector> {
} }
Future<void> _openMenu( Future<void> _openMenu(
BuildContext context, FortniteVersion version, Offset offset) async { BuildContext context, FortniteVersion version, Offset offset, bool close) async {
var result = await showMenu<ContextualOption>( var controller = FlyoutController();
context: context, var result = await controller.showFlyout(
offset: offset,
builder: (context) => MenuFlyout( builder: (context) => MenuFlyout(
items: ContextualOption.values items: ContextualOption.values
.map((entry) => _createOption(context, entry)) .map((entry) => _createOption(context, entry))
.toList() .toList()
) )
); );
switch (result) { switch (result) {
case ContextualOption.openExplorer: case ContextualOption.openExplorer:
if(!mounted){ if(!mounted){
return; return;
} }
Navigator.of(context).pop(); if(close) {
Navigator.of(context).pop();
}
launchUrl(version.location.uri) launchUrl(version.location.uri)
.onError((error, stackTrace) => _onExplorerError()); .onError((error, stackTrace) => _onExplorerError());
break; break;
@@ -141,7 +162,10 @@ class _VersionSelectorState extends State<VersionSelector> {
return; return;
} }
Navigator.of(context).pop(); if(close) {
Navigator.of(context).pop();
}
await _openRenameDialog(context, version); await _openRenameDialog(context, version);
break; break;
@@ -155,7 +179,9 @@ class _VersionSelectorState extends State<VersionSelector> {
return; return;
} }
Navigator.of(context).pop(); if(close) {
Navigator.of(context).pop();
}
_gameController.removeVersion(version); _gameController.removeVersion(version);
if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) { if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) {
@@ -242,7 +268,7 @@ class _VersionSelectorState extends State<VersionSelector> {
header: "Name", header: "Name",
placeholder: "Type the new version name", placeholder: "Type the new version name",
autofocus: true, autofocus: true,
validator: (text) => checkVersion(text, _gameController.versions.value) validator: (text) => checkChangeVersion(text)
), ),
const SizedBox( const SizedBox(

View File

@@ -18,7 +18,7 @@ class PortInput extends StatelessWidget {
label: "Port", label: "Port",
placeholder: "Type the backend server's port", placeholder: "Type the backend server's port",
controller: _serverController.port, controller: _serverController.port,
enabled: _serverController.type.value != ServerType.embedded enabled: _serverController.type.value == ServerType.remote
)) ))
); );
} }

View File

@@ -23,10 +23,7 @@ class _ServerButtonState extends State<ServerButton> {
child: Obx(() => Tooltip( child: Obx(() => Tooltip(
message: _helpMessage, message: _helpMessage,
child: Button( child: Button(
onPressed: () async => _serverController.start( onPressed: () async => _serverController.toggle(),
required: false,
askPortKill: true
),
child: Text(_buttonText())), child: Text(_buttonText())),
)), )),
), ),

View File

@@ -0,0 +1,66 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/util/selector.dart';
class RebootUpdaterInput extends StatefulWidget {
const RebootUpdaterInput({Key? key}) : super(key: key);
@override
State<RebootUpdaterInput> createState() => _RebootUpdaterInputState();
}
class _RebootUpdaterInputState extends State<RebootUpdaterInput> {
final SettingsController _settingsController = Get.find<SettingsController>();
final RxBool _valid = RxBool(true);
late String? Function(String?) validator;
@override
void initState() {
validator = (value) {
var result = value != null && Uri.tryParse(value) != null;
WidgetsBinding.instance.addPostFrameCallback((_) => _valid.value = result);
return result ? null : "Invalid URL";
};
super.initState();
}
@override
Widget build(BuildContext context) {
return InfoLabel(
label: "Reboot Updater",
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Obx(() => Expanded(
child: TextFormBox(
controller: _settingsController.updateUrl,
placeholder: "Type the URL of the reboot updater",
validator: validator,
autovalidateMode: AutovalidateMode.always,
enabled: _settingsController.autoUpdate.value
)
)),
const SizedBox(width: 16.0),
Tooltip(
message: _settingsController.autoUpdate.value ? "Disable automatic updates" : "Enable automatic updates",
child: Obx(() => Padding(
padding: _valid() ? EdgeInsets.zero : const EdgeInsets.only(bottom: 21.0),
child: Button(
onPressed: () => _settingsController.autoUpdate.value = !_settingsController.autoUpdate.value,
child: Icon(_settingsController.autoUpdate.value ? FluentIcons.disable_updates : FluentIcons.refresh)
))
)
)
],
)
);
}
}

View File

@@ -68,7 +68,7 @@ class _FileSelectorState extends State<FileSelector> {
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
) )
), ),
if (widget.allowNavigator) const SizedBox(width: 8.0), if (widget.allowNavigator) const SizedBox(width: 16.0),
if (widget.allowNavigator) if (widget.allowNavigator)
Tooltip( Tooltip(
message: "Select a ${widget.folder ? 'folder' : 'file'}", message: "Select a ${widget.folder ? 'folder' : 'file'}",

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Launcher for project reboot description: Launcher for project reboot
version: "6.0.0" version: "6.4.0"
publish_to: 'none' publish_to: 'none'
@@ -24,7 +24,6 @@ dependencies:
process_run: ^0.12.3+2 process_run: ^0.12.3+2
url_launcher: ^6.1.5 url_launcher: ^6.1.5
archive: ^3.3.1 archive: ^3.3.1
win32_suspend_process: ^1.0.0
version: ^3.0.2 version: ^3.0.2
crypto: ^3.0.2 crypto: ^3.0.2
async: ^2.8.2 async: ^2.8.2
@@ -42,9 +41,6 @@ dependencies:
hex: ^0.2.0 hex: ^0.2.0
uuid: ^3.0.6 uuid: ^3.0.6
dependency_overrides:
win32: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -66,7 +62,7 @@ msix_config:
display_name: Reboot Launcher display_name: Reboot Launcher
publisher_display_name: Auties00 publisher_display_name: Auties00
identity_name: 31868Auties00.RebootLauncher identity_name: 31868Auties00.RebootLauncher
msix_version: 6.0.0.0 msix_version: 6.4.0.0
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029 publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
logo_path: ./assets/icons/reboot.ico logo_path: ./assets/icons/reboot.ico
architecture: x64 architecture: x64

View File

@@ -1,6 +1,8 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <cstdlib>
#include <flutter/dart_project.h> #include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h> #include <flutter/flutter_view_controller.h>
#include <windows.h> #include <windows.h>
@@ -34,6 +36,7 @@ bool CheckOneInstance()
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) { _In_ wchar_t *command_line, _In_ int show_command) {
_putenv_s("OPENSSL_ia32cap", "~0x20000000");
if(!CheckOneInstance()){ if(!CheckOneInstance()){
return false; return false;
} }