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]
bEnableWebsockets=false
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
[OnlineSubsystemMcp.Xmpp Prod]
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80
[/Script/Engine.InputSettings]
ConsoleKey=F8

View File

@@ -9,12 +9,12 @@ bUploadAthenaStats=false
bUploadAthenaStatsV2=false
[/Script/FortniteGame.FortMatchmakingV2]
bCustomKeyEnabled=false
bCustomKeyEnabled=true
[/Script/FortniteGame.FortChatManager]
bShouldRequestGeneralChatRooms=false
bShouldJoinGlobalChat=false
bShouldJoinFounderChat=false
bShouldJoinFoaunderChat=false
bIsAthenaGlobalChatEnabled=false
[/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_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))
[/Script/Engine.InputSettings]
ConsoleKey=F8

View File

@@ -15,4 +15,7 @@ bIsOutOfSeasonMode=true
+DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden)
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",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"]) {
stdout.writeln("Updating reboot dll...");
try {
await downloadRebootDll(0);
await downloadRebootDll(rebootDownloadUrl, 0);
}catch(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();
void main() async {
await Directory(safeBinariesDirectory)
.create(recursive: true);
await safeBinariesDirectory.create(recursive: true);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
await GetStorage.init("game");

View File

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

View File

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

View File

@@ -54,6 +54,6 @@ Future<void> downloadRequiredDLLs() async {
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
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:convert';
import 'dart:io';
@@ -20,7 +21,9 @@ class GameController extends GetxController {
late final HashMap<GameType, GameInstance> gameInstancesMap;
late final RxBool started;
late bool updated;
Future? updater;
late bool error;
late bool failing;
StreamController<bool>? updater;
GameController() {
_storage = GetStorage("game");
@@ -52,6 +55,10 @@ class GameController extends GetxController {
started = RxBool(false);
updated = false;
error = false;
failing = false;
}
String _readUsername() {

View File

@@ -29,18 +29,10 @@ class ServerController extends GetxController {
host.text = _readHost();
port.text = _readPort();
_storage.write("type", value.index);
if(!started.value) {
return;
}
if(value == ServerType.remote){
remoteServer?.close(force: true);
remoteServer = null;
started.value = false;
return;
}
stop();
});
@@ -76,6 +68,7 @@ class ServerController extends GetxController {
break;
case ServerType.remote:
await remoteServer?.close(force: true);
remoteServer = null;
break;
case ServerType.local:
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 'dart:ui';
import '../util/reboot.dart';
class SettingsController extends GetxController {
late final GetStorage _storage;
late final String originalDll;
late final TextEditingController updateUrl;
late final TextEditingController rebootDll;
late final TextEditingController consoleDll;
late final TextEditingController authDll;
late final TextEditingController matchmakingIp;
late final Rx<PaneDisplayMode> displayType;
late final RxBool automaticallyStartMatchmaker;
late final RxBool doNotAskAgain;
late final RxBool advancedMode;
late final RxBool autoUpdate;
late Rx<TutorialPage> tutorialPage;
late double width;
late double height;
@@ -24,15 +30,24 @@ class SettingsController extends GetxController {
SettingsController() {
_storage = GetStorage("settings");
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.dll");
authDll = _createController("cranium2", "craniumv2.dll");
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
matchmakingIp.addListener(() async {
var text = matchmakingIp.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.listen((value) => _storage.write("do_not_ask_again", value));
@@ -40,6 +55,12 @@ class SettingsController extends GetxController {
height = _storage.read("height") ?? window.physicalSize.height;
offsetX = _storage.read("offset_x");
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);
scrollingDistance = 0.0;
@@ -50,7 +71,7 @@ class SettingsController extends GetxController {
TextEditingController _createController(String key, String name) {
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));
return controller;

View File

@@ -229,6 +229,16 @@ class ErrorDialog extends AbstractDialog {
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
Widget build(BuildContext context) {
return InfoDialog(
@@ -239,14 +249,10 @@ class ErrorDialog extends AbstractDialog {
),
if(stackTrace != null)
DialogButton(
text: "Copy error",
type: ButtonType.primary,
onTap: () async {
FlutterClipboard.controlC("An error occurred: $exception\nStacktrace:\n $stackTrace.toString");
Navigator.of(context).pop();
showMessage("Copied error to clipboard");
},
createCopyErrorButton(
error: exception,
stackTrace: stackTrace,
onClick: () => Navigator.pop(context)
)
],
);

View File

@@ -1,6 +1,19 @@
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/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 {
showDialog(
@@ -26,8 +39,17 @@ Future<void> showTokenErrorFixable() async {
builder: (context) => const InfoDialog(
text: "A token error occurred. "
"The backend server has been automatically restarted to fix the issue. "
"Relaunch your game to check if the issue has been automatically fixed. "
"Otherwise, open an issue on Discord."
"The game has been restarted automatically. "
)
);
}
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."
)
);
}
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/embedded/server.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:url_launcher/url_launcher.dart';
import '../../main.dart';
import '../page/home_page.dart';
import '../util/server.dart';
extension ServerControllerDialog on ServerController {
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{
semaphore.acquire();
if (type() == ServerType.local) {
return _pingSelfInteractive(required);
return _pingSelfInteractive();
}
var oldStarted = started();
if(oldStarted && required){
return true;
}
started.value = !started.value;
var result = await _startInternal(oldStarted, required, askPortKill, isRetry);
var result = await _toggle();
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{
semaphore.release();
}
}
Future<bool> _startInternal(bool oldStarted, bool required, bool askPortKill, bool isRetry) async {
if (oldStarted) {
Future<bool> _toggle([ServerResultType? lastResultType]) async {
if (started.value) {
var result = await stop();
if (!result) {
started.value = true;
@@ -53,23 +61,23 @@ extension ServerControllerDialog on ServerController {
return false;
}
var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required);
var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions;
started.value = true;
var conditions = await checkServerPreconditions(host.text, port.text, type.value);
var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions;
if(result.type == ServerResultType.alreadyStarted) {
started.value = false;
return true;
}
var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result);
var handled = await _handleResultType(result, lastResultType);
if (!handled) {
started.value = false;
return false;
}
return handled;
}
Future<ServerResult> _startServer(bool closeAutomatically) async {
Future<ServerResult> _startServer() async {
try{
switch(type()){
case ServerType.embedded:
@@ -79,7 +87,7 @@ extension ServerControllerDialog on ServerController {
embeddedMatchmaker = await startEmbeddedMatchmaker();
break;
case ServerType.remote:
var uriResult = await _pingRemoteInteractive(closeAutomatically);
var uriResult = await _pingRemoteInteractive();
if(uriResult == null){
return ServerResult(
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 {
switch (result.type) {
Future<bool> _handleResultType(ServerResult result, ServerResultType? lastResultType) async {
var newResultType = result.type;
switch (newResultType) {
case ServerResultType.missingHostError:
_showMissingHostError();
return false;
@@ -117,33 +126,43 @@ extension ServerControllerDialog on ServerController {
return false;
case ServerResultType.cannotPingServer:
return false;
case ServerResultType.portTakenError:
if (isRetry) {
_showPortTakenError();
case ServerResultType.backendPortTakenError:
if (lastResultType == ServerResultType.backendPortTakenError) {
_showPortTakenError(3551);
return false;
}
if(askPortKill) {
var result = await _showPortTakenDialog();
if (!result) {
return false;
}
var result = await _showPortTakenDialog(3551);
if (!result) {
return false;
}
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:
showDialog(
context: appKey.currentContext!,
builder: (context) =>
ErrorDialog(
exception: result.error ?? Exception("Unknown error"),
stackTrace: result.stackTrace,
errorMessageBuilder: (
exception) => "Cannot start server: $exception"
)
);
return false;
if(lastResultType == ServerResultType.unknownError) {
_showUnknownError(result);
return false;
}
await resetWinNat();
await stop();
return _toggle(newResultType);
case ServerResultType.alreadyStarted:
case ServerResultType.canStart:
return true;
@@ -152,16 +171,15 @@ extension ServerControllerDialog on ServerController {
}
}
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
Future<bool> _pingSelfInteractive() async {
try {
return await showDialog<bool>(
var resultFuture = compute(pingSelf, port.text)
.then((value) => value != null);
await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: Future.wait([
compute(pingSelf, port.text),
Future.delayed(const Duration(seconds: 1))
]),
future: _waitFutureOrTime(resultFuture),
loadingMessage: "Pinging ${type().id} server...",
successfulBody: FutureBuilderDialog.ofMessage(
"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."),
errorMessageBuilder: (
exception) => "An error occurred while pining the ${type().id} server: $exception",
closeAutomatically: closeAutomatically
closeAutomatically: true
)
) ?? false;
);
return await resultFuture;
} catch (_) {
return false;
}
}
Future<Uri?> _pingRemoteInteractive(bool closeAutomatically) async {
Future<Uri?> _pingRemoteInteractive() async {
try {
var mainFuture = ping(host.text, port.text);
var result = await showDialog<bool>(
await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: Future.wait([
mainFuture,
Future.delayed(const Duration(seconds: 1))
]),
future: _waitFutureOrTime(mainFuture.then((value) => value != null)),
loadingMessage: "Pinging remote server...",
successfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port
@@ -195,32 +211,30 @@ extension ServerControllerDialog on ServerController {
unsuccessfulBody: FutureBuilderDialog.ofMessage(
"The server at ${host.text}:${port
.text} doesn't work. Check the hostname and/or the port and try again."),
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception",
closeAutomatically: closeAutomatically
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
)
) ?? false;
return result ? await mainFuture : null;
return await mainFuture;
} catch (_) {
return null;
}
}
Future<void> _showPortTakenError() async {
Future<void> _showPortTakenError(int port) async {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "Port 3551 is already in use and the associating process cannot be killed. Kill it manually and try again.",
builder: (context) => InfoDialog(
text: "Port $port 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>(
context: appKey.currentContext!,
builder: (context) =>
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: [
DialogButton(
type: ButtonType.secondary,
@@ -286,4 +300,24 @@ extension ServerControllerDialog on ServerController {
void _showMissingHostError() {
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 '../../main.dart';
import '../page/home_page.dart';
void showMessage(String text){
showSnackbar(

View File

@@ -328,6 +328,6 @@ File _getProfileFile(Context context) {
_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';
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";
@override
FutureOr<Response> make404(Context ctx) {
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);
return Response.json(
statusCode: 204,
{}
);
}
@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,
{
"errorCode": _errorName404,
"errorMessage": "Sorry the resource you were trying to find could not be found",
"numericErrorCode": _errorCode,
"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 'matchmaking.dart';
bool _loggingCapabilities = false;
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
var server = _createServer(ipQuery);
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());
var server = Jaguar(port: 3551, errorWriter: EmbeddedErrorWriter());
// Version
server.getJson("unknown", (context) => Response(body: "lawinserver"));
server.getJson("/fortnite/api/version", getVersion);
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.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
return server;
}
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;
}
await server.serve(logRequests: true);
server.log.onRecord.listen((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);
});
_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;
}

View File

@@ -1,4 +1,4 @@
import 'dart:convert';
import 'package:path/path.dart' as path;
import 'dart:io';
import 'package:jaguar/jaguar.dart';
@@ -11,23 +11,16 @@ import '../util/os.dart';
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
const String _engineName = "DefaultEngine.ini";
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync();
List getStorageSettings(Context context) =>
loadEmbeddedDirectory("config")
.listSync()
.map((e) => File(e.path))
.map(_getStorageSetting)
.toList();
const String _gameName = "DefaultGame.ini";
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync();
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);
Map<String, Object> _getStorageSetting(File file){
var name = path.basename(file.path);
var bytes = file.readAsBytesSync();
return {
"uniqueFilename": name,
"filename": name,
@@ -43,16 +36,8 @@ Map<String, Object> _getStorageSetting(String name, String source){
}
Response getStorageSetting(Context context) {
switch(context.pathParams.get("file")){
case _engineName:
return Response(body: _engineIni);
case _gameName:
return Response(body: _gameIni);
case _runtimeName:
return Response(body: _runtimeIni);
default:
return Response();
}
var file = loadEmbedded("config\\${context.pathParams.get("file")}");
return Response(body: file.readAsStringSync());
}
Response getStorageFile(Context context) {
@@ -107,5 +92,5 @@ File _getSettingsFile(Context context) {
_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? launcherProcess;
final Process? eacProcess;
bool tokenError;
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess);
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess)
: tokenError = false;
void kill() {
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/dialog/dialog.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/launcher_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 '../controller/settings_controller.dart';
import '../model/server_type.dart';
import '../model/tutorial_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 _defaultPadding = 12.0;
static const int _headerButtonCount = 3;
static const int _sectionButtonCount = 4;
final GameController _gameController = Get.find<GameController>();
final SettingsController _settingsController = Get.find<SettingsController>();
@@ -51,20 +52,36 @@ class _HomePageState extends State<HomePage> with WindowListener {
@override
void initState() {
windowManager.addListener(this);
_searchController.addListener(() {
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>();
_searchController.addListener(_onSearch);
_onEasyMode();
_settingsController.advancedMode.listen((advanced) {
_onEasyMode();
_index.value = _index.value + (advanced ? 1 : -1);
});
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
void dispose() {
windowManager.removeListener(this);
@@ -119,29 +136,38 @@ class _HomePageState extends State<HomePage> with WindowListener {
}
@override
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) => _calculateSize(),
child: SizeChangedLayoutNotifier(
child: Obx(() => Stack(
children: [
_createNavigationView(),
if(_settingsController.displayType() == PaneDisplayMode.top)
Align(
alignment: Alignment.topRight,
child: WindowTitleBar(focused: _focused())
),
if(_settingsController.displayType() == PaneDisplayMode.top)
_createTopDisplayGestures(),
if(_focused() && isWin11)
const WindowBorder()
])
)
)
);
Widget build(BuildContext context) {
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
return _calculateSize();
},
child: SizeChangedLayoutNotifier(
child: Obx(_getViewStack)
)
);
}
Padding _createTopDisplayGestures() => Padding(
padding: const EdgeInsets.only(
left: _sectionSize * _sectionButtonCount,
Widget _getViewStack() {
var view = _createNavigationView();
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,
),
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){
return Padding(
padding: const EdgeInsets.all(_defaultPadding),
child: body
padding: const EdgeInsets.all(_defaultPadding),
child: body
);
}
@@ -267,10 +293,9 @@ class _HomePageState extends State<HomePage> with WindowListener {
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
if(_settingsController.displayType() != PaneDisplayMode.top)
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: const InfoPage(),
onTap: _onTutorial
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
body: SettingsPage()
)
];
@@ -281,24 +306,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
body: const LauncherPage()
),
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.server_enviroment),
body: ServerPage()
),
if(_settingsController.advancedMode.value)
PaneItem(
title: const Text("Backend"),
icon: const Icon(FluentIcons.server_enviroment),
body: ServerPage()
),
PaneItem(
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
body: SettingsPage()
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: const InfoPage(),
onTap: _onTutorial
),
if(_settingsController.displayType() == PaneDisplayMode.top)
PaneItem(
title: const Text("Tutorial"),
icon: const Icon(FluentIcons.info),
body: const InfoPage(),
onTap: _onTutorial
title: const Text("Settings"),
icon: const Icon(FluentIcons.settings),
body: SettingsPage()
)
];

View File

@@ -14,27 +14,25 @@ class InfoPage extends StatefulWidget {
class _InfoPageState extends State<InfoPage> {
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",
"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",
"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",
"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"
];
final List<String> _ownTitles = [
"Open the settings tab",
"Type 127.0.0.1 as the matchmaking host",
"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",
"Select the version you want to host\n If necessary, install it using the download button",
"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",
"Click launch to start the server and wait until the Reboot GUI shows up",
"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\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\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",
"When you want to start the game, click on the 'Start Bus Countdown' button",
"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",
"Click launch to open the game",
"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\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\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"
];

View File

@@ -1,19 +1,31 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.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/launch_button.dart';
import 'package:reboot_launcher/src/widget/home/username_box.dart';
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import 'package: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 '../widget/shared/smart_input.dart';
import 'home_page.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
@@ -26,21 +38,91 @@ class LauncherPage extends StatefulWidget {
class _LauncherPageState extends State<LauncherPage> {
final GameController _gameController = Get.find<GameController>();
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final BuildController _buildController = Get.find<BuildController>();
@override
void initState() {
if(_gameController.updater == null){
_gameController.updater = compute(downloadRebootDll, _updateTime)
..then((value) => _updateTime = value)
..then((value) => _gameController.updated = true);
_buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {});
if(_gameController.updater == null) {
_startUpdater();
_setupBuildWarning();
}
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 {
var storage = GetStorage("update");
return storage.read("last_update_v2");
@@ -64,63 +146,71 @@ class _LauncherPageState extends State<LauncherPage> {
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _gameController.updater ?? Future.value(true),
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...")
],
),
],
);
}
Widget build(BuildContext context) => StreamBuilder<bool>(
stream: _gameController.updater!.stream,
builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
);
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(snapshot.hasError)
_createUpdateError(snapshot),
UsernameBox(),
const VersionSelector(),
GameTypeSelector(),
const LaunchButton()
],
);
}
);
}
Widget get _homeScreen => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_gameController.error)
_updateError,
UsernameBox(),
Tooltip(
message:
"The hostname of the server that hosts the multiplayer matches",
child: Obx(() => SmartInput(
label: "Matchmaking Host",
placeholder:
"Type the hostname of the server that hosts the multiplayer matches",
controller: _settingsController.matchmakingIp,
validatorMode: AutovalidateMode.always,
validator: checkMatchmaking,
enabled: _serverController.type() == ServerType.embedded)
)
),
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(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (context) => ErrorDialog(
exception: snapshot.error!,
stackTrace: snapshot.stackTrace!,
errorMessageBuilder: (exception) => "Cannot update Reboot dll: ${snapshot.error}"
)
);
_gameController.updated = false;
_gameController.failing = false;
_gameController.error = false;
_gameController.updater?.add(false);
_startUpdater();
},
child: const SizedBox(
width: double.infinity,
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
),
)
)
),
),
);
}

View File

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

View File

@@ -1,87 +1,121 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
import 'package:url_launcher/url_launcher.dart';
import '../util/checks.dart';
import '../widget/setting/url_updater.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_input.dart';
class SettingsPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
SettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tooltip(
message:
"The hostname of the server that hosts the multiplayer matches",
child: Obx(() => SmartInput(
label: "Matchmaking Host",
placeholder:
"Type the hostname of the server that hosts the multiplayer matches",
controller: _settingsController.matchmakingIp,
validatorMode: AutovalidateMode.always,
validator: checkMatchmaking,
enabled: _serverController.type() == ServerType.embedded))),
Tooltip(
message: "The dll that is injected when a server is launched",
child: FileSelector(
label: "Reboot DLL",
placeholder: "Type the path to the reboot dll",
controller: _settingsController.rebootDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
),
Tooltip(
message: "The dll that is injected when a client is launched",
child: FileSelector(
label: "Console DLL",
placeholder: "Type the path to the console dll",
controller: _settingsController.consoleDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always),
),
Tooltip(
message: "The dll that is injected to make the game work",
child: FileSelector(
label: "Cranium DLL",
placeholder:
"Type the path to the dll used for authentication",
controller: _settingsController.authDll,
windowTitle: "Select a dll",
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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 build(BuildContext context) =>
_settingsController.advancedMode.value ? _advancedSettings : _easySettings;
Widget get _advancedSettings => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const RebootUpdaterInput(),
_createFileSelector(),
_createConsoleSelector(),
_createGameSelector(),
_createVersionInfo(),
_createAdvancedSwitch()
]
);
Widget get _easySettings => SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircleAvatar(
radius: 48,
backgroundImage: AssetImage("assets/images/auties.png")),
const SizedBox(
height: 16.0,
),
const Text("Made by Auties00"),
const SizedBox(
height: 4.0,
),
_versionText,
const SizedBox(
height: 8.0,
),
Button(
child: const Text("Switch to advanced mode"),
onPressed: () => _settingsController.advancedMode.value = true
)
],
),
);
Widget _createAdvancedSwitch() => SmartSwitch(
label: "Advanced Mode",
value: _settingsController.advancedMode
);
Widget _createVersionInfo() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Version Status"),
const SizedBox(height: 6.0),
Button(
child: _versionText,
onPressed: () => launchUrl(safeBinariesDirectory.uri)
)
],
);
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(
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]);
log.writeAsString("Starting download of: $manifestUrl\n", mode: FileMode.append);
process.errLines
.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;
}
@@ -104,7 +111,7 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
workingDirectory: safeBinariesDirectory
workingDirectory: safeBinariesDirectory.path
);
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
} finally {

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import '../../main.dart';
import '../page/home_page.dart';
import '../dialog/dialog.dart';
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 'dart:ffi';
import 'package:path/path.dart' as path;
const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -18,7 +20,7 @@ bool get isWin11 {
}
Future<File> loadBinary(String binary, bool safe) async{
var safeBinary = File("$safeBinariesDirectory\\$binary");
var safeBinary = File("${safeBinariesDirectory.path}\\$binary");
if(await safeBinary.exists()){
return safeBinary;
}
@@ -35,36 +37,77 @@ Future<File> loadBinary(String binary, bool safe) async{
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 {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = SW_SHOWDEFAULT;
shellInput.ref.fMask = 0x00000040;
shellInput.ref.nShow = SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1;
}
File _locateInternalBinary(String binary){
return File("$internalBinariesDirectory\\$binary");
}
String get internalBinariesDirectory =>
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
Directory get internalAssetsDirectory =>
Directory("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets");
Directory get tempDirectory =>
Directory("${Platform.environment["Temp"]}");
String get safeBinariesDirectory =>
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
Directory get safeBinariesDirectory =>
Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
Directory get embeddedBackendDirectory =>
Directory("${safeBinariesDirectory.path}\\backend");
File loadEmbedded(String file) {
var safeBinary = File("$safeBinariesDirectory\\backend\\cli\\$file");
var safeBinary = File("${embeddedBackendDirectory.path}\\$file");
if(safeBinary.existsSync()){
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:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/model/reboot_download.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";
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null;
Future<RebootDownload> downloadRebootDll(String url, int? lastUpdateMs) async {
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 {
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 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;
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

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

View File

@@ -1,7 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.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/widget/shared/smart_switch.dart';
class GameTypeSelector extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
@@ -12,33 +14,34 @@ class GameTypeSelector extends StatelessWidget {
Widget build(BuildContext context) {
return Tooltip(
message: "The type of Fortnite instance to launch",
child: InfoLabel(
label: "Type",
child: SizedBox(
width: double.infinity,
child: Obx(() => DropDownButton(
leading: Text(_gameController.type.value.name),
items: GameType.values
.map((type) => _createItem(type))
.toList()))
),
),
child: _createAdvancedSelector(),
);
}
MenuFlyoutItem _createItem(GameType type) {
return 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;
}
);
}
Widget _createAdvancedSelector() => InfoLabel(
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) => 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/reboot.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: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/model/game_instance.dart';
import '../../page/home_page.dart';
import '../../util/process.dart';
import '../shared/smart_check_box.dart';
class LaunchButton extends StatefulWidget {
@@ -39,6 +40,10 @@ class LaunchButton extends StatefulWidget {
class _LaunchButtonState extends State<LaunchButton> {
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
final List<String> _corruptedBuildErrors = [
"when 0 bytes remain",
"Pak chunk signature verification failed!"
];
final List<String> _errorStrings = [
"port 3551 failed: Connection refused",
"Unable to login to Fortnite servers",
@@ -69,7 +74,7 @@ class _LaunchButtonState extends State<LaunchButton> {
child: Obx(() => Tooltip(
message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance",
child: Button(
onPressed: _onPressed,
onPressed: () => _start(_gameController.type()),
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()) {
_onStop(_gameController.type());
_onStop(type);
return;
}
@@ -87,7 +92,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if (_gameController.username.text.isEmpty) {
if(_serverController.type() != ServerType.local){
showMessage("Missing username");
_onStop(_gameController.type());
_onStop(type);
return;
}
@@ -96,25 +101,31 @@ class _LaunchButtonState extends State<LaunchButton> {
if (_gameController.selectedVersionObs.value == null) {
showMessage("No version is selected");
_onStop(_gameController.type());
_onStop(type);
return;
}
for (var element in Injectable.values) {
if(await _getDllPath(element, type) == null) {
return;
}
}
try {
_fail = false;
await _resetLogFile();
var version = _gameController.selectedVersionObs.value!;
var gamePath = version.executable?.path;
if(gamePath == null){
_onError("${version.location.path} no longer contains a Fortnite executable, did you delete it?", null);
_onStop(_gameController.type());
showMissingBuildError(version);
_onStop(type);
return;
}
var result = await _serverController.start(required: true, askPortKill: false);
var result = _serverController.started() || await _serverController.toggle();
if(!result){
showMessage("Cannot launch the game as the backend didn't start up correctly");
_onStop(_gameController.type());
_onStop(type);
return;
}
@@ -122,15 +133,15 @@ class _LaunchButtonState extends State<LaunchButton> {
await compute(patchHeadless, version.executable!);
await _startMatchMakingServer();
await _startGameProcesses(version, _gameController.type());
await _startGameProcesses(version, type);
if(_gameController.type() == GameType.headlessServer){
if(type == GameType.headlessServer){
await _showServerLaunchingWarning();
}
} catch (exception, stacktrace) {
_closeDialogIfOpen(false);
_onError(exception, stacktrace);
_onStop(_gameController.type());
showCorruptedBuildError(type != GameType.client, exception, stacktrace);
_onStop(type);
}
}
@@ -143,7 +154,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
Future<void> _startMatchMakingServer() async {
if(_gameController.type() != GameType.client || _settingsController.doNotAskAgain()){
if(_gameController.type() != GameType.client){
return;
}
@@ -158,57 +169,70 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
var controller = CheckboxController();
var result = await showDialog<bool>(
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){
var result = await _askToStartMatchMakingServer();
if(result != true){
return;
}
var version = _gameController.selectedVersionObs.value!;
_startGameProcesses(
await _startGameProcesses(
version,
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 {
var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type));
gameProcess
@@ -219,20 +243,20 @@ class _LaunchButtonState extends State<LaunchButton> {
}
Future<void> _resetLogFile() async {
if(_logFile != null && await _logFile!.exists()){
if(_logFile != null && await _logFile!.exists()){
await _logFile!.delete();
}
}
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
var launcherFile = version.launcher;
if (launcherFile == null) {
return null;
}
var launcherProcess = await Process.start(launcherFile.path, []);
Win32Process(launcherProcess.pid).suspend();
return launcherProcess;
if (launcherFile == null) {
return null;
}
var launcherProcess = await Process.start(launcherFile.path, []);
suspend(launcherProcess.pid);
return launcherProcess;
}
Future<Process?> _createEacProcess(FortniteVersion version) async {
@@ -242,7 +266,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
var eacProcess = await Process.start(eacFile.path, []);
Win32Process(eacProcess.pid).suspend();
suspend(eacProcess.pid);
return eacProcess;
}
@@ -268,8 +292,8 @@ class _LaunchButtonState extends State<LaunchButton> {
var result = await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) => ProgressDialog(
text: "Launching headless server...",
onStop: () =>_onEnd(_gameController.type())
text: "Launching headless server...",
onStop: () =>_onEnd(_gameController.type())
)
) ?? false;
@@ -290,6 +314,17 @@ class _LaunchButtonState extends State<LaunchButton> {
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(_fail){
return;
@@ -297,7 +332,7 @@ class _LaunchButtonState extends State<LaunchButton> {
_fail = true;
_closeDialogIfOpen(false);
_showTokenError();
_showTokenError(type);
return;
}
@@ -310,30 +345,28 @@ class _LaunchButtonState extends State<LaunchButton> {
}
_injectOrShowError(Injectable.memoryFix, type);
_gameController.currentGameInstance?.tokenError = false;
}
}
Future<void> _showTokenError() async {
if(_serverController.type() == ServerType.embedded) {
showTokenErrorFixable();
await _serverController.start(
required: true,
askPortKill: false
);
} else {
Future<void> _showTokenError(GameType type) async {
if(_serverController.type() != ServerType.embedded) {
showTokenErrorUnfixable();
_gameController.currentGameInstance?.tokenError = true;
return;
}
}
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
return showDialog(
context: context,
builder: (context) => ErrorDialog(
exception: exception,
stackTrace: stackTrace,
errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception"
)
);
var tokenError = _gameController.currentGameInstance?.tokenError;
_gameController.currentGameInstance?.tokenError = true;
await _serverController.restart();
if (tokenError == true) {
showTokenErrorCouldNotFix();
return;
}
showTokenErrorFixable();
_onStop(type);
_start(type);
}
void _onStop(GameType type) {
@@ -351,13 +384,9 @@ class _LaunchButtonState extends State<LaunchButton> {
}
try {
var dllPath = await _getDllPath(injectable);
if(!dllPath.existsSync()) {
await _downloadMissingDll(injectable);
if(!dllPath.existsSync()){
_onDllFail(dllPath, type);
return;
}
var dllPath = await _getDllPath(injectable, type);
if(dllPath == null) {
return;
}
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) {
WidgetsBinding.instance.addPostFrameCallback((_) {
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 {
if(injectable != Injectable.reboot){
await loadBinary("$injectable.dll", true);
return;
}
await downloadRebootDll(0);
await downloadRebootDll(rebootDownloadUrl, 0);
}
}

View File

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

View File

@@ -18,7 +18,7 @@ class PortInput extends StatelessWidget {
label: "Port",
placeholder: "Type the backend server's 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(
message: _helpMessage,
child: Button(
onPressed: () async => _serverController.start(
required: false,
askPortKill: true
),
onPressed: () async => _serverController.toggle(),
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
)
),
if (widget.allowNavigator) const SizedBox(width: 8.0),
if (widget.allowNavigator) const SizedBox(width: 16.0),
if (widget.allowNavigator)
Tooltip(
message: "Select a ${widget.folder ? 'folder' : 'file'}",

View File

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

View File

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