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]
|
[XMPP]
|
||||||
bEnableWebsockets=false
|
bEnableWebsockets=false
|
||||||
|
|
||||||
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
|
[/Script/Engine.InputSettings]
|
||||||
[OnlineSubsystemMcp.Xmpp Prod]
|
ConsoleKey=F8
|
||||||
bUseSSL=false
|
|
||||||
ServerAddr="ws://127.0.0.1"
|
|
||||||
ServerPort=80
|
|
||||||
@@ -9,12 +9,12 @@ bUploadAthenaStats=false
|
|||||||
bUploadAthenaStatsV2=false
|
bUploadAthenaStatsV2=false
|
||||||
|
|
||||||
[/Script/FortniteGame.FortMatchmakingV2]
|
[/Script/FortniteGame.FortMatchmakingV2]
|
||||||
bCustomKeyEnabled=false
|
bCustomKeyEnabled=true
|
||||||
|
|
||||||
[/Script/FortniteGame.FortChatManager]
|
[/Script/FortniteGame.FortChatManager]
|
||||||
bShouldRequestGeneralChatRooms=false
|
bShouldRequestGeneralChatRooms=false
|
||||||
bShouldJoinGlobalChat=false
|
bShouldJoinGlobalChat=false
|
||||||
bShouldJoinFounderChat=false
|
bShouldJoinFoaunderChat=false
|
||||||
bIsAthenaGlobalChatEnabled=false
|
bIsAthenaGlobalChatEnabled=false
|
||||||
|
|
||||||
[/Script/FortniteGame.FortGameInstance]
|
[/Script/FortniteGame.FortGameInstance]
|
||||||
@@ -26,3 +26,6 @@ bBattleRoyaleMatchmakingEnabled=true
|
|||||||
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=6))
|
+FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=true, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=0, bDisplayAsLimitedTime=false, DisplayPriority=6))
|
||||||
+FrontEndPlaylistData=(PlaylistName=Playlist_PlaygroundV2, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=false, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=2, bDisplayAsLimitedTime=false, DisplayPriority=16))
|
+FrontEndPlaylistData=(PlaylistName=Playlist_PlaygroundV2, PlaylistAccess=(bEnabled=true, bIsDefaultPlaylist=false, bVisibleWhenDisabled=false, bDisplayAsNew=false, CategoryIndex=2, bDisplayAsLimitedTime=false, DisplayPriority=16))
|
||||||
+FrontEndPlaylistData=(PlaylistName=Playlist_Campaign, PlaylistAccess=(bEnabled=true, bInvisibleWhenEnabled=true))
|
+FrontEndPlaylistData=(PlaylistName=Playlist_Campaign, PlaylistAccess=(bEnabled=true, bInvisibleWhenEnabled=true))
|
||||||
|
|
||||||
|
[/Script/Engine.InputSettings]
|
||||||
|
ConsoleKey=F8
|
||||||
@@ -15,4 +15,7 @@ bIsOutOfSeasonMode=true
|
|||||||
+DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden)
|
+DisabledTabsForOutOfSeason=(TabName="CareerScreen",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||||
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden)
|
+DisabledTabsForOutOfSeason=(TabName="AthenaDirectAcquisition",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||||
+DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden)
|
+DisabledTabsForOutOfSeason=(TabName="BattlePass",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||||
+DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden)
|
+DisabledTabsForOutOfSeason=(TabName="AthenaCustomize",TabState=EFortRuntimeOptionTabState::Hidden)
|
||||||
|
|
||||||
|
[/Script/Engine.InputSettings]
|
||||||
|
ConsoleKey=F8
|
||||||
BIN
assets/images/auties.png
Normal file
|
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"]) {
|
if(result["update"]) {
|
||||||
stdout.writeln("Updating reboot dll...");
|
stdout.writeln("Updating reboot dll...");
|
||||||
try {
|
try {
|
||||||
await downloadRebootDll(0);
|
await downloadRebootDll(rebootDownloadUrl, 0);
|
||||||
}catch(error){
|
}catch(error){
|
||||||
stderr.writeln("Cannot update reboot dll: $error");
|
stderr.writeln("Cannot update reboot dll: $error");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import 'package:window_manager/window_manager.dart';
|
|||||||
final GlobalKey appKey = GlobalKey();
|
final GlobalKey appKey = GlobalKey();
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await Directory(safeBinariesDirectory)
|
await safeBinariesDirectory.create(recursive: true);
|
||||||
.create(recursive: true);
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
await GetStorage.init("game");
|
await GetStorage.init("game");
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Future<Map<String, dynamic>> getControllerJson(String name) async {
|
|||||||
throw Exception("Missing documents folder");
|
throw Exception("Missing documents folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
var file = File("$folder/$name.gs");
|
var file = File("$folder\\$name.gs");
|
||||||
if(!file.existsSync()){
|
if(!file.existsSync()){
|
||||||
return HashMap();
|
return HashMap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:process_run/shell.dart';
|
import 'package:process_run/shell.dart';
|
||||||
import 'package:reboot_launcher/cli.dart';
|
import 'package:reboot_launcher/cli.dart';
|
||||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
|
||||||
|
|
||||||
import '../model/fortnite_version.dart';
|
import '../model/fortnite_version.dart';
|
||||||
import '../model/game_type.dart';
|
import '../model/game_type.dart';
|
||||||
import '../util/injector.dart';
|
import '../util/injector.dart';
|
||||||
import '../util/os.dart';
|
import '../util/os.dart';
|
||||||
|
import '../util/process.dart';
|
||||||
import '../util/server.dart';
|
import '../util/server.dart';
|
||||||
|
|
||||||
final List<String> _errorStrings = [
|
final List<String> _errorStrings = [
|
||||||
@@ -41,7 +41,6 @@ Future<void> startGame() async {
|
|||||||
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
|
_gameProcess = await Process.start(gamePath, createRebootArgs(username!, type))
|
||||||
..exitCode.then((_) => _onClose())
|
..exitCode.then((_) => _onClose())
|
||||||
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
|
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
|
||||||
_injectOrShowError("craniumv2.dll");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
|
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
|
||||||
Win32Process(_launcherProcess!.pid).suspend();
|
suspend(_launcherProcess!.pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
||||||
@@ -60,7 +59,7 @@ Future<void> _startEacProcess(FortniteVersion dummyVersion) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
_eacProcess = await Process.start(dummyVersion.eacExecutable!.path, []);
|
||||||
Win32Process(_eacProcess!.pid).suspend();
|
suspend(_eacProcess!.pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
||||||
@@ -85,18 +84,14 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(line.contains("Region ")){
|
if(line.contains("Region ")){
|
||||||
_injectRequiredDLLs(hosting, dll);
|
if(hosting) {
|
||||||
}
|
_injectOrShowError(dll, false);
|
||||||
}
|
}else {
|
||||||
|
_injectOrShowError("console.dll");
|
||||||
|
}
|
||||||
|
|
||||||
void _injectRequiredDLLs(bool host, String rebootDll) {
|
_injectOrShowError("leakv2.dll");
|
||||||
if(host) {
|
|
||||||
_injectOrShowError(rebootDll, false);
|
|
||||||
}else {
|
|
||||||
_injectOrShowError("console.dll");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_injectOrShowError("leakv2.dll");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _kill() {
|
void _kill() {
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ Future<void> downloadRequiredDLLs() async {
|
|||||||
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
var tempZip = File("${tempDirectory.path}/reboot_config.zip");
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
await tempZip.writeAsBytes(response.bodyBytes);
|
||||||
|
|
||||||
await extractFileToDisk(tempZip.path, "$safeBinariesDirectory\\backend\\cli");
|
await extractFileToDisk(tempZip.path, "${safeBinariesDirectory.path}\\cli");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
@@ -20,7 +21,9 @@ class GameController extends GetxController {
|
|||||||
late final HashMap<GameType, GameInstance> gameInstancesMap;
|
late final HashMap<GameType, GameInstance> gameInstancesMap;
|
||||||
late final RxBool started;
|
late final RxBool started;
|
||||||
late bool updated;
|
late bool updated;
|
||||||
Future? updater;
|
late bool error;
|
||||||
|
late bool failing;
|
||||||
|
StreamController<bool>? updater;
|
||||||
|
|
||||||
GameController() {
|
GameController() {
|
||||||
_storage = GetStorage("game");
|
_storage = GetStorage("game");
|
||||||
@@ -52,6 +55,10 @@ class GameController extends GetxController {
|
|||||||
started = RxBool(false);
|
started = RxBool(false);
|
||||||
|
|
||||||
updated = false;
|
updated = false;
|
||||||
|
|
||||||
|
error = false;
|
||||||
|
|
||||||
|
failing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _readUsername() {
|
String _readUsername() {
|
||||||
|
|||||||
@@ -29,18 +29,10 @@ class ServerController extends GetxController {
|
|||||||
host.text = _readHost();
|
host.text = _readHost();
|
||||||
port.text = _readPort();
|
port.text = _readPort();
|
||||||
_storage.write("type", value.index);
|
_storage.write("type", value.index);
|
||||||
|
|
||||||
if(!started.value) {
|
if(!started.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(value == ServerType.remote){
|
|
||||||
remoteServer?.close(force: true);
|
|
||||||
remoteServer = null;
|
|
||||||
started.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,6 +68,7 @@ class ServerController extends GetxController {
|
|||||||
break;
|
break;
|
||||||
case ServerType.remote:
|
case ServerType.remote:
|
||||||
await remoteServer?.close(force: true);
|
await remoteServer?.close(force: true);
|
||||||
|
remoteServer = null;
|
||||||
break;
|
break;
|
||||||
case ServerType.local:
|
case ServerType.local:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -5,15 +5,21 @@ import 'package:reboot_launcher/src/model/tutorial_page.dart';
|
|||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import '../util/reboot.dart';
|
||||||
|
|
||||||
class SettingsController extends GetxController {
|
class SettingsController extends GetxController {
|
||||||
late final GetStorage _storage;
|
late final GetStorage _storage;
|
||||||
late final String originalDll;
|
late final String originalDll;
|
||||||
|
late final TextEditingController updateUrl;
|
||||||
late final TextEditingController rebootDll;
|
late final TextEditingController rebootDll;
|
||||||
late final TextEditingController consoleDll;
|
late final TextEditingController consoleDll;
|
||||||
late final TextEditingController authDll;
|
late final TextEditingController authDll;
|
||||||
late final TextEditingController matchmakingIp;
|
late final TextEditingController matchmakingIp;
|
||||||
late final Rx<PaneDisplayMode> displayType;
|
late final Rx<PaneDisplayMode> displayType;
|
||||||
|
late final RxBool automaticallyStartMatchmaker;
|
||||||
late final RxBool doNotAskAgain;
|
late final RxBool doNotAskAgain;
|
||||||
|
late final RxBool advancedMode;
|
||||||
|
late final RxBool autoUpdate;
|
||||||
late Rx<TutorialPage> tutorialPage;
|
late Rx<TutorialPage> tutorialPage;
|
||||||
late double width;
|
late double width;
|
||||||
late double height;
|
late double height;
|
||||||
@@ -24,15 +30,24 @@ class SettingsController extends GetxController {
|
|||||||
SettingsController() {
|
SettingsController() {
|
||||||
_storage = GetStorage("settings");
|
_storage = GetStorage("settings");
|
||||||
|
|
||||||
|
updateUrl = TextEditingController(text: _storage.read("update_url") ?? rebootDownloadUrl);
|
||||||
|
updateUrl.addListener(() => _storage.write("update_url", updateUrl.text));
|
||||||
|
|
||||||
rebootDll = _createController("reboot", "reboot.dll");
|
rebootDll = _createController("reboot", "reboot.dll");
|
||||||
|
|
||||||
consoleDll = _createController("console", "console.dll");
|
consoleDll = _createController("console", "console.dll");
|
||||||
|
|
||||||
authDll = _createController("cranium2", "craniumv2.dll");
|
authDll = _createController("cranium2", "craniumv2.dll");
|
||||||
|
|
||||||
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
|
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
|
||||||
matchmakingIp.addListener(() async {
|
matchmakingIp.addListener(() async {
|
||||||
var text = matchmakingIp.text;
|
var text = matchmakingIp.text;
|
||||||
_storage.write("ip", text);
|
_storage.write("ip", text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
automaticallyStartMatchmaker = RxBool(_storage.read("start_matchmaker_automatically") ?? false);
|
||||||
|
automaticallyStartMatchmaker.listen((value) => _storage.write("start_matchmaker_automatically", value));
|
||||||
|
|
||||||
doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false);
|
doNotAskAgain = RxBool(_storage.read("do_not_ask_again") ?? false);
|
||||||
doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value));
|
doNotAskAgain.listen((value) => _storage.write("do_not_ask_again", value));
|
||||||
|
|
||||||
@@ -40,6 +55,12 @@ class SettingsController extends GetxController {
|
|||||||
height = _storage.read("height") ?? window.physicalSize.height;
|
height = _storage.read("height") ?? window.physicalSize.height;
|
||||||
offsetX = _storage.read("offset_x");
|
offsetX = _storage.read("offset_x");
|
||||||
offsetY = _storage.read("offset_y");
|
offsetY = _storage.read("offset_y");
|
||||||
|
|
||||||
|
advancedMode = RxBool(_storage.read("advanced") ?? false);
|
||||||
|
advancedMode.listen((value) async => _storage.write("advanced", value));
|
||||||
|
autoUpdate = RxBool(_storage.read("auto_update") ?? false);
|
||||||
|
autoUpdate.listen((value) async => _storage.write("auto_update", value));
|
||||||
|
|
||||||
displayType = Rx(PaneDisplayMode.top);
|
displayType = Rx(PaneDisplayMode.top);
|
||||||
|
|
||||||
scrollingDistance = 0.0;
|
scrollingDistance = 0.0;
|
||||||
@@ -50,7 +71,7 @@ class SettingsController extends GetxController {
|
|||||||
TextEditingController _createController(String key, String name) {
|
TextEditingController _createController(String key, String name) {
|
||||||
loadBinary(name, true);
|
loadBinary(name, true);
|
||||||
|
|
||||||
var controller = TextEditingController(text: _storage.read(key) ?? "$safeBinariesDirectory\\$name");
|
var controller = TextEditingController(text: _storage.read(key) ?? "${safeBinariesDirectory.path}\\$name");
|
||||||
controller.addListener(() => _storage.write(key, controller.text));
|
controller.addListener(() => _storage.write(key, controller.text));
|
||||||
|
|
||||||
return controller;
|
return controller;
|
||||||
|
|||||||
@@ -229,6 +229,16 @@ class ErrorDialog extends AbstractDialog {
|
|||||||
|
|
||||||
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
|
const ErrorDialog({super.key, required this.exception, required this.errorMessageBuilder, this.stackTrace});
|
||||||
|
|
||||||
|
static DialogButton createCopyErrorButton({required Object error, required StackTrace? stackTrace, required Function() onClick, ButtonType type = ButtonType.primary}) => DialogButton(
|
||||||
|
text: "Copy error",
|
||||||
|
type: type,
|
||||||
|
onTap: () async {
|
||||||
|
FlutterClipboard.controlC("An error occurred: $error\nStacktrace:\n $stackTrace");
|
||||||
|
showMessage("Copied error to clipboard");
|
||||||
|
onClick();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return InfoDialog(
|
return InfoDialog(
|
||||||
@@ -239,14 +249,10 @@ class ErrorDialog extends AbstractDialog {
|
|||||||
),
|
),
|
||||||
|
|
||||||
if(stackTrace != null)
|
if(stackTrace != null)
|
||||||
DialogButton(
|
createCopyErrorButton(
|
||||||
text: "Copy error",
|
error: exception,
|
||||||
type: ButtonType.primary,
|
stackTrace: stackTrace,
|
||||||
onTap: () async {
|
onClick: () => Navigator.pop(context)
|
||||||
FlutterClipboard.controlC("An error occurred: $exception\nStacktrace:\n $stackTrace.toString");
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
showMessage("Copied error to clipboard");
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:reboot_launcher/main.dart';
|
|
||||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/model/fortnite_version.dart';
|
||||||
|
|
||||||
|
import '../../main.dart';
|
||||||
|
|
||||||
|
const String _unsupportedServerError = "The build you are currently using is not supported by Reboot. "
|
||||||
|
"This means that you cannot currently host this version of the game. "
|
||||||
|
"For a list of supported versions, check #info in the Discord server. "
|
||||||
|
"If you are unsure which version works best, use build 7.40. "
|
||||||
|
"If you are a passionate programmer you can add support by opening a PR on Github. ";
|
||||||
|
|
||||||
|
const String _corruptedBuildError = "The build you are currently using is corrupted. "
|
||||||
|
"This means that some critical files are missing for the game to launch. "
|
||||||
|
"Download the build again from the launcher or, if it's not available there, from another source. "
|
||||||
|
"Occasionally some files might get corrupted if there isn't enough space on your drive.";
|
||||||
|
|
||||||
Future<void> showBrokenError() async {
|
Future<void> showBrokenError() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
@@ -26,8 +39,17 @@ Future<void> showTokenErrorFixable() async {
|
|||||||
builder: (context) => const InfoDialog(
|
builder: (context) => const InfoDialog(
|
||||||
text: "A token error occurred. "
|
text: "A token error occurred. "
|
||||||
"The backend server has been automatically restarted to fix the issue. "
|
"The backend server has been automatically restarted to fix the issue. "
|
||||||
"Relaunch your game to check if the issue has been automatically fixed. "
|
"The game has been restarted automatically. "
|
||||||
"Otherwise, open an issue on Discord."
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showTokenErrorCouldNotFix() async {
|
||||||
|
showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) => const InfoDialog(
|
||||||
|
text: "A token error occurred. "
|
||||||
|
"The game couldn't be recovered, open an issue on Discord."
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -42,4 +64,35 @@ Future<void> showTokenErrorUnfixable() async {
|
|||||||
"Otherwise, open an issue on Discord."
|
"Otherwise, open an issue on Discord."
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showCorruptedBuildError(bool server, [Object? error, StackTrace? stackTrace]) async {
|
||||||
|
if(error == null) {
|
||||||
|
showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: server ? _unsupportedServerError : _corruptedBuildError
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) => ErrorDialog(
|
||||||
|
exception: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
errorMessageBuilder: (exception) => _corruptedBuildError
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> showMissingBuildError(FortniteVersion version) async {
|
||||||
|
showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: "${version.location.path} no longer contains a Fortnite executable. "
|
||||||
|
"This probably means that you deleted it or move it somewhere else."
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -8,41 +8,49 @@ import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
|||||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||||
import 'package:reboot_launcher/src/embedded/server.dart';
|
import 'package:reboot_launcher/src/embedded/server.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
import 'package:reboot_launcher/src/model/server_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:sync/semaphore.dart';
|
import 'package:sync/semaphore.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../page/home_page.dart';
|
||||||
import '../util/server.dart';
|
import '../util/server.dart';
|
||||||
|
|
||||||
extension ServerControllerDialog on ServerController {
|
extension ServerControllerDialog on ServerController {
|
||||||
static Semaphore semaphore = Semaphore();
|
static Semaphore semaphore = Semaphore();
|
||||||
|
|
||||||
Future<bool> start({required bool required, required bool askPortKill, bool isRetry = false}) async {
|
Future<bool> restart() async {
|
||||||
|
await resetWinNat();
|
||||||
|
return (!started() || await stop()) && await toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> toggle() async {
|
||||||
try{
|
try{
|
||||||
semaphore.acquire();
|
semaphore.acquire();
|
||||||
if (type() == ServerType.local) {
|
if (type() == ServerType.local) {
|
||||||
return _pingSelfInteractive(required);
|
return _pingSelfInteractive();
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldStarted = started();
|
var result = await _toggle();
|
||||||
if(oldStarted && required){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
started.value = !started.value;
|
|
||||||
var result = await _startInternal(oldStarted, required, askPortKill, isRetry);
|
|
||||||
if(!result){
|
if(!result){
|
||||||
return false;
|
started.value = false;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _pingSelfInteractive(true);
|
var ping = await _pingSelfInteractive();
|
||||||
|
if(!ping){
|
||||||
|
started.value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}finally{
|
}finally{
|
||||||
semaphore.release();
|
semaphore.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _startInternal(bool oldStarted, bool required, bool askPortKill, bool isRetry) async {
|
Future<bool> _toggle([ServerResultType? lastResultType]) async {
|
||||||
if (oldStarted) {
|
if (started.value) {
|
||||||
var result = await stop();
|
var result = await stop();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
started.value = true;
|
started.value = true;
|
||||||
@@ -53,23 +61,23 @@ extension ServerControllerDialog on ServerController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var conditions = await checkServerPreconditions(host.text, port.text, type.value, !required);
|
started.value = true;
|
||||||
var result = conditions.type == ServerResultType.canStart ? await _startServer(required) : conditions;
|
var conditions = await checkServerPreconditions(host.text, port.text, type.value);
|
||||||
|
var result = conditions.type == ServerResultType.canStart ? await _startServer() : conditions;
|
||||||
if(result.type == ServerResultType.alreadyStarted) {
|
if(result.type == ServerResultType.alreadyStarted) {
|
||||||
started.value = false;
|
started.value = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var handled = await _handleResultType(oldStarted, required, isRetry, askPortKill, result);
|
var handled = await _handleResultType(result, lastResultType);
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
started.value = false;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ServerResult> _startServer(bool closeAutomatically) async {
|
Future<ServerResult> _startServer() async {
|
||||||
try{
|
try{
|
||||||
switch(type()){
|
switch(type()){
|
||||||
case ServerType.embedded:
|
case ServerType.embedded:
|
||||||
@@ -79,7 +87,7 @@ extension ServerControllerDialog on ServerController {
|
|||||||
embeddedMatchmaker = await startEmbeddedMatchmaker();
|
embeddedMatchmaker = await startEmbeddedMatchmaker();
|
||||||
break;
|
break;
|
||||||
case ServerType.remote:
|
case ServerType.remote:
|
||||||
var uriResult = await _pingRemoteInteractive(closeAutomatically);
|
var uriResult = await _pingRemoteInteractive();
|
||||||
if(uriResult == null){
|
if(uriResult == null){
|
||||||
return ServerResult(
|
return ServerResult(
|
||||||
type: ServerResultType.cannotPingServer
|
type: ServerResultType.cannotPingServer
|
||||||
@@ -104,8 +112,9 @@ extension ServerControllerDialog on ServerController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, bool askPortKill, ServerResult result) async {
|
Future<bool> _handleResultType(ServerResult result, ServerResultType? lastResultType) async {
|
||||||
switch (result.type) {
|
var newResultType = result.type;
|
||||||
|
switch (newResultType) {
|
||||||
case ServerResultType.missingHostError:
|
case ServerResultType.missingHostError:
|
||||||
_showMissingHostError();
|
_showMissingHostError();
|
||||||
return false;
|
return false;
|
||||||
@@ -117,33 +126,43 @@ extension ServerControllerDialog on ServerController {
|
|||||||
return false;
|
return false;
|
||||||
case ServerResultType.cannotPingServer:
|
case ServerResultType.cannotPingServer:
|
||||||
return false;
|
return false;
|
||||||
case ServerResultType.portTakenError:
|
case ServerResultType.backendPortTakenError:
|
||||||
if (isRetry) {
|
if (lastResultType == ServerResultType.backendPortTakenError) {
|
||||||
_showPortTakenError();
|
_showPortTakenError(3551);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(askPortKill) {
|
var result = await _showPortTakenDialog(3551);
|
||||||
var result = await _showPortTakenDialog();
|
if (!result) {
|
||||||
if (!result) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await freeLawinPort();
|
await freeLawinPort();
|
||||||
return _startInternal(oldStarted, onlyIfNeeded, askPortKill, true);
|
await stop();
|
||||||
|
return _toggle(newResultType);
|
||||||
|
case ServerResultType.matchmakerPortTakenError:
|
||||||
|
if (lastResultType == ServerResultType.matchmakerPortTakenError) {
|
||||||
|
_showPortTakenError(8080);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _showPortTakenDialog(8080);
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await freeMatchmakerPort();
|
||||||
|
await stop();
|
||||||
|
return _toggle(newResultType);
|
||||||
case ServerResultType.unknownError:
|
case ServerResultType.unknownError:
|
||||||
showDialog(
|
if(lastResultType == ServerResultType.unknownError) {
|
||||||
context: appKey.currentContext!,
|
_showUnknownError(result);
|
||||||
builder: (context) =>
|
return false;
|
||||||
ErrorDialog(
|
}
|
||||||
exception: result.error ?? Exception("Unknown error"),
|
|
||||||
stackTrace: result.stackTrace,
|
await resetWinNat();
|
||||||
errorMessageBuilder: (
|
await stop();
|
||||||
exception) => "Cannot start server: $exception"
|
return _toggle(newResultType);
|
||||||
)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
case ServerResultType.alreadyStarted:
|
case ServerResultType.alreadyStarted:
|
||||||
case ServerResultType.canStart:
|
case ServerResultType.canStart:
|
||||||
return true;
|
return true;
|
||||||
@@ -152,16 +171,15 @@ extension ServerControllerDialog on ServerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _pingSelfInteractive(bool closeAutomatically) async {
|
Future<bool> _pingSelfInteractive() async {
|
||||||
try {
|
try {
|
||||||
return await showDialog<bool>(
|
var resultFuture = compute(pingSelf, port.text)
|
||||||
|
.then((value) => value != null);
|
||||||
|
await showDialog<bool>(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
FutureBuilderDialog(
|
FutureBuilderDialog(
|
||||||
future: Future.wait([
|
future: _waitFutureOrTime(resultFuture),
|
||||||
compute(pingSelf, port.text),
|
|
||||||
Future.delayed(const Duration(seconds: 1))
|
|
||||||
]),
|
|
||||||
loadingMessage: "Pinging ${type().id} server...",
|
loadingMessage: "Pinging ${type().id} server...",
|
||||||
successfulBody: FutureBuilderDialog.ofMessage(
|
successfulBody: FutureBuilderDialog.ofMessage(
|
||||||
"The ${type().id} server works correctly"),
|
"The ${type().id} server works correctly"),
|
||||||
@@ -169,25 +187,23 @@ extension ServerControllerDialog on ServerController {
|
|||||||
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
"The ${type().id} server doesn't work. Check the backend tab for misconfigurations and try again."),
|
||||||
errorMessageBuilder: (
|
errorMessageBuilder: (
|
||||||
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
exception) => "An error occurred while pining the ${type().id} server: $exception",
|
||||||
closeAutomatically: closeAutomatically
|
closeAutomatically: true
|
||||||
)
|
)
|
||||||
) ?? false;
|
);
|
||||||
|
return await resultFuture;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri?> _pingRemoteInteractive(bool closeAutomatically) async {
|
Future<Uri?> _pingRemoteInteractive() async {
|
||||||
try {
|
try {
|
||||||
var mainFuture = ping(host.text, port.text);
|
var mainFuture = ping(host.text, port.text);
|
||||||
var result = await showDialog<bool>(
|
await showDialog<bool>(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
FutureBuilderDialog(
|
FutureBuilderDialog(
|
||||||
future: Future.wait([
|
future: _waitFutureOrTime(mainFuture.then((value) => value != null)),
|
||||||
mainFuture,
|
|
||||||
Future.delayed(const Duration(seconds: 1))
|
|
||||||
]),
|
|
||||||
loadingMessage: "Pinging remote server...",
|
loadingMessage: "Pinging remote server...",
|
||||||
successfulBody: FutureBuilderDialog.ofMessage(
|
successfulBody: FutureBuilderDialog.ofMessage(
|
||||||
"The server at ${host.text}:${port
|
"The server at ${host.text}:${port
|
||||||
@@ -195,32 +211,30 @@ extension ServerControllerDialog on ServerController {
|
|||||||
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
unsuccessfulBody: FutureBuilderDialog.ofMessage(
|
||||||
"The server at ${host.text}:${port
|
"The server at ${host.text}:${port
|
||||||
.text} doesn't work. Check the hostname and/or the port and try again."),
|
.text} doesn't work. Check the hostname and/or the port and try again."),
|
||||||
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception",
|
errorMessageBuilder: (exception) => "An error occurred while pining the server: $exception"
|
||||||
closeAutomatically: closeAutomatically
|
|
||||||
)
|
)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
return result ? await mainFuture : null;
|
return await mainFuture;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showPortTakenError() async {
|
Future<void> _showPortTakenError(int port) async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) => InfoDialog(
|
||||||
const InfoDialog(
|
text: "Port $port is already in use and the associating process cannot be killed. Kill it manually and try again.",
|
||||||
text: "Port 3551 is already in use and the associating process cannot be killed. Kill it manually and try again.",
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _showPortTakenDialog() async {
|
Future<bool> _showPortTakenDialog(int port) async {
|
||||||
return await showDialog<bool>(
|
return await showDialog<bool>(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
InfoDialog(
|
InfoDialog(
|
||||||
text: "Port 3551 is already in use, do you want to kill the associated process?",
|
text: "Port $port is already in use, do you want to kill the associated process?",
|
||||||
buttons: [
|
buttons: [
|
||||||
DialogButton(
|
DialogButton(
|
||||||
type: ButtonType.secondary,
|
type: ButtonType.secondary,
|
||||||
@@ -286,4 +300,24 @@ extension ServerControllerDialog on ServerController {
|
|||||||
void _showMissingHostError() {
|
void _showMissingHostError() {
|
||||||
showMessage("Missing the host name for backend server");
|
showMessage("Missing the host name for backend server");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object?> _showUnknownError(ServerResult result) {
|
||||||
|
return showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) =>
|
||||||
|
ErrorDialog(
|
||||||
|
exception: result.error ?? Exception("Unknown error"),
|
||||||
|
stackTrace: result.stackTrace,
|
||||||
|
errorMessageBuilder: (exception) => "Cannot start the backend: an unknown error occurred"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> _waitFutureOrTime(Future<bool> resultFuture) {
|
||||||
|
return Future.wait<bool>([
|
||||||
|
resultFuture,
|
||||||
|
Future.delayed(const Duration(seconds: 1))
|
||||||
|
.then((value) => true)
|
||||||
|
]).then((value) => value.reduce((f, s) => f && s));
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../page/home_page.dart';
|
||||||
|
|
||||||
void showMessage(String text){
|
void showMessage(String text){
|
||||||
showSnackbar(
|
showSnackbar(
|
||||||
|
|||||||
@@ -328,6 +328,6 @@ File _getProfileFile(Context context) {
|
|||||||
_profiles.createSync(recursive: true);
|
_profiles.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return File("${_profiles.path}\\ClientProfile-${parseSeasonBuild(context)}.json");
|
return File("${_profiles.path}\\ClientProfile.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,28 +4,19 @@ import 'dart:io';
|
|||||||
import 'package:jaguar/jaguar.dart';
|
import 'package:jaguar/jaguar.dart';
|
||||||
|
|
||||||
class EmbeddedErrorWriter extends ErrorWriter {
|
class EmbeddedErrorWriter extends ErrorWriter {
|
||||||
static const String _errorName = "errors.com.lawinserver.common.not_found";
|
static const String _errorName404 = "errors.com.lawinserver.common.not_found";
|
||||||
|
static const String _errorName500 = "errors.com.lawinserver.common.error";
|
||||||
static const String _errorCode = "1004";
|
static const String _errorCode = "1004";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<Response> make404(Context ctx) {
|
FutureOr<Response> make404(Context ctx) {
|
||||||
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
|
stdout.writeln("Unknown path: ${ctx.uri} with method ${ctx.method}");
|
||||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
ctx.response.headers.set('X-Epic-Error-Name', _errorName404);
|
||||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
statusCode: 204,
|
statusCode: 204,
|
||||||
{}
|
{
|
||||||
);
|
"errorCode": _errorName404,
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
|
|
||||||
ctx.response.headers.set('X-Epic-Error-Name', _errorName);
|
|
||||||
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
|
||||||
return Response(
|
|
||||||
statusCode: 500,
|
|
||||||
body: {
|
|
||||||
"errorCode": _errorName,
|
|
||||||
"errorMessage": "Sorry the resource you were trying to find could not be found",
|
"errorMessage": "Sorry the resource you were trying to find could not be found",
|
||||||
"numericErrorCode": _errorCode,
|
"numericErrorCode": _errorCode,
|
||||||
"originatingService": "any",
|
"originatingService": "any",
|
||||||
@@ -33,4 +24,20 @@ class EmbeddedErrorWriter extends ErrorWriter {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<Response> make500(Context ctx, Object error, [StackTrace? stack]) {
|
||||||
|
ctx.response.headers.set('X-Epic-Error-Name', _errorName500);
|
||||||
|
ctx.response.headers.set('X-Epic-Error-Code', _errorCode);
|
||||||
|
return Response.json(
|
||||||
|
statusCode: 500,
|
||||||
|
{
|
||||||
|
"errorCode": _errorName500,
|
||||||
|
"errorMessage": "Sorry the resource you were trying to find threw an error",
|
||||||
|
"numericErrorCode": _errorCode,
|
||||||
|
"originatingService": "any",
|
||||||
|
"intent": "prod"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,24 +14,11 @@ import "error.dart";
|
|||||||
import "lightswitch.dart";
|
import "lightswitch.dart";
|
||||||
import 'matchmaking.dart';
|
import 'matchmaking.dart';
|
||||||
|
|
||||||
bool _loggingCapabilities = false;
|
|
||||||
|
|
||||||
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
|
Future<Jaguar> startEmbeddedServer(String Function() ipQuery) async {
|
||||||
var server = _createServer(ipQuery);
|
var server = Jaguar(port: 3551, errorWriter: EmbeddedErrorWriter());
|
||||||
await server.serve(logRequests: true);
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Jaguar> startEmbeddedMatchmaker() async {
|
|
||||||
var server = _createMatchmaker();
|
|
||||||
server.serve(logRequests: true);
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
Jaguar _createServer(String Function() ipQuery) {
|
|
||||||
var server = Jaguar(address: "127.0.0.1", port: 3551, errorWriter: EmbeddedErrorWriter());
|
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
|
server.getJson("unknown", (context) => Response(body: "lawinserver"));
|
||||||
server.getJson("/fortnite/api/version", getVersion);
|
server.getJson("/fortnite/api/version", getVersion);
|
||||||
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
|
server.getJson("/fortnite/api/v2/versioncheck/*", hasUpdate);
|
||||||
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
|
server.getJson("/fortnite/api/v2/versioncheck*", hasUpdate);
|
||||||
@@ -105,23 +92,7 @@ Jaguar _createServer(String Function() ipQuery) {
|
|||||||
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
|
server.getJson("/fortnite/api/game/v2/privacy/account/:accountId", getPrivacy);
|
||||||
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
|
server.postJson("/fortnite/api/game/v2/privacy/account/:accountId", postPrivacy);
|
||||||
|
|
||||||
return server;
|
await server.serve(logRequests: true);
|
||||||
}
|
|
||||||
Jaguar _createMatchmaker(){
|
|
||||||
var server = Jaguar(address: "127.0.0.1", port: 8080);
|
|
||||||
WebSocket? ws;
|
|
||||||
server.wsStream(
|
|
||||||
"/",
|
|
||||||
(_, input) => ws = input,
|
|
||||||
after: [(_) => queueMatchmaking(ws!)]
|
|
||||||
);
|
|
||||||
return _addLoggingCapabilities(server);
|
|
||||||
}
|
|
||||||
|
|
||||||
Jaguar _addLoggingCapabilities(Jaguar server) {
|
|
||||||
if(_loggingCapabilities){
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
server.log.onRecord.listen((line) {
|
server.log.onRecord.listen((line) {
|
||||||
stdout.writeln(line);
|
stdout.writeln(line);
|
||||||
@@ -133,6 +104,17 @@ Jaguar _addLoggingCapabilities(Jaguar server) {
|
|||||||
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
|
serverLogFile.writeAsString("An error occurred at ${ctx.uri}: \n$exception\n$trace\n", mode: FileMode.append);
|
||||||
});
|
});
|
||||||
|
|
||||||
_loggingCapabilities = true;
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Jaguar> startEmbeddedMatchmaker() async {
|
||||||
|
var server = Jaguar(port: 8080);
|
||||||
|
WebSocket? ws;
|
||||||
|
server.wsStream(
|
||||||
|
"/",
|
||||||
|
(_, input) => ws = input,
|
||||||
|
after: [(_) => queueMatchmaking(ws!)]
|
||||||
|
);
|
||||||
|
await server.serve(logRequests: true);
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'package:path/path.dart' as path;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:jaguar/jaguar.dart';
|
import 'package:jaguar/jaguar.dart';
|
||||||
@@ -11,23 +11,16 @@ import '../util/os.dart';
|
|||||||
|
|
||||||
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
|
final Directory _settings = Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher\\backend\\settings");
|
||||||
|
|
||||||
const String _engineName = "DefaultEngine.ini";
|
List getStorageSettings(Context context) =>
|
||||||
final String _engineIni = loadEmbedded("config/$_engineName").readAsStringSync();
|
loadEmbeddedDirectory("config")
|
||||||
|
.listSync()
|
||||||
|
.map((e) => File(e.path))
|
||||||
|
.map(_getStorageSetting)
|
||||||
|
.toList();
|
||||||
|
|
||||||
const String _gameName = "DefaultGame.ini";
|
Map<String, Object> _getStorageSetting(File file){
|
||||||
final String _gameIni = loadEmbedded("config/$_gameName").readAsStringSync();
|
var name = path.basename(file.path);
|
||||||
|
var bytes = file.readAsBytesSync();
|
||||||
const String _runtimeName = "DefaultRuntimeOptions.ini";
|
|
||||||
final String _runtimeIni = loadEmbedded("config/$_runtimeName").readAsStringSync();
|
|
||||||
|
|
||||||
List<Map<String, Object>> getStorageSettings(Context context) => [
|
|
||||||
_getStorageSetting(_engineName, _engineIni),
|
|
||||||
_getStorageSetting(_gameName, _gameIni),
|
|
||||||
_getStorageSetting(_runtimeName, _runtimeIni)
|
|
||||||
];
|
|
||||||
|
|
||||||
Map<String, Object> _getStorageSetting(String name, String source){
|
|
||||||
var bytes = utf8.encode(source);
|
|
||||||
return {
|
return {
|
||||||
"uniqueFilename": name,
|
"uniqueFilename": name,
|
||||||
"filename": name,
|
"filename": name,
|
||||||
@@ -43,16 +36,8 @@ Map<String, Object> _getStorageSetting(String name, String source){
|
|||||||
}
|
}
|
||||||
|
|
||||||
Response getStorageSetting(Context context) {
|
Response getStorageSetting(Context context) {
|
||||||
switch(context.pathParams.get("file")){
|
var file = loadEmbedded("config\\${context.pathParams.get("file")}");
|
||||||
case _engineName:
|
return Response(body: file.readAsStringSync());
|
||||||
return Response(body: _engineIni);
|
|
||||||
case _gameName:
|
|
||||||
return Response(body: _gameIni);
|
|
||||||
case _runtimeName:
|
|
||||||
return Response(body: _runtimeIni);
|
|
||||||
default:
|
|
||||||
return Response();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Response getStorageFile(Context context) {
|
Response getStorageFile(Context context) {
|
||||||
@@ -107,5 +92,5 @@ File _getSettingsFile(Context context) {
|
|||||||
_settings.createSync(recursive: true);
|
_settings.createSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return File("${_settings.path}\\ClientSettings-${parseSeasonBuild(context)}.Sav");
|
return File("${_settings.path}\\ClientSettings.Sav");
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,10 @@ class GameInstance {
|
|||||||
final Process gameProcess;
|
final Process gameProcess;
|
||||||
final Process? launcherProcess;
|
final Process? launcherProcess;
|
||||||
final Process? eacProcess;
|
final Process? eacProcess;
|
||||||
|
bool tokenError;
|
||||||
|
|
||||||
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess);
|
GameInstance(this.gameProcess, this.launcherProcess, this.eacProcess)
|
||||||
|
: tokenError = false;
|
||||||
|
|
||||||
void kill() {
|
void kill() {
|
||||||
gameProcess.kill(ProcessSignal.sigabrt);
|
gameProcess.kill(ProcessSignal.sigabrt);
|
||||||
|
|||||||
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/controller/server_controller.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
import 'package:reboot_launcher/src/dialog/dialog_button.dart';
|
||||||
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
import 'package:reboot_launcher/src/page/settings_page.dart';
|
import 'package:reboot_launcher/src/page/settings_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
import 'package:reboot_launcher/src/page/launcher_page.dart';
|
||||||
import 'package:reboot_launcher/src/page/server_page.dart';
|
import 'package:reboot_launcher/src/page/server_page.dart';
|
||||||
@@ -17,6 +18,7 @@ import 'package:reboot_launcher/src/widget/os/window_buttons.dart';
|
|||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import '../controller/settings_controller.dart';
|
import '../controller/settings_controller.dart';
|
||||||
|
import '../model/server_type.dart';
|
||||||
import '../model/tutorial_page.dart';
|
import '../model/tutorial_page.dart';
|
||||||
import 'info_page.dart';
|
import 'info_page.dart';
|
||||||
|
|
||||||
@@ -32,7 +34,6 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
static const double _sectionSize = 100.0;
|
static const double _sectionSize = 100.0;
|
||||||
static const double _defaultPadding = 12.0;
|
static const double _defaultPadding = 12.0;
|
||||||
static const int _headerButtonCount = 3;
|
static const int _headerButtonCount = 3;
|
||||||
static const int _sectionButtonCount = 4;
|
|
||||||
|
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
@@ -51,20 +52,36 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
_searchController.addListener(() {
|
_searchController.addListener(_onSearch);
|
||||||
if (searchValue.isEmpty) {
|
_onEasyMode();
|
||||||
_searchItems.value = null;
|
_settingsController.advancedMode.listen((advanced) {
|
||||||
return;
|
_onEasyMode();
|
||||||
}
|
_index.value = _index.value + (advanced ? 1 : -1);
|
||||||
|
|
||||||
_searchItems.value = _allItems.whereType<PaneItem>()
|
|
||||||
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
|
||||||
.toList()
|
|
||||||
.cast<NavigationPaneItem>();
|
|
||||||
});
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSearch() {
|
||||||
|
if (searchValue.isEmpty) {
|
||||||
|
_searchItems.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchItems.value = _allItems.whereType<PaneItem>()
|
||||||
|
.where((item) => (item.title as Text).data!.toLowerCase().contains(searchValue.toLowerCase()))
|
||||||
|
.toList()
|
||||||
|
.cast<NavigationPaneItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEasyMode() {
|
||||||
|
if(_settingsController.advancedMode.value){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameController.type.value = GameType.client;
|
||||||
|
_serverController.type.value = ServerType.embedded;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
windowManager.removeListener(this);
|
windowManager.removeListener(this);
|
||||||
@@ -119,29 +136,38 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => NotificationListener<SizeChangedLayoutNotification>(
|
Widget build(BuildContext context) {
|
||||||
onNotification: (notification) => _calculateSize(),
|
return NotificationListener<SizeChangedLayoutNotification>(
|
||||||
child: SizeChangedLayoutNotifier(
|
onNotification: (notification) {
|
||||||
child: Obx(() => Stack(
|
return _calculateSize();
|
||||||
children: [
|
},
|
||||||
_createNavigationView(),
|
child: SizeChangedLayoutNotifier(
|
||||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
child: Obx(_getViewStack)
|
||||||
Align(
|
)
|
||||||
alignment: Alignment.topRight,
|
);
|
||||||
child: WindowTitleBar(focused: _focused())
|
}
|
||||||
),
|
|
||||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
|
||||||
_createTopDisplayGestures(),
|
|
||||||
if(_focused() && isWin11)
|
|
||||||
const WindowBorder()
|
|
||||||
])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
Padding _createTopDisplayGestures() => Padding(
|
Widget _getViewStack() {
|
||||||
padding: const EdgeInsets.only(
|
var view = _createNavigationView();
|
||||||
left: _sectionSize * _sectionButtonCount,
|
return Stack(
|
||||||
|
children: [
|
||||||
|
view,
|
||||||
|
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: WindowTitleBar(focused: _focused())
|
||||||
|
),
|
||||||
|
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||||
|
_createTopDisplayGestures(view.pane?.items.length ?? 0),
|
||||||
|
if(_focused() && isWin11)
|
||||||
|
const WindowBorder()
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Padding _createTopDisplayGestures(int size) => Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: _sectionSize * size,
|
||||||
right: _headerSize * _headerButtonCount,
|
right: _headerSize * _headerButtonCount,
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -205,11 +231,11 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderObjectWidget _createPage(Widget? body) {
|
Widget _createPage(Widget? body) {
|
||||||
if(_settingsController.displayType() == PaneDisplayMode.top){
|
if(_settingsController.displayType() == PaneDisplayMode.top){
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(_defaultPadding),
|
padding: const EdgeInsets.all(_defaultPadding),
|
||||||
child: body
|
child: body
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,10 +293,9 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
|
List<NavigationPaneItem> _createFooterItems() => searchValue.isNotEmpty ? [] : [
|
||||||
if(_settingsController.displayType() != PaneDisplayMode.top)
|
if(_settingsController.displayType() != PaneDisplayMode.top)
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Tutorial"),
|
title: const Text("Settings"),
|
||||||
icon: const Icon(FluentIcons.info),
|
icon: const Icon(FluentIcons.settings),
|
||||||
body: const InfoPage(),
|
body: SettingsPage()
|
||||||
onTap: _onTutorial
|
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -281,24 +306,25 @@ class _HomePageState extends State<HomePage> with WindowListener {
|
|||||||
body: const LauncherPage()
|
body: const LauncherPage()
|
||||||
),
|
),
|
||||||
|
|
||||||
PaneItem(
|
if(_settingsController.advancedMode.value)
|
||||||
title: const Text("Backend"),
|
PaneItem(
|
||||||
icon: const Icon(FluentIcons.server_enviroment),
|
title: const Text("Backend"),
|
||||||
body: ServerPage()
|
icon: const Icon(FluentIcons.server_enviroment),
|
||||||
),
|
body: ServerPage()
|
||||||
|
),
|
||||||
|
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Settings"),
|
title: const Text("Tutorial"),
|
||||||
icon: const Icon(FluentIcons.settings),
|
icon: const Icon(FluentIcons.info),
|
||||||
body: SettingsPage()
|
body: const InfoPage(),
|
||||||
|
onTap: _onTutorial
|
||||||
),
|
),
|
||||||
|
|
||||||
if(_settingsController.displayType() == PaneDisplayMode.top)
|
if(_settingsController.displayType() == PaneDisplayMode.top)
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Tutorial"),
|
title: const Text("Settings"),
|
||||||
icon: const Icon(FluentIcons.info),
|
icon: const Icon(FluentIcons.settings),
|
||||||
body: const InfoPage(),
|
body: SettingsPage()
|
||||||
onTap: _onTutorial
|
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -14,27 +14,25 @@ class InfoPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _InfoPageState extends State<InfoPage> {
|
class _InfoPageState extends State<InfoPage> {
|
||||||
final List<String> _elseTitles = [
|
final List<String> _elseTitles = [
|
||||||
"Open the settings tab",
|
|
||||||
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
|
|
||||||
"Open the home page",
|
"Open the home page",
|
||||||
|
"Type the ip address of the host, including the port if it's not 7777\n The complete address should follow the schema ip:port",
|
||||||
"Type your username if you haven't already",
|
"Type your username if you haven't already",
|
||||||
"Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button",
|
"Select the exact version that the host is using from the dropdown menu\n If necessary, install it using the download button",
|
||||||
"As you want to play, select client from the dropdown menu",
|
"As you want to play, select client from the dropdown menu",
|
||||||
"Click launch to open the game",
|
"Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
|
||||||
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
|
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
|
||||||
];
|
];
|
||||||
final List<String> _ownTitles = [
|
final List<String> _ownTitles = [
|
||||||
"Open the settings tab",
|
|
||||||
"Type 127.0.0.1 as the matchmaking host",
|
|
||||||
"Open the home page",
|
"Open the home page",
|
||||||
|
"Type 127.0.0.1 as the matchmaking host\n If you didn't know, 127.0.0.1 is the ip for your local machine",
|
||||||
"Type your username if you haven't already",
|
"Type your username if you haven't already",
|
||||||
"Select the version you want to host\n If necessary, install it using the download button",
|
"Select the version you want to host\n If necessary, install it using the download button\n Check the supported versions in #info in the Discord server\n Fortnite 7.40 is the best one to use usually",
|
||||||
"As you want to host, select Headless Server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead",
|
"As you want to host, select headless server from the dropdown menu\n If the headless server doesn't work for your version, use the normal server instead\n The difference between the two is that the first doesn't render a fortnite instance\n Both will not allow you to play, only to host\n You will see an infinite loading screen when using the normal server\n If you want to also play continue reading",
|
||||||
"Click launch to start the server and wait until the Reboot GUI shows up",
|
"Click launch to start the server and wait until the Reboot GUI shows up\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
|
||||||
"To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step",
|
"To allow your friends to join your server, follow the instructions on playit.gg\n If you are an advanced user, open port 7777 on your router\n Finally, share your playit ip or public IPv4 address with your friends\n If you just want to play by yourself, skip this step",
|
||||||
"When you want to start the game, click on the 'Start Bus Countdown' button",
|
"When you want to start the game, click on the 'Start Bus Countdown' button\n Before clicking that button, make all of your friends join\n This is because joining mid-game isn't allowed",
|
||||||
"If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window",
|
"If you also want to play, start a client by selecting Client from the dropdown menu\n Don't close or open again the launcher, use the same window\n Remember to keep both the headless server(or server) and client open\n If you want to close the client or server, simply switch between them using the menu\n The launcher will remember what instances you have opened",
|
||||||
"Click launch to open the game",
|
"Click launch to open the game\n If the game closes immediately, it means that the build you downloaded is corrupted\n The same is valid if an Unreal Crash window opens\n Download another and try again",
|
||||||
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
|
"Once you are in game, click PLAY to enter in-game\n If this doesn't work open the Fortnite console by clicking the button above tab\n If nothing happens, make sure that your keyboard locale is set to English\n Type 'open TYPE_THE_IP' without the quotes, for example: open 85.182.12.1"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:reboot_launcher/main.dart';
|
||||||
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
import 'package:reboot_launcher/src/controller/build_controller.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
import 'package:reboot_launcher/src/dialog/dialog.dart';
|
||||||
|
import 'package:reboot_launcher/src/model/reboot_download.dart';
|
||||||
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
|
||||||
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
|
||||||
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
import 'package:reboot_launcher/src/widget/home/username_box.dart';
|
||||||
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/shared/file_selector.dart';
|
||||||
|
|
||||||
|
import '../dialog/dialog_button.dart';
|
||||||
|
import '../model/server_type.dart';
|
||||||
|
import '../util/checks.dart';
|
||||||
import '../util/reboot.dart';
|
import '../util/reboot.dart';
|
||||||
|
import '../widget/shared/smart_input.dart';
|
||||||
|
import 'home_page.dart';
|
||||||
|
|
||||||
class LauncherPage extends StatefulWidget {
|
class LauncherPage extends StatefulWidget {
|
||||||
const LauncherPage(
|
const LauncherPage(
|
||||||
@@ -26,21 +38,91 @@ class LauncherPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _LauncherPageState extends State<LauncherPage> {
|
class _LauncherPageState extends State<LauncherPage> {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
|
final ServerController _serverController = Get.find<ServerController>();
|
||||||
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
final BuildController _buildController = Get.find<BuildController>();
|
final BuildController _buildController = Get.find<BuildController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
if(_gameController.updater == null){
|
if(_gameController.updater == null) {
|
||||||
_gameController.updater = compute(downloadRebootDll, _updateTime)
|
_startUpdater();
|
||||||
..then((value) => _updateTime = value)
|
_setupBuildWarning();
|
||||||
..then((value) => _gameController.updated = true);
|
|
||||||
_buildController.cancelledDownload
|
|
||||||
.listen((value) => value ? _onCancelWarning() : {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setupBuildWarning() {
|
||||||
|
_buildController.cancelledDownload
|
||||||
|
.listen((value) => value ? _onCancelWarning() : {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startUpdater() {
|
||||||
|
_gameController.updater = StreamController.broadcast();
|
||||||
|
downloadRebootDll(_settingsController.updateUrl.text, _updateTime)
|
||||||
|
..then((result) async {
|
||||||
|
if(!result.hasError){
|
||||||
|
_updateTime = result.updateTime;
|
||||||
|
_gameController.updated = true;
|
||||||
|
_gameController.failing = false;
|
||||||
|
_gameController.error = false;
|
||||||
|
_gameController.updater?.add(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_gameController.failing){
|
||||||
|
_gameController.updated = false;
|
||||||
|
_gameController.failing = false;
|
||||||
|
_gameController.error = true;
|
||||||
|
_gameController.updater?.add(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameController.failing = true;
|
||||||
|
showDialog(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) => InfoDialog(
|
||||||
|
text: "An error occurred while downloading the reboot dll: this usually means that your antivirus flagged it. "
|
||||||
|
"Do you want to add an exclusion to Windows Defender to fix the issue? "
|
||||||
|
"If you are using a different antivirus disable it manually as this won't work. ",
|
||||||
|
buttons: [
|
||||||
|
ErrorDialog.createCopyErrorButton(
|
||||||
|
error: result.error ?? Exception("Unknown error"),
|
||||||
|
stackTrace: result.stackTrace,
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
onClick: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_gameController.updated = false;
|
||||||
|
_gameController.failing = false;
|
||||||
|
_gameController.error = true;
|
||||||
|
_gameController.updater?.add(false);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
DialogButton(
|
||||||
|
text: "Add",
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onTap: () async {
|
||||||
|
Navigator.pop(context);
|
||||||
|
var binary = await loadBinary("antivirus.bat", true);
|
||||||
|
var result = await runElevated(binary.path, "");
|
||||||
|
if(!result) {
|
||||||
|
_gameController.failing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startUpdater();
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
..catchError((error, stackTrace) {
|
||||||
|
_gameController.error = true;
|
||||||
|
_gameController.updater?.add(false);
|
||||||
|
return RebootDownload(0, error, stackTrace);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int? get _updateTime {
|
int? get _updateTime {
|
||||||
var storage = GetStorage("update");
|
var storage = GetStorage("update");
|
||||||
return storage.read("last_update_v2");
|
return storage.read("last_update_v2");
|
||||||
@@ -64,63 +146,71 @@ class _LauncherPageState extends State<LauncherPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => StreamBuilder<bool>(
|
||||||
return FutureBuilder(
|
stream: _gameController.updater!.stream,
|
||||||
future: _gameController.updater ?? Future.value(true),
|
builder: (context, snapshot) => !_gameController.updated && !_gameController.error ? _updateScreen : _homeScreen
|
||||||
builder: (context, snapshot) {
|
);
|
||||||
if (!_gameController.updated && !snapshot.hasData && !snapshot.hasError) {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: const [
|
|
||||||
ProgressRing(),
|
|
||||||
SizedBox(height: 16.0),
|
|
||||||
Text("Updating Reboot DLL...")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
Widget get _homeScreen => Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if(snapshot.hasError)
|
if(_gameController.error)
|
||||||
_createUpdateError(snapshot),
|
_updateError,
|
||||||
UsernameBox(),
|
UsernameBox(),
|
||||||
const VersionSelector(),
|
Tooltip(
|
||||||
GameTypeSelector(),
|
message:
|
||||||
const LaunchButton()
|
"The hostname of the server that hosts the multiplayer matches",
|
||||||
],
|
child: Obx(() => SmartInput(
|
||||||
);
|
label: "Matchmaking Host",
|
||||||
}
|
placeholder:
|
||||||
);
|
"Type the hostname of the server that hosts the multiplayer matches",
|
||||||
}
|
controller: _settingsController.matchmakingIp,
|
||||||
|
validatorMode: AutovalidateMode.always,
|
||||||
|
validator: checkMatchmaking,
|
||||||
|
enabled: _serverController.type() == ServerType.embedded)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
const VersionSelector(),
|
||||||
|
if(_settingsController.advancedMode.value)
|
||||||
|
GameTypeSelector(),
|
||||||
|
const LaunchButton()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
|
Widget get _updateScreen => Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
ProgressRing(),
|
||||||
|
SizedBox(height: 16.0),
|
||||||
|
Text("Updating Reboot DLL...")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _updateError {
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog(
|
_gameController.updated = false;
|
||||||
context: context,
|
_gameController.failing = false;
|
||||||
builder: (context) => ErrorDialog(
|
_gameController.error = false;
|
||||||
exception: snapshot.error!,
|
_gameController.updater?.add(false);
|
||||||
stackTrace: snapshot.stackTrace!,
|
_startUpdater();
|
||||||
errorMessageBuilder: (exception) => "Cannot update Reboot dll: ${snapshot.error}"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: const SizedBox(
|
child: const SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: InfoBar(
|
child: InfoBar(
|
||||||
title: Text("Cannot update dll"),
|
title: Text("The Reboot dll wasn't downloaded: disable your antivirus or proxy and click here to try again"
|
||||||
|
),
|
||||||
severity: InfoBarSeverity.info
|
severity: InfoBarSeverity.info
|
||||||
),
|
)
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,17 @@ class ServerPage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if(_serverController.warning.value)
|
if(_serverController.warning.value)
|
||||||
SizedBox(
|
GestureDetector(
|
||||||
width: double.infinity,
|
onTap: () => _serverController.warning.value = false,
|
||||||
child: InfoBar(
|
child: const MouseRegion(
|
||||||
title: const Text("The backend server handles authentication and parties, not game hosting"),
|
cursor: SystemMouseCursors.click,
|
||||||
severity: InfoBarSeverity.info,
|
child: SizedBox(
|
||||||
onClose: () => _serverController.warning.value = false
|
width: double.infinity,
|
||||||
|
child: InfoBar(
|
||||||
|
title: Text("The backend server handles authentication and parties, not game hosting"),
|
||||||
|
severity: InfoBarSeverity.info
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
HostInput(),
|
HostInput(),
|
||||||
|
|||||||
@@ -1,87 +1,121 @@
|
|||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/server_controller.dart';
|
|
||||||
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
import 'package:reboot_launcher/src/model/server_type.dart';
|
|
||||||
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../util/checks.dart';
|
import '../util/checks.dart';
|
||||||
|
import '../widget/setting/url_updater.dart';
|
||||||
import '../widget/shared/file_selector.dart';
|
import '../widget/shared/file_selector.dart';
|
||||||
import '../widget/shared/smart_input.dart';
|
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
final ServerController _serverController = Get.find<ServerController>();
|
|
||||||
final SettingsController _settingsController = Get.find<SettingsController>();
|
final SettingsController _settingsController = Get.find<SettingsController>();
|
||||||
|
|
||||||
SettingsPage({Key? key}) : super(key: key);
|
SettingsPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) =>
|
||||||
return Column(
|
_settingsController.advancedMode.value ? _advancedSettings : _easySettings;
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Widget get _advancedSettings => Column(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Tooltip(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
message:
|
children: [
|
||||||
"The hostname of the server that hosts the multiplayer matches",
|
const RebootUpdaterInput(),
|
||||||
child: Obx(() => SmartInput(
|
_createFileSelector(),
|
||||||
label: "Matchmaking Host",
|
_createConsoleSelector(),
|
||||||
placeholder:
|
_createGameSelector(),
|
||||||
"Type the hostname of the server that hosts the multiplayer matches",
|
_createVersionInfo(),
|
||||||
controller: _settingsController.matchmakingIp,
|
_createAdvancedSwitch()
|
||||||
validatorMode: AutovalidateMode.always,
|
]
|
||||||
validator: checkMatchmaking,
|
);
|
||||||
enabled: _serverController.type() == ServerType.embedded))),
|
|
||||||
Tooltip(
|
Widget get _easySettings => SizedBox.expand(
|
||||||
message: "The dll that is injected when a server is launched",
|
child: Column(
|
||||||
child: FileSelector(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
label: "Reboot DLL",
|
children: [
|
||||||
placeholder: "Type the path to the reboot dll",
|
const CircleAvatar(
|
||||||
controller: _settingsController.rebootDll,
|
radius: 48,
|
||||||
windowTitle: "Select a dll",
|
backgroundImage: AssetImage("assets/images/auties.png")),
|
||||||
folder: false,
|
const SizedBox(
|
||||||
extension: "dll",
|
height: 16.0,
|
||||||
validator: checkDll,
|
),
|
||||||
validatorMode: AutovalidateMode.always),
|
const Text("Made by Auties00"),
|
||||||
),
|
const SizedBox(
|
||||||
Tooltip(
|
height: 4.0,
|
||||||
message: "The dll that is injected when a client is launched",
|
),
|
||||||
child: FileSelector(
|
_versionText,
|
||||||
label: "Console DLL",
|
const SizedBox(
|
||||||
placeholder: "Type the path to the console dll",
|
height: 8.0,
|
||||||
controller: _settingsController.consoleDll,
|
),
|
||||||
windowTitle: "Select a dll",
|
Button(
|
||||||
folder: false,
|
child: const Text("Switch to advanced mode"),
|
||||||
extension: "dll",
|
onPressed: () => _settingsController.advancedMode.value = true
|
||||||
validator: checkDll,
|
)
|
||||||
validatorMode: AutovalidateMode.always),
|
],
|
||||||
),
|
),
|
||||||
Tooltip(
|
);
|
||||||
message: "The dll that is injected to make the game work",
|
|
||||||
child: FileSelector(
|
Widget _createAdvancedSwitch() => SmartSwitch(
|
||||||
label: "Cranium DLL",
|
label: "Advanced Mode",
|
||||||
placeholder:
|
value: _settingsController.advancedMode
|
||||||
"Type the path to the dll used for authentication",
|
);
|
||||||
controller: _settingsController.authDll,
|
|
||||||
windowTitle: "Select a dll",
|
Widget _createVersionInfo() => Column(
|
||||||
folder: false,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
extension: "dll",
|
children: [
|
||||||
validator: checkDll,
|
const Text("Version Status"),
|
||||||
validatorMode: AutovalidateMode.always)),
|
const SizedBox(height: 6.0),
|
||||||
Column(
|
Button(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: _versionText,
|
||||||
children: [
|
onPressed: () => launchUrl(safeBinariesDirectory.uri)
|
||||||
const Text("Version Status"),
|
)
|
||||||
const SizedBox(height: 6.0),
|
],
|
||||||
Button(
|
);
|
||||||
child: const Text("6.0${kDebugMode ? '-DEBUG' : '-RELEASE'}"),
|
|
||||||
onPressed: () => showMessage("What a nice launcher")
|
Widget _createGameSelector() => Tooltip(
|
||||||
)
|
message: "The dll that is injected to make the game work",
|
||||||
],
|
child: FileSelector(
|
||||||
)
|
label: "Cranium DLL",
|
||||||
]);
|
placeholder:
|
||||||
}
|
"Type the path to the dll used for authentication",
|
||||||
|
controller: _settingsController.authDll,
|
||||||
|
windowTitle: "Select a dll",
|
||||||
|
folder: false,
|
||||||
|
extension: "dll",
|
||||||
|
validator: checkDll,
|
||||||
|
validatorMode: AutovalidateMode.always
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _createConsoleSelector() => Tooltip(
|
||||||
|
message: "The dll that is injected when a client is launched",
|
||||||
|
child: FileSelector(
|
||||||
|
label: "Console DLL",
|
||||||
|
placeholder: "Type the path to the console dll",
|
||||||
|
controller: _settingsController.consoleDll,
|
||||||
|
windowTitle: "Select a dll",
|
||||||
|
folder: false,
|
||||||
|
extension: "dll",
|
||||||
|
validator: checkDll,
|
||||||
|
validatorMode: AutovalidateMode.always),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget _createFileSelector() => Tooltip(
|
||||||
|
message: "The dll that is injected when a server is launched",
|
||||||
|
child: FileSelector(
|
||||||
|
label: "Reboot DLL",
|
||||||
|
placeholder: "Type the path to the reboot dll",
|
||||||
|
controller: _settingsController.rebootDll,
|
||||||
|
windowTitle: "Select a dll",
|
||||||
|
folder: false,
|
||||||
|
extension: "dll",
|
||||||
|
validator: checkDll,
|
||||||
|
validatorMode: AutovalidateMode.always),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get _versionText => const Text("6.4${kDebugMode ? '-DEBUG' : '-RELEASE'}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,19 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
|||||||
|
|
||||||
Future<Process> downloadManifestBuild(
|
Future<Process> downloadManifestBuild(
|
||||||
String manifestUrl, String destination, Function(double, String) onProgress) async {
|
String manifestUrl, String destination, Function(double, String) onProgress) async {
|
||||||
var buildExe = await loadBinary("build.exe", false);
|
var log = await loadBinary("download.txt", true);
|
||||||
|
await log.create();
|
||||||
|
|
||||||
|
var buildExe = await loadBinary("build.exe", true);
|
||||||
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
var process = await Process.start(buildExe.path, [manifestUrl, destination]);
|
||||||
|
|
||||||
|
log.writeAsString("Starting download of: $manifestUrl\n", mode: FileMode.append);
|
||||||
process.errLines
|
process.errLines
|
||||||
.where((message) => message.contains("%"))
|
.where((message) => message.contains("%"))
|
||||||
.forEach((message) => onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1)));
|
.forEach((message) {
|
||||||
|
log.writeAsString("$message\n", mode: FileMode.append);
|
||||||
|
onProgress(double.parse(message.split("%")[0]), message.substring(message.indexOf(" ") + 1));
|
||||||
|
});
|
||||||
|
|
||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
@@ -104,7 +111,7 @@ Future<void> downloadArchiveBuild(String archiveUrl, String destination,
|
|||||||
var shell = Shell(
|
var shell = Shell(
|
||||||
commandVerbose: false,
|
commandVerbose: false,
|
||||||
commentVerbose: false,
|
commentVerbose: false,
|
||||||
workingDirectory: safeBinariesDirectory
|
workingDirectory: safeBinariesDirectory.path
|
||||||
);
|
);
|
||||||
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
|
await shell.run("./winrar.exe x \"${tempFile.path}\" *.* \"${output.path}\"");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ String? checkVersion(String? text, List<FortniteVersion> versions) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? checkChangeVersion(String? text) {
|
||||||
|
if (text == null || text.isEmpty) {
|
||||||
|
return 'Empty version name';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String? checkGameFolder(text) {
|
String? checkGameFolder(text) {
|
||||||
if (text == null || text.isEmpty) {
|
if (text == null || text.isEmpty) {
|
||||||
return 'Empty game path';
|
return 'Empty game path';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../page/home_page.dart';
|
||||||
import '../dialog/dialog.dart';
|
import '../dialog/dialog.dart';
|
||||||
|
|
||||||
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
void onError(Object? exception, StackTrace? stackTrace, bool framework) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import 'package:win32/win32.dart';
|
|||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
const int appBarSize = 2;
|
const int appBarSize = 2;
|
||||||
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ bool get isWin11 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<File> loadBinary(String binary, bool safe) async{
|
Future<File> loadBinary(String binary, bool safe) async{
|
||||||
var safeBinary = File("$safeBinariesDirectory\\$binary");
|
var safeBinary = File("${safeBinariesDirectory.path}\\$binary");
|
||||||
if(await safeBinary.exists()){
|
if(await safeBinary.exists()){
|
||||||
return safeBinary;
|
return safeBinary;
|
||||||
}
|
}
|
||||||
@@ -35,36 +37,77 @@ Future<File> loadBinary(String binary, bool safe) async{
|
|||||||
return safeBinary;
|
return safeBinary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File _locateInternalBinary(String binary) =>
|
||||||
|
File("${internalAssetsDirectory.path}\\binaries\\$binary");
|
||||||
|
|
||||||
|
Future<void> resetWinNat() async {
|
||||||
|
var binary = await loadBinary("winnat.bat", true);
|
||||||
|
await runElevated(binary.path, "");
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> runElevated(String executable, String args) async {
|
Future<bool> runElevated(String executable, String args) async {
|
||||||
var shellInput = calloc<SHELLEXECUTEINFO>();
|
var shellInput = calloc<SHELLEXECUTEINFO>();
|
||||||
shellInput.ref.lpFile = executable.toNativeUtf16();
|
shellInput.ref.lpFile = executable.toNativeUtf16();
|
||||||
shellInput.ref.lpParameters = args.toNativeUtf16();
|
shellInput.ref.lpParameters = args.toNativeUtf16();
|
||||||
shellInput.ref.nShow = SW_SHOWDEFAULT;
|
shellInput.ref.nShow = SW_HIDE;
|
||||||
shellInput.ref.fMask = 0x00000040;
|
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
|
||||||
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
shellInput.ref.lpVerb = "runas".toNativeUtf16();
|
||||||
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
|
||||||
var shellResult = ShellExecuteEx(shellInput);
|
var shellResult = ShellExecuteEx(shellInput);
|
||||||
return shellResult == 1;
|
return shellResult == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
File _locateInternalBinary(String binary){
|
Directory get internalAssetsDirectory =>
|
||||||
return File("$internalBinariesDirectory\\$binary");
|
Directory("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets");
|
||||||
}
|
|
||||||
|
|
||||||
String get internalBinariesDirectory =>
|
|
||||||
"${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\binaries";
|
|
||||||
|
|
||||||
Directory get tempDirectory =>
|
Directory get tempDirectory =>
|
||||||
Directory("${Platform.environment["Temp"]}");
|
Directory("${Platform.environment["Temp"]}");
|
||||||
|
|
||||||
String get safeBinariesDirectory =>
|
Directory get safeBinariesDirectory =>
|
||||||
"${Platform.environment["UserProfile"]}\\.reboot_launcher";
|
Directory("${Platform.environment["UserProfile"]}\\.reboot_launcher");
|
||||||
|
|
||||||
|
Directory get embeddedBackendDirectory =>
|
||||||
|
Directory("${safeBinariesDirectory.path}\\backend");
|
||||||
|
|
||||||
File loadEmbedded(String file) {
|
File loadEmbedded(String file) {
|
||||||
var safeBinary = File("$safeBinariesDirectory\\backend\\cli\\$file");
|
var safeBinary = File("${embeddedBackendDirectory.path}\\$file");
|
||||||
if(safeBinary.existsSync()){
|
if(safeBinary.existsSync()){
|
||||||
return safeBinary;
|
return safeBinary;
|
||||||
}
|
}
|
||||||
|
|
||||||
return File("${File(Platform.resolvedExecutable).parent.path}\\data\\flutter_assets\\assets\\$file");
|
safeBinary.parent.createSync(recursive: true);
|
||||||
|
var internal = File("${internalAssetsDirectory.path}\\$file");
|
||||||
|
if(internal.existsSync()) {
|
||||||
|
internal.copySync(safeBinary.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory loadEmbeddedDirectory(String directory) {
|
||||||
|
var safeBinary = Directory("${embeddedBackendDirectory.path}\\$directory");
|
||||||
|
safeBinary.parent.createSync(recursive: true);
|
||||||
|
var internal = Directory("${internalAssetsDirectory.path}\\$directory");
|
||||||
|
_copyFolder(internal, safeBinary);
|
||||||
|
return safeBinary;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyFolder(Directory dir1, Directory dir2) {
|
||||||
|
if(!dir1.existsSync()){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dir2.existsSync()) {
|
||||||
|
dir2.createSync(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
dir1.listSync().forEach((element) {
|
||||||
|
var newPath = "${dir2.path}/${path.basename(element.path)}";
|
||||||
|
if (element is File) {
|
||||||
|
var newFile = File(newPath);
|
||||||
|
newFile.writeAsBytesSync(element.readAsBytesSync());
|
||||||
|
} else if (element is Directory) {
|
||||||
|
_copyFolder(element, Directory(newPath));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
34
lib/src/util/process.dart
Normal file
@@ -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:crypto/crypto.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:reboot_launcher/src/model/reboot_download.dart';
|
||||||
import 'package:reboot_launcher/src/util/os.dart';
|
import 'package:reboot_launcher/src/util/os.dart';
|
||||||
|
|
||||||
const _rebootUrl =
|
const String rebootDownloadUrl =
|
||||||
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
|
"https://nightly.link/Milxnor/Project-Reboot/workflows/msbuild/main/Release.zip";
|
||||||
|
|
||||||
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
Future<RebootDownload> downloadRebootDll(String url, int? lastUpdateMs) async {
|
||||||
return lastUpdateMs != null ? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs) : null;
|
Directory? outputDir;
|
||||||
|
File? tempZip;
|
||||||
|
try {
|
||||||
|
var now = DateTime.now();
|
||||||
|
var oldRebootDll = await loadBinary("reboot.dll", true);
|
||||||
|
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||||
|
var exists = await oldRebootDll.exists();
|
||||||
|
if (lastUpdate != null &&
|
||||||
|
now.difference(lastUpdate).inHours <= 24 &&
|
||||||
|
await oldRebootDll.exists()) {
|
||||||
|
return RebootDownload(lastUpdateMs!);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = await http.get(Uri.parse(rebootDownloadUrl));
|
||||||
|
var tempZip = await loadBinary("reboot.zip", true);
|
||||||
|
await tempZip.writeAsBytes(response.bodyBytes);
|
||||||
|
|
||||||
|
var outputDir = await safeBinariesDirectory.createTemp("reboot_out");
|
||||||
|
await extractFileToDisk(tempZip.path, outputDir.path);
|
||||||
|
|
||||||
|
var rebootDll = File(outputDir
|
||||||
|
.listSync()
|
||||||
|
.firstWhere((element) => path.extension(element.path) == ".dll")
|
||||||
|
.path);
|
||||||
|
|
||||||
|
if (!exists ||
|
||||||
|
sha1.convert(await oldRebootDll.readAsBytes()) !=
|
||||||
|
sha1.convert(await rebootDll.readAsBytes())) {
|
||||||
|
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
return RebootDownload(now.millisecondsSinceEpoch);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
return RebootDownload(-1, error, stackTrace);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
outputDir?.delete(recursive: true);
|
||||||
|
tempZip?.delete();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> downloadRebootDll(int? lastUpdateMs) async {
|
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
|
||||||
var now = DateTime.now();
|
return lastUpdateMs != null
|
||||||
var oldRebootDll = await loadBinary("reboot.dll", true);
|
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
|
||||||
var lastUpdate = await _getLastUpdate(lastUpdateMs);
|
: null;
|
||||||
var exists = await oldRebootDll.exists();
|
}
|
||||||
if(lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && await oldRebootDll.exists()){
|
|
||||||
return lastUpdateMs!;
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await http.get(Uri.parse(_rebootUrl));
|
|
||||||
var tempZip = File("${tempDirectory.path}/reboot.zip");
|
|
||||||
await tempZip.writeAsBytes(response.bodyBytes);
|
|
||||||
|
|
||||||
var outputDir = await tempDirectory.createTemp("reboot");
|
|
||||||
await extractFileToDisk(tempZip.path, outputDir.path);
|
|
||||||
|
|
||||||
var rebootDll = File(
|
|
||||||
outputDir.listSync()
|
|
||||||
.firstWhere((element) => path.extension(element.path) == ".dll")
|
|
||||||
.path
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await rebootDll.readAsBytes())) {
|
|
||||||
outputDir.delete(recursive: true);
|
|
||||||
return now.millisecondsSinceEpoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
await oldRebootDll.writeAsBytes(await rebootDll.readAsBytes());
|
|
||||||
outputDir.delete(recursive: true);
|
|
||||||
return now.millisecondsSinceEpoch;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,29 +8,40 @@ import 'package:reboot_launcher/src/util/os.dart';
|
|||||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
|
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
|
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
|
||||||
|
|
||||||
Future<bool> isLawinPortFree() async {
|
Future<bool> isLawinPortFree() async {
|
||||||
try {
|
return http.get(Uri.parse("http://127.0.0.1:3551/unknown"))
|
||||||
var portBat = await loadBinary("port.bat", true);
|
.timeout(const Duration(milliseconds: 500))
|
||||||
var process = await Process.run(portBat.path, []);
|
.then((value) => false)
|
||||||
return !process.outText.contains(" LISTENING ");
|
.onError((error, stackTrace) => true);
|
||||||
}catch(_){
|
}
|
||||||
return ServerSocket.bind("127.0.0.1", 3551)
|
|
||||||
.then((socket) => socket.close())
|
Future<bool> isMatchmakerPortFree() async {
|
||||||
.then((_) => true)
|
return HttpServer.bind("127.0.0.1", 8080)
|
||||||
.onError((error, _) => false);
|
.then((socket) => socket.close())
|
||||||
}
|
.then((_) => true)
|
||||||
|
.onError((error, _) => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> freeLawinPort() async {
|
Future<void> freeLawinPort() async {
|
||||||
var releaseBat = await loadBinary("release.bat", false);
|
var releaseBat = await loadBinary("kill_lawin_port.bat", false);
|
||||||
var result = await Process.run(releaseBat.path, []);
|
var result = await Process.run(releaseBat.path, []);
|
||||||
if(!result.outText.contains("Access is denied")){
|
if(result.exitCode == 1){
|
||||||
return;
|
await runElevated(releaseBat.path, "");
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await runElevated(releaseBat.path, "");
|
Future<void> freeMatchmakerPort() async {
|
||||||
|
var releaseBat = await loadBinary("kill_matchmaker_port.bat", false);
|
||||||
|
var result = await Process.run(releaseBat.path, []);
|
||||||
|
if(result.exitCode == 1){
|
||||||
|
await runElevated(releaseBat.path, "");
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> createRebootArgs(String username, GameType type) {
|
List<String> createRebootArgs(String username, GameType type) {
|
||||||
@@ -92,7 +103,7 @@ String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFir
|
|||||||
|
|
||||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||||
|
|
||||||
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type, bool needsFreePort) async {
|
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
|
||||||
host = host.trim();
|
host = host.trim();
|
||||||
if(host.isEmpty){
|
if(host.isEmpty){
|
||||||
return ServerResult(
|
return ServerResult(
|
||||||
@@ -113,19 +124,16 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(type == ServerType.embedded || type == ServerType.remote){
|
if(type != ServerType.local && !(await isLawinPortFree())){
|
||||||
var free = await isLawinPortFree();
|
return ServerResult(
|
||||||
if (!free) {
|
type: ServerResultType.backendPortTakenError
|
||||||
if(!needsFreePort) {
|
);
|
||||||
return ServerResult(
|
}
|
||||||
type: ServerResultType.alreadyStarted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ServerResult(
|
if(type == ServerType.embedded && !(await isMatchmakerPortFree())){
|
||||||
type: ServerResultType.portTakenError
|
return ServerResult(
|
||||||
);
|
type: ServerResultType.backendPortTakenError
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ServerResult(
|
return ServerResult(
|
||||||
@@ -151,7 +159,8 @@ enum ServerResultType {
|
|||||||
missingPortError,
|
missingPortError,
|
||||||
illegalPortError,
|
illegalPortError,
|
||||||
cannotPingServer,
|
cannotPingServer,
|
||||||
portTakenError,
|
backendPortTakenError,
|
||||||
|
matchmakerPortTakenError,
|
||||||
canStart,
|
canStart,
|
||||||
alreadyStarted,
|
alreadyStarted,
|
||||||
unknownError,
|
unknownError,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
import 'package:reboot_launcher/src/controller/game_controller.dart';
|
||||||
|
import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_type.dart';
|
import 'package:reboot_launcher/src/model/game_type.dart';
|
||||||
|
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
|
||||||
|
|
||||||
class GameTypeSelector extends StatelessWidget {
|
class GameTypeSelector extends StatelessWidget {
|
||||||
final GameController _gameController = Get.find<GameController>();
|
final GameController _gameController = Get.find<GameController>();
|
||||||
@@ -12,33 +14,34 @@ class GameTypeSelector extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: "The type of Fortnite instance to launch",
|
message: "The type of Fortnite instance to launch",
|
||||||
child: InfoLabel(
|
child: _createAdvancedSelector(),
|
||||||
label: "Type",
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Obx(() => DropDownButton(
|
|
||||||
leading: Text(_gameController.type.value.name),
|
|
||||||
items: GameType.values
|
|
||||||
.map((type) => _createItem(type))
|
|
||||||
.toList()))
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createItem(GameType type) {
|
Widget _createAdvancedSelector() => InfoLabel(
|
||||||
return MenuFlyoutItem(
|
label: "Type",
|
||||||
text: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Tooltip(
|
child: Obx(() => DropDownButton(
|
||||||
message: type.message,
|
leading: Text(_gameController.type.value.name),
|
||||||
child: Text(type.name)
|
items: GameType.values
|
||||||
)
|
.map((type) => _createItem(type))
|
||||||
),
|
.toList())
|
||||||
onPressed: () {
|
)
|
||||||
_gameController.type(type);
|
)
|
||||||
_gameController.started.value = _gameController.currentGameInstance != null;
|
);
|
||||||
}
|
|
||||||
);
|
MenuFlyoutItem _createItem(GameType type) => MenuFlyoutItem(
|
||||||
}
|
text: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Tooltip(
|
||||||
|
message: type.message,
|
||||||
|
child: Text(type.name)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
_gameController.type(type);
|
||||||
|
_gameController.started.value = _gameController.currentGameInstance != null;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import 'package:reboot_launcher/src/util/injector.dart';
|
|||||||
import 'package:reboot_launcher/src/util/patcher.dart';
|
import 'package:reboot_launcher/src/util/patcher.dart';
|
||||||
import 'package:reboot_launcher/src/util/reboot.dart';
|
import 'package:reboot_launcher/src/util/reboot.dart';
|
||||||
import 'package:reboot_launcher/src/util/server.dart';
|
import 'package:reboot_launcher/src/util/server.dart';
|
||||||
import 'package:win32_suspend_process/win32_suspend_process.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import 'package:reboot_launcher/src/../main.dart';
|
import 'package:reboot_launcher/src/../main.dart';
|
||||||
@@ -26,6 +25,8 @@ import 'package:reboot_launcher/src/controller/settings_controller.dart';
|
|||||||
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
import 'package:reboot_launcher/src/dialog/snackbar.dart';
|
||||||
import 'package:reboot_launcher/src/model/game_instance.dart';
|
import 'package:reboot_launcher/src/model/game_instance.dart';
|
||||||
|
|
||||||
|
import '../../page/home_page.dart';
|
||||||
|
import '../../util/process.dart';
|
||||||
import '../shared/smart_check_box.dart';
|
import '../shared/smart_check_box.dart';
|
||||||
|
|
||||||
class LaunchButton extends StatefulWidget {
|
class LaunchButton extends StatefulWidget {
|
||||||
@@ -39,6 +40,10 @@ class LaunchButton extends StatefulWidget {
|
|||||||
|
|
||||||
class _LaunchButtonState extends State<LaunchButton> {
|
class _LaunchButtonState extends State<LaunchButton> {
|
||||||
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
final String _shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
||||||
|
final List<String> _corruptedBuildErrors = [
|
||||||
|
"when 0 bytes remain",
|
||||||
|
"Pak chunk signature verification failed!"
|
||||||
|
];
|
||||||
final List<String> _errorStrings = [
|
final List<String> _errorStrings = [
|
||||||
"port 3551 failed: Connection refused",
|
"port 3551 failed: Connection refused",
|
||||||
"Unable to login to Fortnite servers",
|
"Unable to login to Fortnite servers",
|
||||||
@@ -69,7 +74,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
child: Obx(() => Tooltip(
|
child: Obx(() => Tooltip(
|
||||||
message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance",
|
message: _gameController.started() ? "Close the running Fortnite instance" : "Launch a new Fortnite instance",
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: _onPressed,
|
onPressed: () => _start(_gameController.type()),
|
||||||
child: Text(_gameController.started() ? "Close" : "Launch")
|
child: Text(_gameController.started() ? "Close" : "Launch")
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
@@ -77,9 +82,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPressed() async {
|
void _start(GameType type) async {
|
||||||
if (_gameController.started()) {
|
if (_gameController.started()) {
|
||||||
_onStop(_gameController.type());
|
_onStop(type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +92,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
if (_gameController.username.text.isEmpty) {
|
if (_gameController.username.text.isEmpty) {
|
||||||
if(_serverController.type() != ServerType.local){
|
if(_serverController.type() != ServerType.local){
|
||||||
showMessage("Missing username");
|
showMessage("Missing username");
|
||||||
_onStop(_gameController.type());
|
_onStop(type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,25 +101,31 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
if (_gameController.selectedVersionObs.value == null) {
|
if (_gameController.selectedVersionObs.value == null) {
|
||||||
showMessage("No version is selected");
|
showMessage("No version is selected");
|
||||||
_onStop(_gameController.type());
|
_onStop(type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var element in Injectable.values) {
|
||||||
|
if(await _getDllPath(element, type) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_fail = false;
|
||||||
await _resetLogFile();
|
await _resetLogFile();
|
||||||
|
|
||||||
var version = _gameController.selectedVersionObs.value!;
|
var version = _gameController.selectedVersionObs.value!;
|
||||||
var gamePath = version.executable?.path;
|
var gamePath = version.executable?.path;
|
||||||
if(gamePath == null){
|
if(gamePath == null){
|
||||||
_onError("${version.location.path} no longer contains a Fortnite executable, did you delete it?", null);
|
showMissingBuildError(version);
|
||||||
_onStop(_gameController.type());
|
_onStop(type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _serverController.start(required: true, askPortKill: false);
|
var result = _serverController.started() || await _serverController.toggle();
|
||||||
if(!result){
|
if(!result){
|
||||||
showMessage("Cannot launch the game as the backend didn't start up correctly");
|
_onStop(type);
|
||||||
_onStop(_gameController.type());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,15 +133,15 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
await compute(patchHeadless, version.executable!);
|
await compute(patchHeadless, version.executable!);
|
||||||
|
|
||||||
await _startMatchMakingServer();
|
await _startMatchMakingServer();
|
||||||
await _startGameProcesses(version, _gameController.type());
|
await _startGameProcesses(version, type);
|
||||||
|
|
||||||
if(_gameController.type() == GameType.headlessServer){
|
if(type == GameType.headlessServer){
|
||||||
await _showServerLaunchingWarning();
|
await _showServerLaunchingWarning();
|
||||||
}
|
}
|
||||||
} catch (exception, stacktrace) {
|
} catch (exception, stacktrace) {
|
||||||
_closeDialogIfOpen(false);
|
_closeDialogIfOpen(false);
|
||||||
_onError(exception, stacktrace);
|
showCorruptedBuildError(type != GameType.client, exception, stacktrace);
|
||||||
_onStop(_gameController.type());
|
_onStop(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +154,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _startMatchMakingServer() async {
|
Future<void> _startMatchMakingServer() async {
|
||||||
if(_gameController.type() != GameType.client || _settingsController.doNotAskAgain()){
|
if(_gameController.type() != GameType.client){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,57 +169,70 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var controller = CheckboxController();
|
var result = await _askToStartMatchMakingServer();
|
||||||
var result = await showDialog<bool>(
|
if(result != true){
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Text(
|
|
||||||
"The matchmaking ip is set to the local machine, but no server is running. "
|
|
||||||
"If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12.0),
|
|
||||||
|
|
||||||
SmartCheckBox(
|
|
||||||
controller: controller,
|
|
||||||
content: const Text("Don't ask again")
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
Button(
|
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
|
||||||
child: const Text('Ignore'),
|
|
||||||
),
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
|
||||||
child: const Text('Start a server'),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
) ?? false;
|
|
||||||
_settingsController.doNotAskAgain.value = controller.value;
|
|
||||||
|
|
||||||
if(!result){
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = _gameController.selectedVersionObs.value!;
|
var version = _gameController.selectedVersionObs.value!;
|
||||||
_startGameProcesses(
|
await _startGameProcesses(
|
||||||
version,
|
version,
|
||||||
GameType.headlessServer
|
GameType.headlessServer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _askToStartMatchMakingServer() async {
|
||||||
|
if(_settingsController.doNotAskAgain()) {
|
||||||
|
return _settingsController.automaticallyStartMatchmaker();
|
||||||
|
}
|
||||||
|
|
||||||
|
var controller = CheckboxController();
|
||||||
|
var result = await showDialog<bool>(
|
||||||
|
context: appKey.currentContext!,
|
||||||
|
builder: (context) =>
|
||||||
|
ContentDialog(
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Text(
|
||||||
|
"The matchmaking ip is set to the local machine, but no server is running. "
|
||||||
|
"If you want to start a match for your friends or just test out Reboot, you need to start a server, either now from this prompt or later manually.",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12.0),
|
||||||
|
|
||||||
|
SmartCheckBox(
|
||||||
|
controller: controller,
|
||||||
|
content: const Text("Don't ask again")
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Button(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Ignore'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('Start a server'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
_settingsController.doNotAskAgain.value = controller.value;
|
||||||
|
if(result != null){
|
||||||
|
_settingsController.automaticallyStartMatchmaker.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Process> _createGameProcess(String gamePath, GameType type) async {
|
Future<Process> _createGameProcess(String gamePath, GameType type) async {
|
||||||
var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type));
|
var gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, type));
|
||||||
gameProcess
|
gameProcess
|
||||||
@@ -219,20 +243,20 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _resetLogFile() async {
|
Future<void> _resetLogFile() async {
|
||||||
if(_logFile != null && await _logFile!.exists()){
|
if(_logFile != null && await _logFile!.exists()){
|
||||||
await _logFile!.delete();
|
await _logFile!.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
|
Future<Process?> _createLauncherProcess(FortniteVersion version) async {
|
||||||
var launcherFile = version.launcher;
|
var launcherFile = version.launcher;
|
||||||
if (launcherFile == null) {
|
if (launcherFile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var launcherProcess = await Process.start(launcherFile.path, []);
|
var launcherProcess = await Process.start(launcherFile.path, []);
|
||||||
Win32Process(launcherProcess.pid).suspend();
|
suspend(launcherProcess.pid);
|
||||||
return launcherProcess;
|
return launcherProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Process?> _createEacProcess(FortniteVersion version) async {
|
Future<Process?> _createEacProcess(FortniteVersion version) async {
|
||||||
@@ -242,7 +266,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var eacProcess = await Process.start(eacFile.path, []);
|
var eacProcess = await Process.start(eacFile.path, []);
|
||||||
Win32Process(eacProcess.pid).suspend();
|
suspend(eacProcess.pid);
|
||||||
return eacProcess;
|
return eacProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +292,8 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
var result = await showDialog<bool>(
|
var result = await showDialog<bool>(
|
||||||
context: appKey.currentContext!,
|
context: appKey.currentContext!,
|
||||||
builder: (context) => ProgressDialog(
|
builder: (context) => ProgressDialog(
|
||||||
text: "Launching headless server...",
|
text: "Launching headless server...",
|
||||||
onStop: () =>_onEnd(_gameController.type())
|
onStop: () =>_onEnd(_gameController.type())
|
||||||
)
|
)
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
|
||||||
@@ -290,6 +314,17 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(_corruptedBuildErrors.any((element) => line.contains(element))){
|
||||||
|
if(_fail){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fail = true;
|
||||||
|
showCorruptedBuildError(type != GameType.client);
|
||||||
|
_onStop(type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(_errorStrings.any((element) => line.contains(element))){
|
if(_errorStrings.any((element) => line.contains(element))){
|
||||||
if(_fail){
|
if(_fail){
|
||||||
return;
|
return;
|
||||||
@@ -297,7 +332,7 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
|
|
||||||
_fail = true;
|
_fail = true;
|
||||||
_closeDialogIfOpen(false);
|
_closeDialogIfOpen(false);
|
||||||
_showTokenError();
|
_showTokenError(type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,30 +345,28 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_injectOrShowError(Injectable.memoryFix, type);
|
_injectOrShowError(Injectable.memoryFix, type);
|
||||||
|
_gameController.currentGameInstance?.tokenError = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showTokenError() async {
|
Future<void> _showTokenError(GameType type) async {
|
||||||
if(_serverController.type() == ServerType.embedded) {
|
if(_serverController.type() != ServerType.embedded) {
|
||||||
showTokenErrorFixable();
|
|
||||||
await _serverController.start(
|
|
||||||
required: true,
|
|
||||||
askPortKill: false
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showTokenErrorUnfixable();
|
showTokenErrorUnfixable();
|
||||||
|
_gameController.currentGameInstance?.tokenError = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<Object?> _onError(Object exception, StackTrace? stackTrace) async {
|
var tokenError = _gameController.currentGameInstance?.tokenError;
|
||||||
return showDialog(
|
_gameController.currentGameInstance?.tokenError = true;
|
||||||
context: context,
|
await _serverController.restart();
|
||||||
builder: (context) => ErrorDialog(
|
if (tokenError == true) {
|
||||||
exception: exception,
|
showTokenErrorCouldNotFix();
|
||||||
stackTrace: stackTrace,
|
return;
|
||||||
errorMessageBuilder: (exception) => "Cannot launch fortnite: $exception"
|
}
|
||||||
)
|
|
||||||
);
|
showTokenErrorFixable();
|
||||||
|
_onStop(type);
|
||||||
|
_start(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStop(GameType type) {
|
void _onStop(GameType type) {
|
||||||
@@ -351,13 +384,9 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var dllPath = await _getDllPath(injectable);
|
var dllPath = await _getDllPath(injectable, type);
|
||||||
if(!dllPath.existsSync()) {
|
if(dllPath == null) {
|
||||||
await _downloadMissingDll(injectable);
|
return;
|
||||||
if(!dllPath.existsSync()){
|
|
||||||
_onDllFail(dllPath, type);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await injectDll(gameProcess.pid, dllPath.path);
|
await injectDll(gameProcess.pid, dllPath.path);
|
||||||
@@ -367,6 +396,34 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<File?> _getDllPath(Injectable injectable, GameType type) async {
|
||||||
|
Future<File> getPath(Injectable injectable) async {
|
||||||
|
switch(injectable){
|
||||||
|
case Injectable.reboot:
|
||||||
|
return File(_settingsController.rebootDll.text);
|
||||||
|
case Injectable.console:
|
||||||
|
return File(_settingsController.consoleDll.text);
|
||||||
|
case Injectable.cranium:
|
||||||
|
return File(_settingsController.authDll.text);
|
||||||
|
case Injectable.memoryFix:
|
||||||
|
return await loadBinary("leakv2.dll", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dllPath = await getPath(injectable);
|
||||||
|
if(dllPath.existsSync()) {
|
||||||
|
return dllPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _downloadMissingDll(injectable);
|
||||||
|
if(dllPath.existsSync()) {
|
||||||
|
return dllPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDllFail(dllPath, type);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void _onDllFail(File dllPath, GameType type) {
|
void _onDllFail(File dllPath, GameType type) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if(_fail){
|
if(_fail){
|
||||||
@@ -380,26 +437,13 @@ class _LaunchButtonState extends State<LaunchButton> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<File> _getDllPath(Injectable injectable) async {
|
|
||||||
switch(injectable){
|
|
||||||
case Injectable.reboot:
|
|
||||||
return File(_settingsController.rebootDll.text);
|
|
||||||
case Injectable.console:
|
|
||||||
return File(_settingsController.consoleDll.text);
|
|
||||||
case Injectable.cranium:
|
|
||||||
return File(_settingsController.authDll.text);
|
|
||||||
case Injectable.memoryFix:
|
|
||||||
return await loadBinary("leakv2.dll", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _downloadMissingDll(Injectable injectable) async {
|
Future<void> _downloadMissingDll(Injectable injectable) async {
|
||||||
if(injectable != Injectable.reboot){
|
if(injectable != Injectable.reboot){
|
||||||
await loadBinary("$injectable.dll", true);
|
await loadBinary("$injectable.dll", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await downloadRebootDll(0);
|
await downloadRebootDll(rebootDownloadUrl, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,42 +60,62 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
|
|
||||||
Widget _createSelector(BuildContext context) {
|
Widget _createSelector(BuildContext context) {
|
||||||
return Tooltip(
|
return Tooltip(
|
||||||
message: "The version of Fortnite to launch",
|
message: "The version of Fortnite to launch",
|
||||||
child: Obx(() => DropDownButton(
|
child: Obx(() => _createOptionsMenu(
|
||||||
leading: Text(_gameController.selectedVersionObs.value?.name ??
|
version: _gameController.selectedVersionObs(),
|
||||||
"Select a version"),
|
close: false,
|
||||||
items: _gameController.hasNoVersions
|
child: DropDownButton(
|
||||||
? [_createDefaultVersionItem()]
|
leading: Text(_gameController.selectedVersionObs.value?.name
|
||||||
: _gameController.versions.value
|
?? "Select a version"),
|
||||||
.map((version) => _createVersionItem(context, version))
|
items: _createSelectorItems(context)
|
||||||
.toList()))
|
)
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createVersionItem(
|
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
|
||||||
BuildContext context, FortniteVersion version) {
|
return _gameController.hasNoVersions ? [_createDefaultVersionItem()]
|
||||||
return MenuFlyoutItem(
|
: _gameController.versions.value
|
||||||
text: Listener(
|
.map((version) => _createVersionItem(context, version))
|
||||||
onPointerDown: (event) async {
|
.toList();
|
||||||
if (event.kind != PointerDeviceKind.mouse ||
|
}
|
||||||
event.buttons != kSecondaryMouseButton) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _openMenu(context, version, event.position);
|
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) {
|
||||||
},
|
return MenuFlyoutItem(
|
||||||
|
text: _createOptionsMenu(
|
||||||
|
version: version,
|
||||||
|
close: true,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Text(version.name)
|
child: Text(version.name)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () => _gameController.selectedVersion = version);
|
onPressed: () => _gameController.selectedVersion = version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _createOptionsMenu({required FortniteVersion? version, required bool close, required Widget child}) {
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) async {
|
||||||
|
if (event.kind != PointerDeviceKind.mouse ||
|
||||||
|
event.buttons != kSecondaryMouseButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(version == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _openMenu(context, version, event.position, close);
|
||||||
|
},
|
||||||
|
child: child
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuFlyoutItem _createDefaultVersionItem() {
|
MenuFlyoutItem _createDefaultVersionItem() {
|
||||||
return MenuFlyoutItem(
|
return MenuFlyoutItem(
|
||||||
text: const SizedBox(
|
text: const SizedBox(
|
||||||
width: double.infinity, child: Text("No versions available")),
|
width: double.infinity, child: Text("No versions available. Add it using the buttons on the right.")),
|
||||||
trailing: const Expanded(child: SizedBox()),
|
trailing: const Expanded(child: SizedBox()),
|
||||||
onPressed: () {});
|
onPressed: () {});
|
||||||
}
|
}
|
||||||
@@ -114,24 +134,25 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openMenu(
|
Future<void> _openMenu(
|
||||||
BuildContext context, FortniteVersion version, Offset offset) async {
|
BuildContext context, FortniteVersion version, Offset offset, bool close) async {
|
||||||
var result = await showMenu<ContextualOption>(
|
var controller = FlyoutController();
|
||||||
context: context,
|
var result = await controller.showFlyout(
|
||||||
offset: offset,
|
|
||||||
builder: (context) => MenuFlyout(
|
builder: (context) => MenuFlyout(
|
||||||
items: ContextualOption.values
|
items: ContextualOption.values
|
||||||
.map((entry) => _createOption(context, entry))
|
.map((entry) => _createOption(context, entry))
|
||||||
.toList()
|
.toList()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case ContextualOption.openExplorer:
|
case ContextualOption.openExplorer:
|
||||||
if(!mounted){
|
if(!mounted){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
if(close) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
launchUrl(version.location.uri)
|
launchUrl(version.location.uri)
|
||||||
.onError((error, stackTrace) => _onExplorerError());
|
.onError((error, stackTrace) => _onExplorerError());
|
||||||
break;
|
break;
|
||||||
@@ -141,7 +162,10 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
if(close) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
await _openRenameDialog(context, version);
|
await _openRenameDialog(context, version);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -155,7 +179,9 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
if(close) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
_gameController.removeVersion(version);
|
_gameController.removeVersion(version);
|
||||||
if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) {
|
if (_gameController.selectedVersionObs.value?.name == version.name || _gameController.hasNoVersions) {
|
||||||
@@ -242,7 +268,7 @@ class _VersionSelectorState extends State<VersionSelector> {
|
|||||||
header: "Name",
|
header: "Name",
|
||||||
placeholder: "Type the new version name",
|
placeholder: "Type the new version name",
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
validator: (text) => checkVersion(text, _gameController.versions.value)
|
validator: (text) => checkChangeVersion(text)
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PortInput extends StatelessWidget {
|
|||||||
label: "Port",
|
label: "Port",
|
||||||
placeholder: "Type the backend server's port",
|
placeholder: "Type the backend server's port",
|
||||||
controller: _serverController.port,
|
controller: _serverController.port,
|
||||||
enabled: _serverController.type.value != ServerType.embedded
|
enabled: _serverController.type.value == ServerType.remote
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ class _ServerButtonState extends State<ServerButton> {
|
|||||||
child: Obx(() => Tooltip(
|
child: Obx(() => Tooltip(
|
||||||
message: _helpMessage,
|
message: _helpMessage,
|
||||||
child: Button(
|
child: Button(
|
||||||
onPressed: () async => _serverController.start(
|
onPressed: () async => _serverController.toggle(),
|
||||||
required: false,
|
|
||||||
askPortKill: true
|
|
||||||
),
|
|
||||||
child: Text(_buttonText())),
|
child: Text(_buttonText())),
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
|
|||||||
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
|
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
if (widget.allowNavigator) const SizedBox(width: 8.0),
|
if (widget.allowNavigator) const SizedBox(width: 16.0),
|
||||||
if (widget.allowNavigator)
|
if (widget.allowNavigator)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Select a ${widget.folder ? 'folder' : 'file'}",
|
message: "Select a ${widget.folder ? 'folder' : 'file'}",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: reboot_launcher
|
name: reboot_launcher
|
||||||
description: Launcher for project reboot
|
description: Launcher for project reboot
|
||||||
version: "6.0.0"
|
version: "6.4.0"
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ dependencies:
|
|||||||
process_run: ^0.12.3+2
|
process_run: ^0.12.3+2
|
||||||
url_launcher: ^6.1.5
|
url_launcher: ^6.1.5
|
||||||
archive: ^3.3.1
|
archive: ^3.3.1
|
||||||
win32_suspend_process: ^1.0.0
|
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
crypto: ^3.0.2
|
crypto: ^3.0.2
|
||||||
async: ^2.8.2
|
async: ^2.8.2
|
||||||
@@ -42,9 +41,6 @@ dependencies:
|
|||||||
hex: ^0.2.0
|
hex: ^0.2.0
|
||||||
uuid: ^3.0.6
|
uuid: ^3.0.6
|
||||||
|
|
||||||
dependency_overrides:
|
|
||||||
win32: ^3.0.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -66,7 +62,7 @@ msix_config:
|
|||||||
display_name: Reboot Launcher
|
display_name: Reboot Launcher
|
||||||
publisher_display_name: Auties00
|
publisher_display_name: Auties00
|
||||||
identity_name: 31868Auties00.RebootLauncher
|
identity_name: 31868Auties00.RebootLauncher
|
||||||
msix_version: 6.0.0.0
|
msix_version: 6.4.0.0
|
||||||
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
|
||||||
logo_path: ./assets/icons/reboot.ico
|
logo_path: ./assets/icons/reboot.ico
|
||||||
architecture: x64
|
architecture: x64
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include <flutter/dart_project.h>
|
#include <flutter/dart_project.h>
|
||||||
#include <flutter/flutter_view_controller.h>
|
#include <flutter/flutter_view_controller.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -34,6 +36,7 @@ bool CheckOneInstance()
|
|||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
_putenv_s("OPENSSL_ia32cap", "~0x20000000");
|
||||||
if(!CheckOneInstance()){
|
if(!CheckOneInstance()){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||