checkpoint
1
assets/binaries/antivirus.bat
Normal file
@@ -0,0 +1 @@
|
||||
powershell -inputformat none -outputformat none -NonInteractive -Command Add-MpPreference -ExclusionPath "%UserProfile%/.reboot_launcher"
|
||||
1
assets/binaries/kill_lawin_port.bat
Normal file
@@ -0,0 +1 @@
|
||||
for /f "tokens=5" %%a in ('netstat -aon ^| find ":3551" ^| find "LISTENING"') do taskkill /f /pid %%a
|
||||
1
assets/binaries/kill_matchmaker_port.bat
Normal file
@@ -0,0 +1 @@
|
||||
for /f "tokens=5" %%a in ('netstat -aon ^| find ":8080" ^| find "LISTENING"') do taskkill /f /pid %%a
|
||||
@@ -1 +0,0 @@
|
||||
netstat -ano|find ":3551"
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
sc query "MongoDB" | findstr /i "STATE"
|
||||
2
assets/binaries/winnat.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
net stop winnat
|
||||
net start winnat
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -16,3 +16,6 @@ bIsOutOfSeasonMode=true
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
+DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||
|
||||
[/Script/Engine.InputSettings]
|
||||
ConsoleKey=F8
|
||||
BIN
assets/images/auties.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
void _injectRequiredDLLs(bool host, String rebootDll) {
|
||||
if(host) {
|
||||
_injectOrShowError(rebootDll, false);
|
||||
if(hosting) {
|
||||
_injectOrShowError(dll, false);
|
||||
}else {
|
||||
_injectOrShowError("console.dll");
|
||||
}
|
||||
|
||||
_injectOrShowError("leakv2.dll");
|
||||
}
|
||||
}
|
||||
|
||||
void _kill() {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -43,3 +65,34 @@ Future<void> showTokenErrorUnfixable() async {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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."
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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){
|
||||
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();
|
||||
var result = await _showPortTakenDialog(3551);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
await freeLawinPort();
|
||||
return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true);
|
||||
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"
|
||||
)
|
||||
);
|
||||
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:
|
||||
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,
|
||||
@@ -287,3 +301,23 @@ extension ServerControllerDialog on ServerController {
|
||||
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));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import '../../main.dart';
|
||||
import '../page/home_page.dart';
|
||||
|
||||
void showMessage(String text){
|
||||
showSnackbar(
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
11
lib/src/model/reboot_download.dart
Normal 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;
|
||||
}
|
||||
@@ -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,7 +52,16 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
@override
|
||||
void initState() {
|
||||
windowManager.addListener(this);
|
||||
_searchController.addListener(() {
|
||||
_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;
|
||||
@@ -61,8 +71,15 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
||||
.toList()
|
||||
.cast<NavigationPaneItem>();
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onEasyMode() {
|
||||
if(_settingsController.advancedMode.value){
|
||||
return;
|
||||
}
|
||||
|
||||
_gameController.type.value = GameType.client;
|
||||
_serverController.type.value = ServerType.embedded;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -119,29 +136,38 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) => _calculateSize(),
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener<SizeChangedLayoutNotification>(
|
||||
onNotification: (notification) {
|
||||
return _calculateSize();
|
||||
},
|
||||
child: SizeChangedLayoutNotifier(
|
||||
child: Obx(() => Stack(
|
||||
child: Obx(_getViewStack)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getViewStack() {
|
||||
var view = _createNavigationView();
|
||||
return Stack(
|
||||
children: [
|
||||
_createNavigationView(),
|
||||
view,
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: WindowTitleBar(focused: _focused())
|
||||
),
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
_createTopDisplayGestures(),
|
||||
_createTopDisplayGestures(view.pane?.items.length ?? 0),
|
||||
if(_focused() && isWin11)
|
||||
const WindowBorder()
|
||||
])
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Padding _createTopDisplayGestures() => Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: _sectionSize * _sectionButtonCount,
|
||||
Padding _createTopDisplayGestures(int size) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: _sectionSize * size,
|
||||
right: _headerSize * _headerButtonCount,
|
||||
),
|
||||
child: SizedBox(
|
||||
@@ -205,7 +231,7 @@ 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),
|
||||
@@ -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()
|
||||
),
|
||||
|
||||
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()
|
||||
),
|
||||
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Tutorial"),
|
||||
icon: const Icon(FluentIcons.info),
|
||||
body: const InfoPage(),
|
||||
onTap: _onTutorial
|
||||
),
|
||||
|
||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||
PaneItem(
|
||||
title: const Text("Settings"),
|
||||
icon: const Icon(FluentIcons.settings),
|
||||
body: SettingsPage()
|
||||
)
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
|
||||
|
||||
@@ -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,19 +38,89 @@ 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);
|
||||
if(_gameController.updater == null) {
|
||||
_startUpdater();
|
||||
_setupBuildWarning();
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _setupBuildWarning() {
|
||||
_buildController.cancelledDownload
|
||||
.listen((value) => value ? _onCancelWarning() : {});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
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 {
|
||||
@@ -64,12 +146,39 @@ 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(
|
||||
Widget build(BuildContext context) => StreamBuilder<bool>(
|
||||
stream: _gameController.updater!.stream,
|
||||
builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
|
||||
);
|
||||
|
||||
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 get _updateScreen => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
@@ -82,46 +191,27 @@ class _LauncherPageState extends State<LauncherPage> {
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(snapshot.hasError)
|
||||
_createUpdateError(snapshot),
|
||||
UsernameBox(),
|
||||
const VersionSelector(),
|
||||
GameTypeSelector(),
|
||||
const LaunchButton()
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
|
||||
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"),
|
||||
severity: InfoBarSeverity.info
|
||||
title: Text("The Reboot dll wasn't downloaded: disable your antivirus or proxy and click here to try again"
|
||||
),
|
||||
severity: InfoBarSeverity.info
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,17 @@ class ServerPage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if(_serverController.warning.value)
|
||||
SizedBox(
|
||||
GestureDetector(
|
||||
onTap: () => _serverController.warning.value = false,
|
||||
child: const MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: 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
|
||||
title: Text("The backend server handles authentication and parties, not game hosting"),
|
||||
severity: InfoBarSeverity.info
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
HostInput(),
|
||||
|
||||
@@ -1,65 +1,82 @@
|
||||
|
||||
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(
|
||||
Widget build(BuildContext context) =>
|
||||
_settingsController.advancedMode.value ? _advancedSettings : _easySettings;
|
||||
|
||||
Widget get _advancedSettings => 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),
|
||||
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,
|
||||
),
|
||||
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),
|
||||
const Text("Made by Auties00"),
|
||||
const SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Tooltip(
|
||||
_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",
|
||||
@@ -70,18 +87,35 @@ class SettingsPage extends StatelessWidget {
|
||||
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")
|
||||
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'}");
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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<int> downloadRebootDll(int? lastUpdateMs) async {
|
||||
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 lastUpdateMs!;
|
||||
if (lastUpdate != null &&
|
||||
now.difference(lastUpdate).inHours <= 24 &&
|
||||
await oldRebootDll.exists()) {
|
||||
return RebootDownload(lastUpdateMs!);
|
||||
}
|
||||
|
||||
var response = await http.get(Uri.parse(_rebootUrl));
|
||||
var tempZip = File("${tempDirectory.path}/reboot.zip");
|
||||
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
||||
var tempZip = await loadBinary("reboot.zip", true);
|
||||
await tempZip.writeAsBytes(response.bodyBytes);
|
||||
|
||||
var outputDir = await tempDirectory.createTemp("reboot");
|
||||
var outputDir = await safeBinariesDirectory.createTemp("reboot_out");
|
||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||
|
||||
var rebootDll = File(
|
||||
outputDir.listSync()
|
||||
var rebootDll = File(outputDir
|
||||
.listSync()
|
||||
.firstWhere((element) => path.extension(element.path) == ".dll")
|
||||
.path
|
||||
);
|
||||
.path);
|
||||
|
||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
if (!exists ||
|
||||
sha1.convert(await oldRebootDll.readAsBytes()) !=
|
||||
sha1.convert(await rebootDll.readAsBytes())) {
|
||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||
}
|
||||
|
||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||
outputDir.delete(recursive: true);
|
||||
return now.millisecondsSinceEpoch;
|
||||
return RebootDownload(now.millisecondsSinceEpoch);
|
||||
} catch (error, stackTrace) {
|
||||
return RebootDownload(-1, error, stackTrace);
|
||||
} finally {
|
||||
try {
|
||||
outputDir?.delete(recursive: true);
|
||||
tempZip?.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||
return lastUpdateMs != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
||||
: null;
|
||||
}
|
||||
@@ -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)
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
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,20 +124,17 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
||||
);
|
||||
}
|
||||
|
||||
if(type == ServerType.embedded || type == ServerType.remote){
|
||||
var free = await isLawinPortFree();
|
||||
if (!free) {
|
||||
if(!needsFreePort) {
|
||||
if(type != ServerType.local && !(await isLawinPortFree())){
|
||||
return ServerResult(
|
||||
type: ServerResultType.alreadyStarted
|
||||
type: ServerResultType.backendPortTakenError
|
||||
);
|
||||
}
|
||||
|
||||
if(type == ServerType.embedded && !(await isMatchmakerPortFree())){
|
||||
return ServerResult(
|
||||
type: ServerResultType.portTakenError
|
||||
type: ServerResultType.backendPortTakenError
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ServerResult(
|
||||
type: ServerResultType.canStart
|
||||
@@ -151,7 +159,8 @@ enum ServerResultType {
|
||||
missingPortError,
|
||||
illegalPortError,
|
||||
cannotPingServer,
|
||||
portTakenError,
|
||||
backendPortTakenError,
|
||||
matchmakerPortTakenError,
|
||||
canStart,
|
||||
alreadyStarted,
|
||||
unknownError,
|
||||
|
||||
@@ -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,7 +14,11 @@ class GameTypeSelector extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: "The type of Fortnite instance to launch",
|
||||
child: InfoLabel(
|
||||
child: _createAdvancedSelector(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _createAdvancedSelector() => InfoLabel(
|
||||
label: "Type",
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -20,14 +26,12 @@ class GameTypeSelector extends StatelessWidget {
|
||||
leading: Text(_gameController.type.value.name),
|
||||
items: GameType.values
|
||||
.map((type) => _createItem(type))
|
||||
.toList()))
|
||||
),
|
||||
),
|
||||
.toList())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createItem(GameType type) {
|
||||
return MenuFlyoutItem(
|
||||
MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
|
||||
text: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Tooltip(
|
||||
@@ -40,5 +44,4 @@ class GameTypeSelector extends StatelessWidget {
|
||||
_gameController.started.value = _gameController.currentGameInstance != null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +169,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _askToStartMatchMakingServer();
|
||||
if(result != true){
|
||||
return;
|
||||
}
|
||||
|
||||
var version = _gameController.selectedVersionObs.value!;
|
||||
await _startGameProcesses(
|
||||
version,
|
||||
GameType.headlessServer
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _askToStartMatchMakingServer() async {
|
||||
if(_settingsController.doNotAskAgain()) {
|
||||
return _settingsController.automaticallyStartMatchmaker();
|
||||
}
|
||||
|
||||
var controller = CheckboxController();
|
||||
var result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
context: appKey.currentContext!,
|
||||
builder: (context) =>
|
||||
ContentDialog(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -195,18 +224,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
)
|
||||
],
|
||||
)
|
||||
) ?? false;
|
||||
);
|
||||
_settingsController.doNotAskAgain.value = controller.value;
|
||||
|
||||
if(!result){
|
||||
return;
|
||||
if(result != null){
|
||||
_settingsController.automaticallyStartMatchmaker.value = result;
|
||||
}
|
||||
|
||||
var version = _gameController.selectedVersionObs.value!;
|
||||
_startGameProcesses(
|
||||
version,
|
||||
GameType.headlessServer
|
||||
);
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Future<Process> _createGameProcess(String gamePath, GameType type) async {
|
||||
@@ -231,7 +255,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
var launcherProcess = await Process.start(launcherFile.path, []);
|
||||
Win32Process(launcherProcess.pid).suspend();
|
||||
suspend(launcherProcess.pid);
|
||||
return launcherProcess;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,14 +384,10 @@ class _LaunchButtonState extends State<LaunchButton> {
|
||||
}
|
||||
|
||||
try {
|
||||
var dllPath = await _getDllPath(injectable);
|
||||
if(!dllPath.existsSync()) {
|
||||
await _downloadMissingDll(injectable);
|
||||
if(!dllPath.existsSync()){
|
||||
_onDllFail(dllPath, type);
|
||||
var dllPath = await _getDllPath(injectable, type);
|
||||
if(dllPath == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await injectDll(gameProcess.pid, dllPath.path);
|
||||
} catch (exception) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,41 +61,61 @@ 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()))
|
||||
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) {
|
||||
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||
return _gameController.hasNoVersions ? [_createDefaultVersionItem()]
|
||||
: _gameController.versions.value
|
||||
.map((version) => _createVersionItem(context, version))
|
||||
.toList();
|
||||
}
|
||||
|
||||
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) {
|
||||
return MenuFlyoutItem(
|
||||
text: Listener(
|
||||
text: _createOptionsMenu(
|
||||
version: version,
|
||||
close: true,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(version.name)
|
||||
),
|
||||
),
|
||||
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;
|
||||
}
|
||||
|
||||
await _openMenu(context, version, event.position);
|
||||
if(version == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _openMenu(context, version, event.position, close);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(version.name)
|
||||
),
|
||||
),
|
||||
onPressed: () => _gameController.selectedVersion = version);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if(close) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
await _openRenameDialog(context, version);
|
||||
break;
|
||||
|
||||
@@ -155,7 +179,9 @@ class _VersionSelectorState extends State<VersionSelector> {
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
)),
|
||||
),
|
||||
|
||||
66
lib/src/widget/setting/url_updater.dart
Normal 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)
|
||||
))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'}",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||