ip for click to play

This commit is contained in:
Alessandro Autiero
2022-10-25 19:42:09 +02:00
parent 691cd53f26
commit ef7f34e0e3
26 changed files with 509 additions and 725 deletions

View File

@@ -1,102 +1,46 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:process_run/shell.dart';
import 'package:reboot_launcher/src/cli/compatibility.dart';
import 'package:reboot_launcher/src/cli/config.dart';
import 'package:reboot_launcher/src/cli/game.dart';
import 'package:reboot_launcher/src/cli/reboot.dart';
import 'package:reboot_launcher/src/cli/server.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart';
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:http/http.dart' as http;
// Needed because binaries can't be loaded in any other way
const String _craniumDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848323825675/cranium.dll";
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1031230848005046373/console.dll";
Process? _gameProcess;
Process? _eacProcess;
Process? _launcherProcess;
void main(List<String> args){
handleCLI(args);
}
Future<Map<String, dynamic>> _getControllerJson(String name) async {
var folder = await _getWindowsPath(FOLDERID_Documents);
if(folder == null){
throw Exception("Missing documents folder");
}
var file = File("$folder/$name.gs");
if(!file.existsSync()){
return HashMap();
}
return jsonDecode(file.readAsStringSync());
}
Future<String?> _getWindowsPath(String folderID) {
final Pointer<Pointer<Utf16>> pathPtrPtr = calloc<Pointer<Utf16>>();
final Pointer<GUID> knownFolderID = calloc<GUID>()..ref.setGUID(folderID);
try {
final int hr = SHGetKnownFolderPath(
knownFolderID,
KF_FLAG_DEFAULT,
NULL,
pathPtrPtr,
);
if (FAILED(hr)) {
if (hr == E_INVALIDARG || hr == E_FAIL) {
throw WindowsException(hr);
}
return Future<String?>.value();
}
final String path = pathPtrPtr.value.toDartString();
return Future<String>.value(path);
} finally {
calloc.free(pathPtrPtr);
calloc.free(knownFolderID);
}
}
Future<void> handleCLI(List<String> args) async {
stdout.writeln("Reboot Launcher");
stdout.writeln("Wrote by Auties00");
stdout.writeln("Version 3.13");
stdout.writeln("Version 4.4");
_killOld();
kill();
var gameJson = await _getControllerJson("game");
var serverJson = await _getControllerJson("server");
var settingsJson = await _getControllerJson("settings");
var versions = _getVersions(gameJson);
var gameJson = await getControllerJson("game");
var serverJson = await getControllerJson("server");
var settingsJson = await getControllerJson("settings");
var versions = getVersions(gameJson);
var parser = ArgParser()
..addCommand("list")
..addCommand("launch")
..addOption("version", defaultsTo: gameJson["version"])
..addOption("username")
..addOption("server-type", allowed: _getServerTypes(), defaultsTo: _getDefaultServerType(serverJson))
..addOption("server-type", allowed: getServerTypes(), defaultsTo: getDefaultServerType(serverJson))
..addOption("server-host")
..addOption("server-port")
..addOption("dll", defaultsTo: settingsJson["reboot"] ?? (await loadBinary("reboot.dll", true)).path)
..addOption("type", allowed: _getTypes(), defaultsTo: _getDefaultType(gameJson))
..addOption("type", allowed: getGameTypes(), defaultsTo: getDefaultGameType(gameJson))
..addFlag("update", defaultsTo: settingsJson["auto_update"] ?? true, negatable: true)
..addFlag("log", defaultsTo: false);
..addFlag("log", defaultsTo: false)
..addFlag("memory-fix", defaultsTo: false, negatable: true);
var result = parser.parse(args);
if (result.command?.name == "list") {
stdout.writeln("Versions list: ");
@@ -106,220 +50,44 @@ Future<void> handleCLI(List<String> args) async {
}
var dll = result["dll"];
var type = _getType(result);
var type = getGameType(result);
var username = result["username"];
username ??= gameJson["${type == GameType.client ? "game" : "server"}_username"];
var verbose = result["log"];
var dummyVersion = _createVersion(gameJson["version"], result["version"], versions);
await _updateDLLs();
var dummyVersion = _createVersion(gameJson["version"], result["version"], result["memory-fix"], versions);
await downloadRequiredDLLs();
if(result["update"]) {
stdout.writeln("Updating reboot dll...");
await downloadRebootDll(0);
}
stdout.writeln("Launching game(type: ${type.name})...");
await _startLauncherProcess(dummyVersion);
if (result["type"] == "headless_server") {
if(dummyVersion.executable == null){
throw Exception("Missing game executable at: ${dummyVersion.location.path}");
}
await patch(dummyVersion.executable!);
if (result["type"] == "headless_server") {
await patchHeadless(dummyVersion.executable!);
}else if(result["type"] == "client"){
await patchMatchmaking(dummyVersion.executable!);
}
var serverType = _getServerType(result);
var serverType = getServerType(result);
var host = result["server-host"] ?? serverJson["${serverType.id}_host"];
var port = result["server-port"] ?? serverJson["${serverType.id}_port"];
var started = await _startServerIfNeeded(host, port, serverType);
var started = await startServer(host, port, serverType);
if(!started){
stderr.writeln("Cannot start server!");
return;
}
await _startGameProcess(username, type, verbose, dll, dummyVersion);
await startGame(username, type, verbose, dll, dummyVersion);
}
void _killOld() async {
var shell = Shell(
commandVerbose: false,
commentVerbose: false,
verbose: false
);
try {
await shell.run("taskkill /f /im FortniteLauncher.exe");
await shell.run("taskkill /f /im FortniteClient-Win64-Shipping_EAC.exe");
}catch(_){
}
}
Iterable<String> _getTypes() => GameType.values.map((entry) => entry.id);
Iterable<String> _getServerTypes() => ServerType.values.map((entry) => entry.id);
String _getDefaultServerType(Map<String, dynamic> json) {
var type = ServerType.values.elementAt(json["type"] ?? 0);
return type.id;
}
GameType _getType(ArgResults result) {
var type = GameType.of(result["type"]);
if(type == null){
throw Exception("Unknown game type: $result. Use --type only with ${_getTypes().join(", ")}");
}
return type;
}
ServerType _getServerType(ArgResults result) {
var type = ServerType.of(result["server-type"]);
if(type == null){
throw Exception("Unknown server type: $result. Use --server-type only with ${_getServerTypes().join(", ")}");
}
return type;
}
String _getDefaultType(Map<String, dynamic> json){
var type = GameType.values.elementAt(json["type"] ?? 0);
switch(type){
case GameType.client:
return "client";
case GameType.server:
return "server";
case GameType.headlessServer:
return "headless_server";
}
}
Future<void> _updateDLLs() async {
stdout.writeln("Downloading necessary components...");
var consoleDll = await loadBinary("console.dll", true);
if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){
throw Exception("Cannot download console.dll");
}
await consoleDll.writeAsBytes(response.bodyBytes);
}
var craniumDll = await loadBinary("cranium.dll", true);
if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_craniumDownload));
if(response.statusCode != 200){
throw Exception("Cannot download cranium.dll");
}
await craniumDll.writeAsBytes(response.bodyBytes);
}
}
List<FortniteVersion> _getVersions(Map<String, dynamic> gameJson) {
Iterable iterable = jsonDecode(gameJson["versions"] ?? "[]");
return iterable.map((entry) => FortniteVersion.fromJson(entry))
.toList();
}
Future<void> _startGameProcess(String? username, GameType type, bool verbose, String dll, FortniteVersion version) async {
var gamePath = version.executable?.path;
if (gamePath == null) {
throw Exception("${version.location
.path} no longer contains a Fortnite executable. Did you delete it?");
}
var hosting = type != GameType.client;
if (username == null) {
username = "Reboot${hosting ? 'Host' : 'Player'}";
stdout.writeln("No username was specified, using $username by default. Use --username to specify one");
}
_gameProcess = await Process.start(gamePath, createRebootArgs(username, type == GameType.headlessServer))
..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, hosting, verbose));
await _injectOrShowError("cranium.dll");
}
void _onClose() {
_kill();
stdout.writeln("The game was closed");
exit(0);
}
Future<void> _startLauncherProcess(FortniteVersion dummyVersion) async {
if (dummyVersion.launcher == null) {
return;
}
_launcherProcess = await Process.start(dummyVersion.launcher!.path, []);
Win32Process(_launcherProcess!.pid).suspend();
}
Future<bool> _startServerIfNeeded(String? host, String? port, ServerType type) async {
stdout.writeln("Starting lawin server...");
switch(type){
case ServerType.local:
var result = await ping(host ?? "127.0.0.1", port ?? "3551");
if(result == null){
throw Exception("Local lawin server is not running");
}
stdout.writeln("Detected local lawin server");
return true;
case ServerType.embedded:
stdout.writeln("Starting an embedded server...");
return await _changeEmbeddedServerState();
case ServerType.remote:
if(host == null){
throw Exception("Missing host for remote server");
}
if(port == null){
throw Exception("Missing host for remote server");
}
stdout.writeln("Starting a reverse proxy to $host:$port");
return await _changeReverseProxyState(host, port) != null;
}
}
Future<bool> _changeEmbeddedServerState() async {
return true;
}
Future<HttpServer?> _changeReverseProxyState(String host, String port) async {
host = host.trim();
if(host.isEmpty){
throw Exception("Missing host name");
}
port = port.trim();
if(port.isEmpty){
throw Exception("Missing port");
}
if(int.tryParse(port) == null){
throw Exception("Invalid port, use only numbers");
}
try{
var uri = await ping(host, port);
if(uri == null){
return null;
}
return await shelf_io.serve(proxyHandler(uri), "127.0.0.1", 3551);
}catch(error){
throw Exception("Cannot start reverse proxy");
}
}
FortniteVersion _createVersion(String? versionName, String? versionPath, List<FortniteVersion> versions) {
FortniteVersion _createVersion(String? versionName, String? versionPath, bool memoryFix, List<FortniteVersion> versions) {
if (versionPath != null) {
return FortniteVersion(name: "dummy", location: Directory(versionPath));
return FortniteVersion(name: "dummy", location: Directory(versionPath), memoryFix: memoryFix);
}
if(versionName != null){
@@ -333,67 +101,3 @@ FortniteVersion _createVersion(String? versionName, String? versionPath, List<Fo
throw Exception(
"Specify a version using --version or open the launcher GUI and select it manually");
}
void _onGameOutput(String line, String rebootDll, bool host, bool verbose) {
if(verbose) {
stdout.writeln(line);
}
if (line.contains("FOnlineSubsystemGoogleCommon::Shutdown()")) {
_onClose();
return;
}
if(line.contains("port 3551 failed: Connection refused")){
stderr.writeln("Connection refused from lawin server");
_onClose();
return;
}
if(line.contains("HTTP 400 response from ") || line.contains("Unable to login to Fortnite servers")){
stderr.writeln("Connection refused from lawin server");
_onClose();
return;
}
if(line.contains("Network failure when attempting to check platform restrictions") || line.contains("UOnlineAccountCommon::ForceLogout")){
stderr.writeln("Expired token, please reopen the game");
_kill();
_onClose();
return;
}
if (line.contains("Game Engine Initialized") && !host) {
_injectOrShowError("console.dll");
return;
}
if(line.contains("Region") && host){
_injectOrShowError(rebootDll, false);
}
}
void _kill() {
_gameProcess?.kill(ProcessSignal.sigabrt);
_eacProcess?.kill(ProcessSignal.sigabrt);
_launcherProcess?.kill(ProcessSignal.sigabrt);
}
Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
if (_gameProcess == null) {
return;
}
try {
stdout.writeln("Injecting $binary...");
var dll = locate ? await loadBinary(binary, true) : File(binary);
if(!dll.existsSync()){
throw Exception("Cannot inject $dll: missing file");
}
await injectDll(_gameProcess!.pid, dll.path);
} catch (exception) {
throw Exception("Cannot inject binary: $binary");
}
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
@@ -12,6 +13,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/settings_controller.dart';
import 'package:reboot_launcher/src/page/home_page.dart';
import 'package:reboot_launcher/src/util/error.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:system_theme/system_theme.dart';
@@ -26,6 +28,7 @@ void main(List<String> args) async {
}
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
await GetStorage.init("game");
await GetStorage.init("server");
@@ -45,7 +48,10 @@ void main(List<String> args) async {
appWindow.show();
});
runApp(const RebootApplication());
runZonedGuarded(() =>
runApp(const RebootApplication()),
(error, stack) => onError(error, stack, false)
);
}
class RebootApplication extends StatefulWidget {

View File

@@ -3,8 +3,10 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/dialog/server_dialogs.dart';
import 'package:reboot_launcher/src/util/server.dart';
import '../dialog/snackbar.dart';
import '../model/server_type.dart';
class ServerController extends GetxController {
@@ -17,6 +19,7 @@ class ServerController extends GetxController {
late final Rx<ServerType> type;
late final RxBool warning;
late RxBool started;
late int embeddedServerCounter;
Process? embeddedServer;
HttpServer? reverseProxy;
@@ -36,7 +39,7 @@ class ServerController extends GetxController {
if(value == ServerType.remote){
reverseProxy?.close(force: true);
reverseProxy = null;
started(false);
started.value = false;
return;
}
@@ -53,6 +56,8 @@ class ServerController extends GetxController {
warning.listen((value) => _storage.write("lawin_value", value));
started = RxBool(false);
embeddedServerCounter = 0;
}
String _readHost() {
@@ -65,8 +70,9 @@ class ServerController extends GetxController {
return _storage.read("${type.value.id}_port") ?? _serverPort;
}
Future<ServerResult> start() async {
var result = await checkServerPreconditions(host.text, port.text, type.value);
Future<ServerResult> start(bool needsFreePort) async {
var lastCounter = ++embeddedServerCounter;
var result = await checkServerPreconditions(host.text, port.text, type.value, needsFreePort);
if(result.type != ServerResultType.canStart){
return result;
}
@@ -74,7 +80,16 @@ class ServerController extends GetxController {
try{
switch(type()){
case ServerType.embedded:
embeddedServer = await startEmbeddedServer();
await _startEmbeddedServer();
embeddedServer?.exitCode.then((value) async {
if (!started() || lastCounter != embeddedServerCounter) {
return;
}
started.value = false;
await freeLawinPort();
showUnexpectedError();
});
break;
case ServerType.remote:
var uriResult = await result.uri!;
@@ -97,21 +112,34 @@ class ServerController extends GetxController {
);
}
var myself = await pingSelf();
var myself = await pingSelf(port.text);
if(myself == null){
return ServerResult(
type: ServerResultType.cannotPingServer
type: ServerResultType.cannotPingServer,
pid: embeddedServer?.pid
);
}
started(true);
return ServerResult(
type: ServerResultType.started
);
}
Future<void> _startEmbeddedServer() async {
var result = await startEmbeddedServer();
if(result != null){
embeddedServer = result;
return;
}
showMessage("The server is corrupted, trying to fix it");
await serverLocation.parent.delete(recursive: true);
await downloadServerInteractive(true);
await _startEmbeddedServer();
}
Future<bool> stop() async {
started(false);
started.value = false;
try{
switch(type()){
case ServerType.embedded:
@@ -125,7 +153,7 @@ class ServerController extends GetxController {
}
return true;
}catch(_){
started(true);
started.value = true;
return false;
}
}

View File

@@ -1,13 +1,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/model/game_type.dart';
import 'package:ini/ini.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:system_theme/system_theme.dart';
import 'package:reboot_launcher/src/util/server.dart';
class SettingsController extends GetxController {
late final GetStorage _storage;
@@ -15,7 +12,7 @@ class SettingsController extends GetxController {
late final TextEditingController rebootDll;
late final TextEditingController consoleDll;
late final TextEditingController craniumDll;
late final RxBool autoUpdate;
late final TextEditingController matchmakingIp;
SettingsController() {
_storage = GetStorage("settings");
@@ -23,9 +20,23 @@ class SettingsController extends GetxController {
rebootDll = _createController("reboot", "reboot.dll");
consoleDll = _createController("console", "console.dll");
craniumDll = _createController("cranium", "cranium.dll");
autoUpdate = RxBool(_storage.read("auto_update") ?? true);
matchmakingIp = TextEditingController(text: _storage.read("ip") ?? "127.0.0.1");
matchmakingIp.addListener(() async {
var text = matchmakingIp.text;
_storage.write("ip", text);
if(await serverConfig.exists()){
var config = Config.fromString(await serverConfig.readAsString());
if(text.contains(":")){
config.set("GameServer", "ip", text.substring(0, text.indexOf(":")));
config.set("GameServer", "port", text.substring(text.indexOf(":") + 1));
}else {
config.set("GameServer", "ip", text);
config.set("GameServer", "port", "7777");
}
autoUpdate.listen((value) => _storage.write("auto_update", value));
serverConfig.writeAsString(config.toString());
}
});
}
TextEditingController _createController(String key, String name) {

View File

@@ -8,12 +8,14 @@ import 'package:reboot_launcher/src/dialog/dialog_button.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import '../util/checks.dart';
import '../widget/os/file_selector.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_check_box.dart';
class AddLocalVersion extends StatelessWidget {
final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _gamePathController = TextEditingController();
final CheckboxController _injectMemoryFixController = CheckboxController();
AddLocalVersion({Key? key})
: super(key: key);
@@ -47,6 +49,15 @@ class AddLocalVersion extends StatelessWidget {
folder: true
),
const SizedBox(
height: 16.0
),
SmartCheckBox(
controller: _injectMemoryFixController,
content: const Text("Inject memory leak fix")
),
const SizedBox(height: 8.0),
],
),
@@ -61,7 +72,9 @@ class AddLocalVersion extends StatelessWidget {
onTap: () {
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_gamePathController.text)));
location: Directory(_gamePathController.text),
memoryFix: _injectMemoryFixController.value
));
},
)
]

View File

@@ -15,7 +15,8 @@ import 'package:reboot_launcher/src/widget/home/version_name_input.dart';
import '../util/checks.dart';
import '../widget/home/build_selector.dart';
import '../widget/os/file_selector.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_check_box.dart';
import 'dialog.dart';
class AddServerVersion extends StatefulWidget {
@@ -30,6 +31,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
final BuildController _buildController = Get.find<BuildController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController();
final CheckboxController _injectMemoryFixController = CheckboxController();
late Future _future;
DownloadStatus _status = DownloadStatus.none;
String _timeLeft = "00:00:00";
@@ -97,7 +99,12 @@ class _AddServerVersionState extends State<AddServerVersion> {
];
case DownloadStatus.error:
return [DialogButton(type: ButtonType.only)];
return [
DialogButton(
type: ButtonType.only,
onTap: () => Navigator.of(context).pop(),
)
];
default:
return [
DialogButton(
@@ -154,7 +161,9 @@ class _AddServerVersionState extends State<AddServerVersion> {
_status = DownloadStatus.done;
_gameController.addVersion(FortniteVersion(
name: _nameController.text,
location: Directory(_pathController.text)));
location: Directory(_pathController.text),
memoryFix: _injectMemoryFixController.value
));
});
}
@@ -230,7 +239,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
padding: const EdgeInsets.only(bottom: 16),
child: SizedBox(
width: double.infinity,
child: Text("An error was occurred while downloading:$_error",
child: Text("An error occurred while downloading:$_error",
textAlign: TextAlign.center)),
);
}
@@ -269,7 +278,7 @@ class _AddServerVersionState extends State<AddServerVersion> {
const SizedBox(
height: 8,
),
if(_manifestDownloadProcess != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -277,16 +286,19 @@ class _AddServerVersionState extends State<AddServerVersion> {
"${_downloadProgress.round()}%",
style: FluentTheme.maybeOf(context)?.typography.body,
),
if(_manifestDownloadProcess != null)
Text(
"Time left: $_timeLeft",
style: FluentTheme.maybeOf(context)?.typography.body,
),
],
),
if(_manifestDownloadProcess != null)
const SizedBox(
height: 8,
),
SizedBox(
width: double.infinity,
child: ProgressBar(value: _downloadProgress.toDouble())),
@@ -313,7 +325,13 @@ class _AddServerVersionState extends State<AddServerVersion> {
windowTitle: "Select download destination",
controller: _pathController,
validator: checkDownloadDestination,
folder: true),
folder: true
),
const SizedBox(height: 16.0),
SmartCheckBox(
controller: _injectMemoryFixController,
content: const Text("Inject memory leak fix")
),
const SizedBox(height: 8.0),
],
);

View File

@@ -53,7 +53,7 @@ class FormDialog extends AbstractDialog {
}
DialogButton _createFormButton(DialogButton entry, BuildContext context) {
if (entry.type == ButtonType.secondary) {
if (entry.type != ButtonType.primary) {
return entry;
}
@@ -102,8 +102,9 @@ class InfoDialog extends AbstractDialog {
class ProgressDialog extends AbstractDialog {
final String text;
final Function()? onStop;
const ProgressDialog({required this.text, super.key});
const ProgressDialog({required this.text, this.onStop, super.key});
@override
Widget build(BuildContext context) {
@@ -119,7 +120,8 @@ class ProgressDialog extends AbstractDialog {
buttons: [
DialogButton(
text: "Close",
type: ButtonType.only
type: ButtonType.only,
onTap: onStop
)
]
);

View File

@@ -52,7 +52,9 @@ class _DialogButtonState extends State<DialogButton> {
);
}
void _onDefaultSecondaryActionTap() => Navigator.of(context).pop(null);
void _onDefaultSecondaryActionTap() {
Navigator.of(context).pop(null);
}
}
enum ButtonType {

View File

@@ -14,19 +14,30 @@ import '../util/server.dart';
extension ServerControllerDialog on ServerController {
static Semaphore semaphore = Semaphore();
Future<bool> changeStateInteractive(bool ignorePrompts, [bool isRetry = false]) async {
Future<bool> changeStateInteractive(bool onlyIfNeeded, [bool isRetry = false]) async {
try{
semaphore.acquire();
if (type() == ServerType.local) {
return _checkLocalServerInteractive(ignorePrompts);
return _checkLocalServerInteractive(onlyIfNeeded);
}
var oldStarted = started();
started(!started());
if(oldStarted && onlyIfNeeded){
return true;
}
started.value = !started.value;
return await _doStateChange(oldStarted, onlyIfNeeded, isRetry);
}finally{
semaphore.release();
}
}
Future<bool> _doStateChange(bool oldStarted, bool onlyIfNeeded, bool isRetry) async {
if (oldStarted) {
var result = await stop();
if (!result) {
started(true);
started.value = true;
_showCannotStopError();
return true;
}
@@ -34,29 +45,22 @@ extension ServerControllerDialog on ServerController {
return false;
}
var result = await start();
var handled = await _handleResultType(result, ignorePrompts, isRetry);
var result = await start(!onlyIfNeeded);
if(result.type == ServerResultType.ignoreStart) {
started.value = false;
return true;
}
var handled = await _handleResultType(oldStarted, onlyIfNeeded, isRetry, result);
if (!handled) {
started(false);
started.value = false;
return false;
}
embeddedServer?.exitCode.then((value) {
if (!started()) {
return;
}
_showUnexpectedError();
started(false);
});
return handled;
}finally{
semaphore.release();
}
}
Future<bool> _handleResultType(ServerResult result, bool ignorePrompts, bool isRetry) async {
Future<bool> _handleResultType(bool oldStarted, bool onlyIfNeeded, bool isRetry, ServerResult result) async {
switch (result.type) {
case ServerResultType.missingHostError:
_showMissingHostError();
@@ -68,6 +72,10 @@ extension ServerControllerDialog on ServerController {
_showIllegalPortError();
return false;
case ServerResultType.cannotPingServer:
if(!started() || result.pid != embeddedServer?.pid){
return false;
}
_showPingErrorDialog();
return false;
case ServerResultType.portTakenError:
@@ -82,18 +90,18 @@ extension ServerControllerDialog on ServerController {
}
await freeLawinPort();
return changeStateInteractive(ignorePrompts, true);
return _doStateChange(oldStarted, onlyIfNeeded, true);
case ServerResultType.serverDownloadRequiredError:
if (isRetry) {
return false;
}
var result = await _downloadServerInteractive();
var result = await downloadServerInteractive(false);
if (!result) {
return false;
}
return changeStateInteractive(ignorePrompts, true);
return _doStateChange(oldStarted, onlyIfNeeded, true);
case ServerResultType.unknownError:
showDialog(
context: appKey.currentContext!,
@@ -106,6 +114,7 @@ extension ServerControllerDialog on ServerController {
)
);
return false;
case ServerResultType.ignoreStart:
case ServerResultType.started:
return true;
case ServerResultType.canStart:
@@ -116,7 +125,7 @@ extension ServerControllerDialog on ServerController {
Future<bool> _checkLocalServerInteractive(bool ignorePrompts) async {
try {
var future = pingSelf();
var future = pingSelf(port.text);
if(!ignorePrompts) {
await showDialog(
context: appKey.currentContext!,
@@ -138,22 +147,6 @@ extension ServerControllerDialog on ServerController {
}
}
Future<bool> _downloadServerInteractive() async {
var download = compute(downloadServer, true);
return await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: download,
loadingMessage: "Downloading server...",
loadedBody: FutureBuilderDialog.ofMessage(
"The server was downloaded successfully"),
errorMessageBuilder: (
message) => "Cannot download server: $message"
)
) ?? download.isCompleted();
}
Future<void> _showPortTakenError() async {
showDialog(
context: appKey.currentContext!,
@@ -186,6 +179,10 @@ extension ServerControllerDialog on ServerController {
}
void _showPingErrorDialog() {
if(!started.value){
return;
}
showDialog(
context: appKey.currentContext!,
builder: (context) =>
@@ -196,6 +193,10 @@ extension ServerControllerDialog on ServerController {
}
void _showCannotStopError() {
if(!started.value){
return;
}
showDialog(
context: appKey.currentContext!,
builder: (context) =>
@@ -205,12 +206,12 @@ extension ServerControllerDialog on ServerController {
);
}
void _showUnexpectedError() {
void showUnexpectedError() {
showDialog(
context: appKey.currentContext!,
builder: (context) =>
const InfoDialog(
text: "The lawin terminated died unexpectedly"
text: "The lawin server died unexpectedly"
)
);
}
@@ -227,3 +228,20 @@ extension ServerControllerDialog on ServerController {
showMessage("Missing the host name for lawin server");
}
}
Future<bool> downloadServerInteractive(bool closeAutomatically) async {
var download = compute(downloadServer, true);
return await showDialog<bool>(
context: appKey.currentContext!,
builder: (context) =>
FutureBuilderDialog(
future: download,
loadingMessage: "Downloading server...",
loadedBody: FutureBuilderDialog.ofMessage(
"The server was downloaded successfully"),
errorMessageBuilder: (
message) => "Cannot download server: $message",
closeAutomatically: closeAutomatically
)
) ?? download.isCompleted();
}

View File

@@ -5,12 +5,14 @@ import 'package:path/path.dart' as path;
class FortniteVersion {
String name;
Directory location;
bool memoryFix;
FortniteVersion.fromJson(json)
: name = json["name"],
location = Directory(json["location"]);
location = Directory(json["location"]),
memoryFix = json["memory_fix"] ?? false;
FortniteVersion({required this.name, required this.location});
FortniteVersion({required this.name, required this.location, required this.memoryFix});
static File? findExecutable(Directory directory, String name) {
try{

View File

@@ -1,9 +1,10 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:url_launcher/url_launcher.dart';
const String _discordLink = "https://discord.gg/NJU4QjxSMF";
class InfoPage extends StatelessWidget {
const InfoPage({Key? key}) : super(key: key);
@@ -42,8 +43,8 @@ class InfoPage extends StatelessWidget {
Button _createDiscordButton() {
return Button(
child: const Text("Join the discord"),
onPressed: () => launchUrl(Uri.parse(_discordLink)));
child: const Text("Open file directory"),
onPressed: () => launchUrl(Directory(safeBinariesDirectory).uri));
}
CircleAvatar _createAutiesAvatar() {
@@ -55,7 +56,7 @@ class InfoPage extends StatelessWidget {
Align _createVersionInfo() {
return const Align(
alignment: Alignment.bottomRight,
child: Text("Version 4.0${kDebugMode ? '-DEBUG' : ''}")
child: Text("Version 4.4${kDebugMode ? '-DEBUG' : ''}")
);
}
}

View File

@@ -11,11 +11,9 @@ import 'package:reboot_launcher/src/widget/home/game_type_selector.dart';
import 'package:reboot_launcher/src/widget/home/launch_button.dart';
import 'package:reboot_launcher/src/widget/home/username_box.dart';
import 'package:reboot_launcher/src/widget/home/version_selector.dart';
import 'package:url_launcher/url_launcher.dart';
import '../controller/settings_controller.dart';
import '../util/reboot.dart';
import '../widget/shared/warning_info.dart';
class LauncherPage extends StatefulWidget {
const LauncherPage(
@@ -33,10 +31,9 @@ class _LauncherPageState extends State<LauncherPage> {
@override
void initState() {
if(_gameController.updater == null && _settingsController.autoUpdate.value){
if(_gameController.updater == null){
_gameController.updater = compute(downloadRebootDll, _updateTime)
..then((value) => _updateTime = value)
..onError(_saveError);
..then((value) => _updateTime = value);
_buildController.cancelledDownload
.listen((value) => value ? _onCancelWarning() : {});
}
@@ -54,13 +51,6 @@ class _LauncherPageState extends State<LauncherPage> {
storage.write("last_update", updateTime);
}
Future<void> _saveError(Object? error, StackTrace stackTrace) async {
var errorFile = await loadBinary("error.txt", true);
errorFile.writeAsString(
"Error: $error\nStacktrace: $stackTrace", mode: FileMode.write);
throw Exception("Cannot update reboot.dll");
}
void _onCancelWarning() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if(!mounted) {
@@ -78,7 +68,7 @@ class _LauncherPageState extends State<LauncherPage> {
return Padding(
padding: const EdgeInsets.all(12.0),
child: FutureBuilder(
future: _gameController.updater,
future: _gameController.updater ?? Future.value(true),
builder: (context, snapshot) {
if (!snapshot.hasData && !snapshot.hasError) {
return Row(
@@ -114,11 +104,12 @@ class _LauncherPageState extends State<LauncherPage> {
}
Widget _createUpdateError(AsyncSnapshot<Object?> snapshot) {
return WarningInfo(
text: "Cannot update Reboot DLL",
icon: FluentIcons.info,
severity: InfoBarSeverity.warning,
onPressed: () => loadBinary("error.txt", true).then((file) => launchUrl(file.uri))
return const SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("Cannot update dll"),
severity: InfoBarSeverity.warning
),
);
}
}

View File

@@ -5,7 +5,6 @@ import 'package:reboot_launcher/src/widget/server/host_input.dart';
import 'package:reboot_launcher/src/widget/server/server_type_selector.dart';
import 'package:reboot_launcher/src/widget/server/port_input.dart';
import 'package:reboot_launcher/src/widget/server/server_button.dart';
import 'package:reboot_launcher/src/widget/shared/warning_info.dart';
class ServerPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
@@ -21,10 +20,13 @@ class ServerPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if(_serverController.warning.value)
WarningInfo(
text: "The lawin server handles authentication and parties, not game hosting",
icon: FluentIcons.accept,
onPressed: () => _serverController.warning.value = false
SizedBox(
width: double.infinity,
child: InfoBar(
title: const Text("The lawin server handles authentication and parties, not game hosting"),
severity: InfoBarSeverity.info,
onClose: () => _serverController.warning.value = false
),
),
HostInput(),
PortInput(),

View File

@@ -1,13 +1,18 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_launcher/src/controller/server_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/model/server_type.dart';
import 'package:reboot_launcher/src/widget/shared/smart_switch.dart';
import '../util/checks.dart';
import '../widget/os/file_selector.dart';
import '../widget/shared/file_selector.dart';
import '../widget/shared/smart_input.dart';
class SettingsPage extends StatelessWidget {
final ServerController _serverController = Get.find<ServerController>();
final SettingsController _settingsController = Get.find<SettingsController>();
SettingsPage({Key? key}) : super(key: key);
@@ -20,7 +25,21 @@ class SettingsPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FileSelector(
Tooltip(
message: "The hostname of the server that hosts the multiplayer matches",
child: Obx(() => SmartInput(
label: "Matchmaking Host",
placeholder:
"Type the hostname of the server that hosts the multiplayer matches",
controller: _settingsController.matchmakingIp,
validatorMode: AutovalidateMode.always,
validator: checkMatchmaking,
enabled: _serverController.type() == ServerType.embedded
))
),
Tooltip(
message: "The dll that is injected when a server is launched",
child: FileSelector(
label: "Reboot DLL",
placeholder: "Type the path to the reboot dll",
controller: _settingsController.rebootDll,
@@ -28,10 +47,11 @@ class SettingsPage extends StatelessWidget {
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always
validatorMode: AutovalidateMode.always),
),
FileSelector(
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,
@@ -39,10 +59,11 @@ class SettingsPage extends StatelessWidget {
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always
validatorMode: AutovalidateMode.always),
),
FileSelector(
Tooltip(
message: "The dll that is injected to make the game work",
child: FileSelector(
label: "Cranium DLL",
placeholder: "Type the path to the cranium dll",
controller: _settingsController.craniumDll,
@@ -50,15 +71,8 @@ class SettingsPage extends StatelessWidget {
folder: false,
extension: "dll",
validator: checkDll,
validatorMode: AutovalidateMode.always
),
SmartSwitch(
value: _settingsController.autoUpdate,
label: "Update DLLs"
)
]
),
validatorMode: AutovalidateMode.always))
]),
);
}
}

View File

@@ -54,3 +54,11 @@ String? checkDll(String? text) {
return null;
}
String? checkMatchmaking(String? text) {
if (text == null || text.isEmpty) {
return "Empty hostname";
}
return null;
}

View File

@@ -1,8 +1,5 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:path/path.dart' as path;
const int appBarSize = 2;
final RegExp _regex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -16,22 +13,6 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000;
}
Future<String?> openFolderPicker(String title) async =>
await FilePicker.platform.getDirectoryPath(dialogTitle: title);
Future<String?> openFilePicker(String extension) async {
var result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowMultiple: false,
allowedExtensions: [extension]
);
if(result == null || result.files.isEmpty){
return null;
}
return result.files.first.path;
}
Future<File> loadBinary(String binary, bool safe) async{
var safeBinary = File("$safeBinariesDirectory\\$binary");
if(await safeBinary.exists()){

View File

@@ -1,16 +1,31 @@
import 'dart:io';
import 'dart:typed_data';
final Uint8List _original = Uint8List.fromList([
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patched = Uint8List.fromList([
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
Future<bool> patch(File file) async {
if(_original.length != _patched.length){
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
Future<bool> patchHeadless(File file) async =>
_patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
@@ -19,16 +34,16 @@ Future<bool> patch(File file) async {
var offset = 0;
var counter = 0;
while(offset < length){
if(read[offset] == _original[counter]){
if(read[offset] == original[counter]){
counter++;
}else {
counter = 0;
}
offset++;
if(counter == _original.length){
for(var index = 0; index < _patched.length; index++){
read[offset - counter + index] = _patched[index];
if(counter == original.length){
for(var index = 0; index < patched.length; index++){
read[offset - counter + index] = patched[index];
}
await file.writeAsBytes(read, mode: FileMode.write);
@@ -36,5 +51,8 @@ Future<bool> patch(File file) async {
}
}
return false;
}catch(_){
return false;
}
}

View File

@@ -33,7 +33,7 @@ Future<int> downloadRebootDll(int? lastUpdateMs) async {
.firstWhere((element) => path.extension(element.path) == ".dll");
if (exists && sha1.convert(await oldRebootDll.readAsBytes()) == sha1.convert(await File(rebootDll.path).readAsBytes())) {
outputDir.delete(recursive: true);
return lastUpdateMs ?? now.millisecondsSinceEpoch;
return now.millisecondsSinceEpoch;
}
await rebootDll.rename(oldRebootDll.path);

View File

@@ -8,11 +8,13 @@ import 'package:reboot_launcher/src/util/os.dart';
import 'package:http/http.dart' as http;
import 'package:shelf_proxy/shelf_proxy.dart';
import 'package:shelf/shelf_io.dart';
import 'package:path/path.dart' as path;
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin\\Lawin.exe");
final serverLocation = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Lawin.exe");
final serverConfig = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\lawin_new\\Config\\config.ini");
final serverLogFile = File("${Platform.environment["UserProfile"]}\\.reboot_launcher\\server.txt");
const String _serverUrl =
"https://cdn.discordapp.com/attachments/1026121175878881290/1031230792069820487/LawinServer.zip";
"https://cdn.discordapp.com/attachments/1031262639457828910/1034506676843327549/lawin.zip";
Future<bool> downloadServer(ignored) async {
var response = await http.get(Uri.parse(_serverUrl));
@@ -63,7 +65,7 @@ List<String> createRebootArgs(String username, bool headless) {
return args;
}
Future<Uri?> pingSelf() async => ping("127.0.0.1", "3551");
Future<Uri?> pingSelf(String port) async => ping("127.0.0.1", port);
Future<Uri?> ping(String host, String port, [bool https=false]) async {
var hostName = _getHostName(host);
@@ -72,14 +74,15 @@ Future<Uri?> ping(String host, String port, [bool https=false]) async {
var uri = Uri(
scheme: declaredScheme ?? (https ? "https" : "http"),
host: hostName,
port: int.parse(port)
port: int.parse(port),
path: "unknown"
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
var body = utf8.decode(await response.single);
return response.statusCode == 200 && body.contains("Welcome to LawinServer!") ? uri : null;
return body.contains("epicgames") || body.contains("lawinserver") ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await ping(host, port, true);
}
@@ -89,7 +92,7 @@ String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFir
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type) async {
Future<ServerResult> checkServerPreconditions(String host, String port, ServerType type, bool needsFreePort) async {
host = host.trim();
if(host.isEmpty){
return ServerResult(
@@ -113,6 +116,13 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
if(type == ServerType.embedded || type == ServerType.remote){
var free = await isLawinPortFree();
if (!free) {
if(!needsFreePort) {
return ServerResult(
uri: pingSelf(port),
type: ServerResultType.ignoreStart
);
}
return ServerResult(
type: ServerResultType.portTakenError
);
@@ -131,21 +141,42 @@ Future<ServerResult> checkServerPreconditions(String host, String port, ServerTy
);
}
Future<Process> startEmbeddedServer() async {
return await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
Future<Process?> startEmbeddedServer() async {
await resetServerLog();
try {
var process = await Process.start(serverLocation.path, [], workingDirectory: serverLocation.parent.path);
process.outLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
process.errLines.forEach((line) => serverLogFile.writeAsString("$line\n", mode: FileMode.append));
return process;
} on ProcessException {
return null;
}
}
Future<HttpServer> startRemoteServer(Uri uri) async {
return await serve(proxyHandler(uri), "127.0.0.1", 3551);
}
Future<void> resetServerLog() async {
try {
if(await serverLogFile.exists()) {
await serverLogFile.delete();
}
await serverLogFile.create();
}catch(_){
// Ignored
}
}
class ServerResult {
final Future<Uri?>? uri;
final int? pid;
final Object? error;
final StackTrace? stackTrace;
final ServerResultType type;
ServerResult({this.uri, this.error, this.stackTrace, required this.type});
ServerResult({this.uri, this.pid, this.error, this.stackTrace, required this.type});
}
enum ServerResultType {
@@ -156,7 +187,8 @@ enum ServerResultType {
portTakenError,
serverDownloadRequiredError,
canStart,
ignoreStart,
started,
unknownError,
stopped
stopped,
}

View File

@@ -18,7 +18,6 @@ import 'package:reboot_launcher/src/util/injector.dart';
import 'package:reboot_launcher/src/util/patcher.dart';
import 'package:reboot_launcher/src/util/reboot.dart';
import 'package:reboot_launcher/src/util/server.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:win32_suspend_process/win32_suspend_process.dart';
import 'package:path/path.dart' as path;
@@ -55,10 +54,10 @@ class _LaunchButtonState extends State<LaunchButton> {
child: SizedBox(
width: double.infinity,
child: Obx(() => Tooltip(
message: _gameController.started.value ? "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(
onPressed: _onPressed,
child: Text(_gameController.started.value ? "Close" : "Launch")
child: Text(_gameController.started() ? "Close" : "Launch")
),
)),
),
@@ -66,41 +65,38 @@ class _LaunchButtonState extends State<LaunchButton> {
}
void _onPressed() async {
if (_gameController.started()) {
_onStop();
return;
}
_gameController.started.value = true;
if (_gameController.username.text.isEmpty) {
showMessage("Missing in-game username");
_updateServerState(false);
_gameController.started.value = false;
return;
}
if (_gameController.selectedVersionObs.value == null) {
showMessage("No version is selected");
_updateServerState(false);
return;
}
if (_gameController.started.value) {
_onStop();
_gameController.started.value = false;
return;
}
try {
_updateServerState(true);
var version = _gameController.selectedVersionObs.value!;
var hosting = _gameController.type.value == GameType.headlessServer;
var gamePath = version.executable?.path;
if(gamePath == null){
_onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null);
_onStop();
return;
}
if (version.launcher != null) {
_gameController.launcherProcess = await Process.start(version.launcher!.path, []);
Win32Process(_gameController.launcherProcess!.pid).suspend();
}
if(hosting){
await patch(version.executable!);
}
if(!mounted){
_onStop();
return;
}
var result = await _serverController.changeStateInteractive(true);
if(!result){
_onStop();
@@ -111,19 +107,16 @@ class _LaunchButtonState extends State<LaunchButton> {
await _logFile!.delete();
}
var gamePath = version.executable?.path;
if(gamePath == null){
_onError("${version.location.path} no longer contains a Fortnite executable. Did you delete it?", null);
_onStop();
return;
}
_gameController.gameProcess = await Process.start(gamePath, createRebootArgs(_gameController.username.text, hosting))
await patch(version.executable!);
var headlessHosting = _gameController.type() == GameType.headlessServer;
var arguments = createRebootArgs(_gameController.username.text, headlessHosting);
_gameController.gameProcess = await Process.start(gamePath, arguments)
..exitCode.then((_) => _onEnd())
..outLines.forEach(_onGameOutput);
await _injectOrShowError(Injectable.cranium);
if(hosting){
..outLines.forEach((line) => _onGameOutput(line, version.memoryFix))
..errLines.forEach((line) => _onGameOutput(line, version.memoryFix));
if(headlessHosting){
await _showServerLaunchingWarning();
}
} catch (exception, stacktrace) {
@@ -133,12 +126,15 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<void> _updateServerState(bool value) async {
if (_gameController.started.value == value) {
return;
Future<bool> patch(File file) async {
switch(_gameController.type()){
case GameType.client:
return await compute(patchMatchmaking, file);
case GameType.server:
return false;
case GameType.headlessServer:
return await compute(patchHeadless, file);
}
_gameController.started(value);
}
void _onEnd() {
@@ -170,16 +166,13 @@ class _LaunchButtonState extends State<LaunchButton> {
var result = await showDialog<bool>(
context: context,
builder: (context) => InfoDialog.ofOnly(
text: "Launching headless reboot server...",
button: DialogButton(
type: ButtonType.only,
onTap: () {
builder: (context) => ProgressDialog(
text: "Launching headless server...",
onStop: () {
Navigator.of(context).pop(false);
_onStop();
}
)
)
);
if(result != null && result){
@@ -189,7 +182,11 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop();
}
void _onGameOutput(String line) {
void _onGameOutput(String line, bool memoryFix) {
if(kDebugMode){
print(line);
}
if(_logFile != null){
_logFile!.writeAsString("$line\n", mode: FileMode.append);
}
@@ -220,13 +217,22 @@ class _LaunchButtonState extends State<LaunchButton> {
return;
}
if(line.contains("Region")){
if(line.contains("Platform has ")){
_injectOrShowError(Injectable.cranium);
return;
}
if(line.contains("Login: Completing Sign-in")){
if(_gameController.type.value == GameType.client){
_injectOrShowError(Injectable.console);
}else {
_injectOrShowError(Injectable.reboot)
.then((value) => _closeDialogIfOpen(true));
}
if(memoryFix){
_injectOrShowError(Injectable.memoryFix);
}
}
}
@@ -242,7 +248,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
void _onStop() {
_updateServerState(false);
_gameController.started.value = false;
_gameController.kill();
}
@@ -253,7 +259,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
try {
var dllPath = _getDllPath(injectable);
var dllPath = await _getDllPath(injectable);
if(!dllPath.existsSync()) {
await _downloadMissingDll(injectable);
if(!dllPath.existsSync()){
@@ -284,7 +290,7 @@ class _LaunchButtonState extends State<LaunchButton> {
});
}
File _getDllPath(Injectable injectable){
Future<File> _getDllPath(Injectable injectable) async {
switch(injectable){
case Injectable.reboot:
return File(_settingsController.rebootDll.text);
@@ -292,6 +298,8 @@ class _LaunchButtonState extends State<LaunchButton> {
return File(_settingsController.consoleDll.text);
case Injectable.cranium:
return File(_settingsController.craniumDll.text);
case Injectable.memoryFix:
return await loadBinary("fix.dll", true);
}
}
@@ -308,5 +316,6 @@ class _LaunchButtonState extends State<LaunchButton> {
enum Injectable {
console,
cranium,
reboot
reboot,
memoryFix
}

View File

@@ -4,6 +4,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import 'package:reboot_launcher/src/model/fortnite_version.dart';
import 'package:reboot_launcher/src/dialog/add_local_version.dart';
import 'package:reboot_launcher/src/widget/shared/smart_check_box.dart';
@@ -119,7 +120,7 @@ class _VersionSelectorState extends State<VersionSelector> {
context: context,
offset: offset,
builder: (context) => MenuFlyout(
items: ContextualOption.values
items: ContextualOption.getValues(version.memoryFix)
.map((entry) => _createOption(context, entry))
.toList()
)
@@ -172,8 +173,26 @@ class _VersionSelectorState extends State<VersionSelector> {
}
break;
case ContextualOption.enableMemoryFix:
if(!mounted){
return;
}
case null:
version.memoryFix = true;
Navigator.of(context).pop();
showMessage("Enabled memory fix");
break;
case ContextualOption.disableMemoryFix:
if(!mounted){
return;
}
version.memoryFix = false;
Navigator.of(context).pop();
showMessage("Disabled memory fix");
break;
default:
break;
}
}
@@ -280,11 +299,21 @@ class _VersionSelectorState extends State<VersionSelector> {
enum ContextualOption {
openExplorer,
rename,
enableMemoryFix,
disableMemoryFix,
delete;
static List<ContextualOption> getValues(bool memoryFix){
return memoryFix
? [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.disableMemoryFix, ContextualOption.delete]
: [ContextualOption.openExplorer, ContextualOption.rename, ContextualOption.enableMemoryFix, ContextualOption.delete];
}
String get name {
return this == ContextualOption.openExplorer ? "Open in explorer"
: this == ContextualOption.rename ? "Rename"
: this == ContextualOption.enableMemoryFix ? "Enable memory leak fix"
: this == ContextualOption.disableMemoryFix ? "Disable memory leak fix"
: "Delete";
}
}

View File

@@ -1,85 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/src/dialog/snackbar.dart';
import '../../util/os.dart';
class FileSelector extends StatefulWidget {
final String label;
final String placeholder;
final String windowTitle;
final bool allowNavigator;
final TextEditingController controller;
final String? Function(String?) validator;
final AutovalidateMode? validatorMode;
final String? extension;
final bool folder;
const FileSelector(
{required this.label,
required this.placeholder,
required this.windowTitle,
required this.controller,
required this.validator,
required this.folder,
this.extension,
this.validatorMode,
this.allowNavigator = true,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
super(key: key);
@override
State<FileSelector> createState() => _FileSelectorState();
}
class _FileSelectorState extends State<FileSelector> {
bool _selecting = false;
@override
Widget build(BuildContext context) {
return InfoLabel(
label: widget.label,
child: Row(
children: [
Expanded(
child: TextFormBox(
controller: widget.controller,
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction
)
),
if (widget.allowNavigator) const SizedBox(width: 8.0),
if (widget.allowNavigator)
Tooltip(
message: "Select a ${widget.folder ? 'folder' : 'file'}",
child: Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
),
)
],
)
);
}
void _onPressed() {
if(_selecting){
showMessage("Folder selector is already opened");
return;
}
_selecting = true;
if(widget.folder) {
compute(openFolderPicker, widget.windowTitle)
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then((_) => _selecting = false);
return;
}
compute(openFilePicker, widget.extension!)
.then((value) => widget.controller.text = value ?? widget.controller.text)
.then((_) => _selecting = false);
}
}

View File

@@ -8,6 +8,8 @@ class SmartInput extends StatelessWidget {
final bool enabled;
final VoidCallback? onTap;
final bool readOnly;
final AutovalidateMode validatorMode;
final String? Function(String?)? validator;
const SmartInput(
{Key? key,
@@ -17,12 +19,14 @@ class SmartInput extends StatelessWidget {
this.onTap,
this.enabled = true,
this.readOnly = false,
this.type = TextInputType.text})
this.type = TextInputType.text,
this.validatorMode = AutovalidateMode.disabled,
this.validator})
: super(key: key);
@override
Widget build(BuildContext context) {
return TextBox(
return TextFormBox(
enabled: enabled,
controller: controller,
header: label,
@@ -30,6 +34,8 @@ class SmartInput extends StatelessWidget {
placeholder: placeholder,
onTap: onTap,
readOnly: readOnly,
autovalidateMode: validatorMode,
validator: validator
);
}
}

View File

@@ -1,28 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart';
class WarningInfo extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final IconData icon;
final InfoBarSeverity severity;
const WarningInfo(
{Key? key,
required this.text,
required this.icon,
required this.onPressed,
this.severity = InfoBarSeverity.info})
: super(key: key);
@override
Widget build(BuildContext context) {
return InfoBar(
severity: severity,
title: Text(text),
action: IconButton(
icon: Icon(icon),
onPressed: onPressed
)
);
}
}

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Launcher for project reboot
version: "4.0.0"
version: "4.4.0"
publish_to: 'none'
@@ -36,6 +36,7 @@ dependencies:
win32: 3.0.0
clipboard: ^0.1.3
sync: ^0.3.0
ini: ^2.1.0
dependency_overrides:
win32: ^3.0.0
@@ -46,6 +47,7 @@ dev_dependencies:
flutter_lints: ^2.0.1
msix: ^3.6.3
hex: ^0.2.0
flutter:
uses-material-design: true
@@ -58,7 +60,7 @@ msix_config:
display_name: Reboot Launcher
publisher_display_name: Auties00
identity_name: 31868Auties00.RebootLauncher
msix_version: 4.0.0.0
msix_version: 4.4.0.0
publisher: CN=E6CD08C6-DECF-4034-A3EB-2D5FA2CA8029
logo_path: ./assets/icons/reboot.ico
architecture: x64

View File

@@ -1,3 +1,3 @@
flutter_distributor package --platform windows --targets exe
flutter pub run msix:create
dart compile exe ./../lib/cli.dart --output ./../dist/cli/reboot.exe
dart compile exe ./lib/cli.dart --output ./dist/cli/reboot.exe