2 Commits
9.1.2 ... 9.1.4

Author SHA1 Message Date
Alessandro Autiero
0a775e2f3f Merge pull request #52 from Auties00/_onLoggedIn
9.1.3
2024-06-04 22:32:10 +02:00
Alessandro Autiero
2bf084d120 9.1.3 2024-06-04 20:31:06 +02:00
28 changed files with 731 additions and 516 deletions

View File

@@ -1,9 +1,14 @@
# Reboot Launcher ![Banner](https://i.imgur.com/p0P4tcI.png)
![Screenshot (34)](https://github.com/Auties00/reboot_launcher/assets/28218457/de2cac8e-7060-4e11-a91f-e01e3c174b9c) GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
![Screenshot (35)](https://github.com/Auties00/reboot_launcher/assets/28218457/de43d2b8-09fc-4d34-beb1-aa6f7fcaa479) Join our discord at https://discord.gg/reboot
![Screenshot (36)](https://github.com/Auties00/reboot_launcher/assets/28218457/3337f5cd-81d6-45d8-ab47-8018fb8a6cee)
![Screenshot (37)](https://github.com/Auties00/reboot_launcher/assets/28218457/51086ec7-5e68-4411-b704-7837970741c8) ## Modules
![Screenshot (38)](https://github.com/Auties00/reboot_launcher/assets/28218457/9aca3e00-85e3-4580-95bd-fef8b389f40b)
![Screenshot (39)](https://github.com/Auties00/reboot_launcher/assets/28218457/faa5d3a3-18c2-4d53-84c5-6eadc0bf4069) - COMMON: Shared business logic for CLI and GUI modules
![Screenshot (33)](https://github.com/Auties00/reboot_launcher/assets/28218457/6c449aa6-e515-4680-9ee2-d219761f3268) - CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- GUI: Stable graphical user interface to play and host Fortnite S0-14
## Installation
Check the releases section

View File

@@ -5,7 +5,6 @@ import 'package:reboot_cli/src/game.dart';
import 'package:reboot_cli/src/reboot.dart'; import 'package:reboot_cli/src/reboot.dart';
import 'package:reboot_cli/src/server.dart'; import 'package:reboot_cli/src/server.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/matchmaker.dart' as matchmaker;
late String? username; late String? username;
late bool host; late bool host;
@@ -82,7 +81,7 @@ void main(List<String> args) async {
return; return;
} }
matchmaker.writeMatchmakingIp(result["matchmaking-address"]); writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"]; autoRestart = result["auto-restart"];
await startGame(); await startGame();
} }

View File

@@ -24,7 +24,7 @@ Future<void> startGame() async {
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, "")) _gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
..exitCode.then((_) => _onClose()) ..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose)); ..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
_injectOrShowError("cobalt.dll"); _injectOrShowError("cobalt.dll");
} }
@@ -52,6 +52,17 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
stdout.writeln(line); stdout.writeln(line);
} }
handleGameOutput(
line: line,
host: hosting,
onDisplayAttached: () {}, // TODO: Support virtual desktops
onLoggedIn: onLoggedIn,
onMatchEnd: onMatchEnd,
onShutdown: onShutdown,
onTokenError: onTokenError,
onBuildCorrupted: onBuildCorrupted
);
if (line.contains(kShutdownLine)) { if (line.contains(kShutdownLine)) {
_onClose(); _onClose();
return; return;

View File

@@ -2,7 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
extension ProcessExtension on Process { extension ProcessExtension on Process {
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n")); Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n")); Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event, allowMalformed: true).split("\n"));
} }

View File

@@ -1,11 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart';
class GameInstance { class GameInstance {
final String versionName; final String versionName;
final int gamePid; final int gamePid;
final int? launcherPid; final int? launcherPid;
final int? eacPid; final int? eacPid;
final List<InjectableDll> injectedDlls;
bool hosting; bool hosting;
bool launched; bool launched;
bool movedToVirtualDesktop; bool movedToVirtualDesktop;
@@ -19,7 +22,7 @@ class GameInstance {
required this.eacPid, required this.eacPid,
required this.hosting, required this.hosting,
required this.child required this.child
}): tokenError = false, launched = false, movedToVirtualDesktop = false; }): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
void kill() { void kill() {
Process.killPid(gamePid, ProcessSignal.sigabrt); Process.killPid(gamePid, ProcessSignal.sigabrt);

View File

@@ -145,9 +145,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
'"${tempFile.path}"' '"${tempFile.path}"'
], ],
); );
var completed = false;
process.stdOutput.listen((data) { process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
if(data.toLowerCase().contains("everything is ok")) { if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(startTime, now, 100, true, options); _onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt); process?.kill(ProcessSignal.sigabrt);
return; return;
@@ -166,6 +168,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
_onError(data, options); _onError(data, options);
} }
}); });
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break; break;
case ".rar": case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
@@ -183,10 +190,12 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
'"${options.destination.path}"' '"${options.destination.path}"'
] ]
); );
var completed = false;
process.stdOutput.listen((data) { process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch; final now = DateTime.now().millisecondsSinceEpoch;
data = data.replaceAll("\r", "").replaceAll("\b", "").trim(); data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") { if(data == "All OK") {
completed = true;
_onProgress(startTime, now, 100, true, options); _onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt); process?.kill(ProcessSignal.sigabrt);
return; return;
@@ -205,6 +214,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
_onError(data, options); _onError(data, options);
} }
}); });
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break; break;
default: default:
throw ArgumentError("Unexpected file extension: $extension}"); throw ArgumentError("Unexpected file extension: $extension}");

View File

@@ -238,6 +238,31 @@ List<String> createRebootArgs(String username, String password, bool host, bool
return args; return args;
} }
void handleGameOutput({
required String line,
required bool host,
required void Function() onDisplayAttached,
required void Function() onLoggedIn,
required void Function() onMatchEnd,
required void Function() onShutdown,
required void Function() onTokenError,
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) {
onDisplayAttached();
}
}
String _parseUsername(String username, bool host) { String _parseUsername(String username, bool host) {
if(host) { if(host) {
return "Player${Random().nextInt(1000)}"; return "Player${Random().nextInt(1000)}";

View File

@@ -260,7 +260,7 @@
"missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted", "missingExecutableError": "Missing Fortnite executable: usually this means that the installation was moved or deleted",
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version", "corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
"corruptedDllError": "Cannot inject dll: {error}", "corruptedDllError": "Cannot inject dll: {error}",
"tokenError": "Cannot log in into Fortnite: authentication error", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
"serverNoLongerAvailable": "{owner}'s server is no longer available", "serverNoLongerAvailable": "{owner}'s server is no longer available",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available",

View File

@@ -17,7 +17,6 @@ import 'package:reboot_launcher/src/controller/backend_controller.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/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/info_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/controller/update_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
@@ -25,6 +24,7 @@ import 'package:reboot_launcher/src/dialog/implementation/error.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/page/implementation/info_page.dart'; import 'package:reboot_launcher/src/page/implementation/info_page.dart';
import 'package:reboot_launcher/src/util/log.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
@@ -33,24 +33,30 @@ import 'package:system_theme/system_theme.dart';
import 'package:url_protocol/url_protocol.dart'; import 'package:url_protocol/url_protocol.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:win32/win32.dart';
const double kDefaultWindowWidth = 1536; const double kDefaultWindowWidth = 1164;
const double kDefaultWindowHeight = 1224; const double kDefaultWindowHeight = 864;
const String kCustomUrlSchema = "Reboot"; const String kCustomUrlSchema = "Reboot";
Version? appVersion; Version? appVersion;
bool appWithNoStorage = false;
void main() => runZonedGuarded( void main() {
log("[APP] Called");
runZonedGuarded(
() => _startApp(), () => _startApp(),
(error, stack) => onError(error, stack, false), (error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification( zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
) )
); );
}
Future<void> _startApp() async { Future<void> _startApp() async {
final errors = <Object>[]; final errors = <Object>[];
try { try {
log("[APP] Starting application");
final pathError = await _initPath(); final pathError = await _initPath();
if(pathError != null) { if(pathError != null) {
errors.add(pathError); errors.add(pathError);
@@ -66,10 +72,6 @@ Future<void> _startApp() async {
errors.add(notificationsError); errors.add(notificationsError);
} }
WidgetsFlutterBinding.ensureInitialized();
_initWindow();
final tilesError = InfoPage.initInfoTiles(); final tilesError = InfoPage.initInfoTiles();
if(tilesError != null) { if(tilesError != null) {
errors.add(tilesError); errors.add(tilesError);
@@ -80,22 +82,24 @@ Future<void> _startApp() async {
errors.add(versionError); errors.add(versionError);
} }
final storageError = await _initStorage(); final storageErrors = await _initStorage();
if(storageError != null) { errors.addAll(storageErrors);
errors.add(storageError);
} WidgetsFlutterBinding.ensureInitialized();
_initWindow();
final urlError = await _initUrlHandler(); final urlError = await _initUrlHandler();
if(urlError != null) { if(urlError != null) {
errors.add(urlError); errors.add(urlError);
} }
_checkGameServer();
}catch(uncaughtError) { }catch(uncaughtError) {
errors.add(uncaughtError); errors.add(uncaughtError);
} finally{ } finally{
runApp(const RebootApplication()); log("[APP] Started applications with errors: $errors");
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors)); runApp(RebootApplication(
errors: errors,
));
} }
} }
@@ -132,10 +136,6 @@ Future<Object?> _initPath() async {
} }
} }
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
}
Future<Object?> _initVersion() async { Future<Object?> _initVersion() async {
try { try {
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
@@ -146,67 +146,17 @@ Future<Object?> _initVersion() async {
} }
} }
Future<void> _checkGameServer() async {
try {
var backendController = Get.find<BackendController>();
var address = backendController.gameServerAddress.text;
if(isLocalHost(address)) {
return;
}
var result = await pingGameServer(address);
if(result) {
return;
}
var oldOwner = backendController.gameServerOwner.value;
backendController.joinLocalHost();
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
));
}catch(_) {
// Intended behaviour
// Just ignore the error
}
}
Future<Object?> _initUrlHandler() async { Future<Object?> _initUrlHandler() async {
try { try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
var appLinks = AppLinks();
var initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
return null; return null;
}catch(error) { }catch(error) {
return error; return error;
} }
} }
void _joinServer(Uri uri) {
var hostingController = Get.find<HostingController>();
var backendController = Get.find<BackendController>();
var uuid = _parseCustomUrl(uri);
var server = hostingController.findServerById(uuid);
if(server != null) {
backendController.joinServer(hostingController.uuid, server);
}else {
showInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
String _parseCustomUrl(Uri uri) => uri.host;
void _initWindow() => doWhenWindowReady(() async { void _initWindow() => doWhenWindowReady(() async {
try {
await SystemTheme.accentColor.load(); await SystemTheme.accentColor.load();
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await Window.initialize(); await Window.initialize();
@@ -231,32 +181,69 @@ void _initWindow() => doWhenWindowReady(() async {
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
); );
} }
}catch(error, stackTrace) {
onError(error, stackTrace, false);
}finally {
appWindow.show(); appWindow.show();
}
}); });
Future<Object?> _initStorage() async { Future<List<Object>> _initStorage() async {
final errors = <Object>[];
try { try {
await GetStorage("game", settingsDirectory.path).initStorage; await GetStorage("game", settingsDirectory.path).initStorage;
await GetStorage("backend", settingsDirectory.path).initStorage; await GetStorage("backend", settingsDirectory.path).initStorage;
await GetStorage("update", settingsDirectory.path).initStorage; await GetStorage("update", settingsDirectory.path).initStorage;
await GetStorage("settings", settingsDirectory.path).initStorage; await GetStorage("settings", settingsDirectory.path).initStorage;
await GetStorage("hosting", settingsDirectory.path).initStorage; await GetStorage("hosting", settingsDirectory.path).initStorage;
Get.put(GameController());
Get.put(BackendController());
Get.put(BuildController());
Get.put(SettingsController());
Get.put(HostingController());
Get.put(InfoController());
Get.put(UpdateController());
return null;
}catch(error) { }catch(error) {
return error; appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
} }
try {
Get.put(GameController());
}catch(error) {
errors.add(error);
}
try {
Get.put(BackendController());
}catch(error) {
errors.add(error);
}
try {
Get.put(BuildController());
}catch(error) {
errors.add(error);
}
try {
Get.put(HostingController());
}catch(error) {
errors.add(error);
}
try {
Get.put(UpdateController());
}catch(error) {
errors.add(error);
}
try {
Get.put(SettingsController());
}catch(error) {
errors.add(error);
}
return errors;
} }
class RebootApplication extends StatefulWidget { class RebootApplication extends StatefulWidget {
const RebootApplication({Key? key}) : super(key: key); final List<Object> errors;
const RebootApplication({Key? key, required this.errors}) : super(key: key);
@override @override
State<RebootApplication> createState() => _RebootApplicationState(); State<RebootApplication> createState() => _RebootApplicationState();
@@ -265,6 +252,16 @@ class RebootApplication extends StatefulWidget {
class _RebootApplicationState extends State<RebootApplication> { class _RebootApplicationState extends State<RebootApplication> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors));
}
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
}
@override @override
Widget build(BuildContext context) => Obx(() => FluentApp( Widget build(BuildContext context) => Obx(() => FluentApp(
locale: Locale(_settingsController.language.value), locale: Locale(_settingsController.language.value),

View File

@@ -5,9 +5,10 @@ import 'package:fluent_ui/fluent_ui.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_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
class BackendController extends GetxController { class BackendController extends GetxController {
late final GetStorage storage; late final GetStorage? storage;
late final TextEditingController host; late final TextEditingController host;
late final TextEditingController port; late final TextEditingController port;
late final Rx<ServerType> type; late final Rx<ServerType> type;
@@ -21,13 +22,13 @@ class BackendController extends GetxController {
HttpServer? remoteServer; HttpServer? remoteServer;
BackendController() { BackendController() {
storage = GetStorage("backend"); storage = appWithNoStorage ? null : GetStorage("backend");
started = RxBool(false); started = RxBool(false);
type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0)); type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
type.listen((value) { type.listen((value) {
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;
} }
@@ -36,13 +37,13 @@ class BackendController extends GetxController {
}); });
host = TextEditingController(text: _readHost()); host = TextEditingController(text: _readHost());
host.addListener(() => host.addListener(() =>
storage.write("${type.value.name}_host", host.text)); storage?.write("${type.value.name}_host", host.text));
port = TextEditingController(text: _readPort()); port = TextEditingController(text: _readPort());
port.addListener(() => port.addListener(() =>
storage.write("${type.value.name}_port", port.text)); storage?.write("${type.value.name}_port", port.text));
detached = RxBool(storage.read("detached") ?? false); detached = RxBool(storage?.read("detached") ?? false);
detached.listen((value) => storage.write("detached", value)); detached.listen((value) => storage?.write("detached", value));
gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? "127.0.0.1"); gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1");
var lastValue = gameServerAddress.text; var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue); writeMatchmakingIp(lastValue);
gameServerAddress.addListener(() { gameServerAddress.addListener(() {
@@ -53,7 +54,7 @@ class BackendController extends GetxController {
lastValue = newValue; lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length); gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
storage.write("game_server_address", newValue); storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue); writeMatchmakingIp(newValue);
}); });
watchMatchmakingIp().listen((event) { watchMatchmakingIp().listen((event) {
@@ -62,15 +63,15 @@ class BackendController extends GetxController {
} }
}); });
gameServerAddressFocusNode = FocusNode(); gameServerAddressFocusNode = FocusNode();
gameServerOwner = RxnString(storage.read("game_server_owner")); gameServerOwner = RxnString(storage?.read("game_server_owner"));
gameServerOwner.listen((value) => storage.write("game_server_owner", value)); gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
} }
void reset() async { void reset() async {
type.value = ServerType.values.elementAt(0); type.value = ServerType.values.elementAt(0);
for (final type in ServerType.values) { for (final type in ServerType.values) {
storage.write("${type.name}_host", null); storage?.write("${type.name}_host", null);
storage.write("${type.name}_port", null); storage?.write("${type.name}_port", null);
} }
host.text = type.value != ServerType.remote ? kDefaultBackendHost : ""; host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
@@ -79,7 +80,7 @@ class BackendController extends GetxController {
} }
String _readHost() { String _readHost() {
String? value = storage.read("${type.value.name}_host"); String? value = storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) { if (value != null && value.isNotEmpty) {
return value; return value;
} }
@@ -92,7 +93,7 @@ class BackendController extends GetxController {
} }
String _readPort() => String _readPort() =>
storage.read("${type.value.name}_port") ?? kDefaultBackendPort.toString(); storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start() async* { Stream<ServerResult> start() async* {
try { try {

View File

@@ -9,10 +9,12 @@ import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart'; import 'package:reboot_launcher/src/util/keyboard.dart';
import '../../main.dart';
class GameController extends GetxController { class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage _storage; late final GetStorage? _storage;
late final TextEditingController username; late final TextEditingController username;
late final TextEditingController password; late final TextEditingController password;
late final TextEditingController customLaunchArgs; late final TextEditingController customLaunchArgs;
@@ -23,38 +25,37 @@ class GameController extends GetxController {
late final Rx<PhysicalKeyboardKey> consoleKey; late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() { GameController() {
_storage = GetStorage("game"); _storage = appWithNoStorage ? null : GetStorage("game");
Iterable decodedVersionsJson = jsonDecode( Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
_storage.read("versions") ?? "[]"); final decodedVersions = decodedVersionsJson
var decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry)) .map((entry) => FortniteVersion.fromJson(entry))
.toList(); .toList();
versions = Rx(decodedVersions); versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions()); versions.listen((data) => _saveVersions());
var decodedSelectedVersionName = _storage.read("version"); final decodedSelectedVersionName = _storage?.read("version");
var decodedSelectedVersion = decodedVersions.firstWhereOrNull(( final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
element) => element.name == decodedSelectedVersionName); element) => element.name == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion); _selectedVersion = Rxn(decodedSelectedVersion);
username = TextEditingController( username = TextEditingController(
text: _storage.read("username") ?? kDefaultPlayerName); text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage.write("username", username.text)); username.addListener(() => _storage?.write("username", username.text));
password = TextEditingController(text: _storage.read("password") ?? ""); password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text)); password.addListener(() => _storage?.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? ""); customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => customLaunchArgs.addListener(() =>
_storage.write("custom_launch_args", customLaunchArgs.text)); _storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false); started = RxBool(false);
instance = Rxn(); instance = Rxn();
consoleKey = Rx(_readConsoleKey()); consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value); _writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) { consoleKey.listen((newValue) {
_storage.write("console_key", newValue.usbHidUsage); _storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue); _writeConsoleKey(newValue);
}); });
} }
PhysicalKeyboardKey _readConsoleKey() { PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage.read("console_key"); final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) { if(consoleKeyValue == null) {
return _kDefaultConsoleKey; return _kDefaultConsoleKey;
} }
@@ -113,7 +114,7 @@ class GameController extends GetxController {
Future<void> _saveVersions() async { Future<void> _saveVersions() async {
var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList()); var serialized = jsonEncode(versions.value.map((entry) => entry.toJson()).toList());
await _storage.write("versions", serialized); await _storage?.write("versions", serialized);
} }
bool get hasVersions => versions.value.isNotEmpty; bool get hasVersions => versions.value.isNotEmpty;
@@ -124,7 +125,7 @@ class GameController extends GetxController {
set selectedVersion(FortniteVersion? version) { set selectedVersion(FortniteVersion? version) {
_selectedVersion.value = version; _selectedVersion.value = version;
_storage.write("version", version?.name); _storage?.write("version", version?.name);
} }
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) { void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {

View File

@@ -2,11 +2,12 @@ import 'package:fluent_ui/fluent_ui.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_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class HostingController extends GetxController { class HostingController extends GetxController {
late final GetStorage _storage; late final GetStorage? _storage;
late final String uuid; late final String uuid;
late final TextEditingController name; late final TextEditingController name;
late final TextEditingController description; late final TextEditingController description;
@@ -22,23 +23,23 @@ class HostingController extends GetxController {
late final Rxn<Set<Map<String, dynamic>>> servers; late final Rxn<Set<Map<String, dynamic>>> servers;
HostingController() { HostingController() {
_storage = GetStorage("hosting"); _storage = appWithNoStorage ? null : GetStorage("hosting");
uuid = _storage.read("uuid") ?? const Uuid().v4(); uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage.write("uuid", uuid); _storage?.write("uuid", uuid);
name = TextEditingController(text: _storage.read("name")); name = TextEditingController(text: _storage?.read("name"));
name.addListener(() => _storage.write("name", name.text)); name.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage.read("description")); description = TextEditingController(text: _storage?.read("description"));
description.addListener(() => _storage.write("description", description.text)); description.addListener(() => _storage?.write("description", description.text));
password = TextEditingController(text: _storage.read("password") ?? ""); password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text)); password.addListener(() => _storage?.write("password", password.text));
discoverable = RxBool(_storage.read("discoverable") ?? false); discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage.write("discoverable", value)); discoverable.listen((value) => _storage?.write("discoverable", value));
headless = RxBool(_storage.read("headless") ?? true); headless = RxBool(_storage?.read("headless") ?? true);
headless.listen((value) => _storage.write("headless", value)); headless.listen((value) => _storage?.write("headless", value));
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true); virtualDesktop = RxBool(_storage?.read("virtual_desktop") ?? true);
virtualDesktop.listen((value) => _storage.write("virtual_desktop", value)); virtualDesktop.listen((value) => _storage?.write("virtual_desktop", value));
autoRestart = RxBool(_storage.read("auto_restart") ?? true); autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage.write("auto_restart", value)); autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false); started = RxBool(false);
published = RxBool(false); published = RxBool(false);
showPassword = RxBool(false); showPassword = RxBool(false);

View File

@@ -1,8 +0,0 @@
import 'package:get/get.dart';
class InfoController extends GetxController {
List<String>? links;
Map<String, String> linksData;
InfoController() : linksData = {};
}

View File

@@ -11,7 +11,7 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class UpdateController { class UpdateController {
late final GetStorage _storage; late final GetStorage? _storage;
late final RxnInt timestamp; late final RxnInt timestamp;
late final Rx<UpdateStatus> status; late final Rx<UpdateStatus> status;
late final Rx<UpdateTimer> timer; late final Rx<UpdateTimer> timer;
@@ -21,17 +21,17 @@ class UpdateController {
Future? _updater; Future? _updater;
UpdateController() { UpdateController() {
_storage = GetStorage("update"); _storage = appWithNoStorage ? null : GetStorage("update");
timestamp = RxnInt(_storage.read("ts")); timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage.write("ts", value)); timestamp.listen((value) => _storage?.write("ts", value));
var timerIndex = _storage.read("timer"); var timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex)); timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage.write("timer", value.index)); timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl); url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage.write("update_url", url.text)); url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting); status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage.read("custom_game_server") ?? false); customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage.write("custom_game_server", value)); customGameServer.listen((value) => _storage?.write("custom_game_server", value));
} }
Future<void> notifyLauncherUpdate() async { Future<void> notifyLauncherUpdate() async {
@@ -65,17 +65,17 @@ class UpdateController {
); );
} }
Future<void> updateReboot([bool force = false]) async { Future<void> updateReboot({bool force = false, bool silent = false}) async {
if(_updater != null) { if(_updater != null) {
return await _updater; return await _updater;
} }
final result = _updateReboot(force); final result = _updateReboot(force, silent);
_updater = result; _updater = result;
return await result; return await result;
} }
Future<void> _updateReboot([bool force = false]) async { Future<void> _updateReboot(bool force, bool silent) async {
try { try {
if(customGameServer.value) { if(customGameServer.value) {
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
@@ -92,23 +92,29 @@ class UpdateController {
return; return;
} }
if(!silent) {
infoBarEntry = showInfoBar( infoBarEntry = showInfoBar(
translations.downloadingDll("reboot"), translations.downloadingDll("reboot"),
loading: true, loading: true,
duration: null duration: null
); );
}
timestamp.value = await downloadRebootDll(url.text); timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success; status.value = UpdateStatus.success;
infoBarEntry?.close(); infoBarEntry?.close();
if(!silent) {
infoBarEntry = showInfoBar( infoBarEntry = showInfoBar(
translations.downloadDllSuccess("reboot"), translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
}
}catch(message) { }catch(message) {
if(!silent) {
infoBarEntry?.close(); infoBarEntry?.close();
var error = message.toString(); var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error =
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase(); error = error.toLowerCase();
status.value = UpdateStatus.error; status.value = UpdateStatus.error;
showInfoBar( showInfoBar(
@@ -116,10 +122,14 @@ class UpdateController {
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
action: Button( action: Button(
onPressed: () => updateReboot(true), onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry), child: Text(translations.downloadDllRetry),
) )
); );
}
}finally { }finally {
_updater = null; _updater = null;
} }

View File

@@ -4,14 +4,14 @@ import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import '../../util/log.dart';
String? lastError; String? lastError;
void onError(Object exception, StackTrace? stackTrace, bool framework) { void onError(Object exception, StackTrace? stackTrace, bool framework) {
if(!kDebugMode) { log("[ERROR] $exception");
return; log("[STACKTRACE] $stackTrace");
}
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){ if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
return; return;
} }

View File

@@ -2,20 +2,26 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage; import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/controller/update_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/dll.dart'; import 'package:reboot_launcher/src/dialog/implementation/dll.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart'; import 'package:reboot_launcher/src/page/abstract/page_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/dll.dart'; import 'package:reboot_launcher/src/util/dll.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/info_bar_area.dart';
@@ -33,6 +39,8 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin { class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
static const double _kDefaultPadding = 12.0; static const double _kDefaultPadding = 12.0;
final BackendController _backendController = Get.find<BackendController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>(); final UpdateController _updateController = Get.find<UpdateController>();
final GlobalKey _searchKey = GlobalKey(); final GlobalKey _searchKey = GlobalKey();
@@ -45,9 +53,62 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override @override
void initState() { void initState() {
windowManager.addListener(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdates());
super.initState(); super.initState();
windowManager.addListener(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkUpdates();
_initAppLink();
_checkGameServer();
});
}
void _initAppLink() async {
final appLinks = AppLinks();
final initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
}
void _joinServer(Uri uri) {
final uuid = uri.host;
final server = _hostingController.findServerById(uuid);
if(server != null) {
_backendController.joinServer(_hostingController.uuid, server);
}else {
showInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
Future<void> _checkGameServer() async {
try {
final address = _backendController.gameServerAddress.text;
if(isLocalHost(address)) {
return;
}
var result = await pingGameServer(address);
if(result) {
return;
}
var oldOwner = _backendController.gameServerOwner.value;
_backendController.joinLocalHost();
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
));
}catch(_) {
// Intended behaviour
// Just ignore the error
}
} }
void _checkUpdates() { void _checkUpdates() {
@@ -58,7 +119,10 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
} }
for(final injectable in InjectableDll.values) { for(final injectable in InjectableDll.values) {
downloadCriticalDllInteractive("${injectable.name}.dll"); downloadCriticalDllInteractive(
injectable.path,
silent: true
);
} }
watchDlls().listen((filePath) => showDllDeletedDialog(() { watchDlls().listen((filePath) => showDllDeletedDialog(() {
@@ -157,7 +221,8 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
super.build(context); super.build(context);
_settingsController.language.value; _settingsController.language.value;
loadTranslations(context); loadTranslations(context);
return Obx(() => NavigationPaneTheme( return Obx(() {
return NavigationPaneTheme(
data: NavigationPaneThemeData( data: NavigationPaneThemeData(
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93), backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
), ),
@@ -203,8 +268,8 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
onOpenSearch: () => _searchFocusNode.requestFocus(), onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child transitionBuilder: (child, animation) => child
) )
),
); );
});
} }
Widget get _backButton => StreamBuilder( Widget get _backButton => StreamBuilder(

View File

@@ -69,9 +69,11 @@ class InfoPage extends RebootPage {
class _InfoPageState extends RebootPageState<InfoPage> { class _InfoPageState extends RebootPageState<InfoPage> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
RxInt _counter = RxInt(kDebugMode ? 0 : 180); RxInt _counter = RxInt(kDebugMode ? 0 : 180);
late bool _showButton;
@override @override
void initState() { void initState() {
_showButton = _settingsController.firstRun.value;
if(_settingsController.firstRun.value) { if(_settingsController.firstRun.value) {
Timer.periodic(const Duration(seconds: 1), (timer) { Timer.periodic(const Duration(seconds: 1), (timer) {
if (_counter.value <= 0) { if (_counter.value <= 0) {
@@ -89,11 +91,12 @@ class _InfoPageState extends RebootPageState<InfoPage> {
List<Widget> get settings => InfoPage._infoTiles; List<Widget> get settings => InfoPage._infoTiles;
@override @override
Widget? get button => Obx(() { Widget? get button {
if(!_settingsController.firstRun.value) { if(!_showButton) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Obx(() {
final totalSecondsLeft = _counter.value; final totalSecondsLeft = _counter.value;
final minutesLeft = totalSecondsLeft ~/ 60; final minutesLeft = totalSecondsLeft ~/ 60;
final secondsLeft = totalSecondsLeft % 60; final secondsLeft = totalSecondsLeft % 60;
@@ -101,7 +104,10 @@ class _InfoPageState extends RebootPageState<InfoPage> {
width: double.infinity, width: double.infinity,
height: 48, height: 48,
child: Button( child: Button(
onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null, onPressed: totalSecondsLeft <= 0 ? () {
_showButton = false;
pageIndex.value = RebootPageType.play.index;
} : null,
child: Text( child: Text(
totalSecondsLeft <= 0 ? "I have read the instructions" totalSecondsLeft <= 0 ? "I have read the instructions"
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}" : "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
@@ -110,3 +116,4 @@ class _InfoPageState extends RebootPageState<InfoPage> {
); );
}); });
} }
}

View File

@@ -11,6 +11,7 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_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/controller/update_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/data.dart'; import 'package:reboot_launcher/src/dialog/implementation/data.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart';
@@ -65,8 +66,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
} }
@override @override
Widget get button => const LaunchButton( Widget get button => LaunchButton(
host: true host: true,
startLabel: translations.startHosting,
stopLabel: translations.stopHosting
); );
@override @override
@@ -194,6 +197,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.settingsServerTypeName), title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription), subtitle: Text(translations.settingsServerTypeDescription),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), leading: Text(_updateController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: { items: {
false: translations.settingsServerTypeEmbeddedName, false: translations.settingsServerTypeEmbeddedName,
@@ -209,7 +214,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
_updateController.customGameServer.value = entry.key; _updateController.customGameServer.value = entry.key;
_updateController.infoBarEntry?.close(); _updateController.infoBarEntry?.close();
if(!entry.key) { if(!entry.key) {
_updateController.updateReboot(true); _updateController.updateReboot(
force: true
);
} }
} }
)).toList() )).toList()
@@ -256,13 +263,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.settingsServerTimerName), title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle), subtitle: Text(translations.settingsServerTimerSubtitle),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_updateController.timer.value.text), leading: Text(_updateController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem( items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text), text: Text(entry.text),
onPressed: () { onPressed: () {
_updateController.timer.value = entry; _updateController.timer.value = entry;
_updateController.infoBarEntry?.close(); _updateController.infoBarEntry?.close();
_updateController.updateReboot(true); _updateController.updateReboot(
force: true
);
} }
)).toList() )).toList()
)) ))

View File

@@ -5,6 +5,7 @@ import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.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/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/implementation/data.dart'; import 'package:reboot_launcher/src/dialog/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart';
@@ -46,6 +47,8 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
title: Text(translations.settingsUtilsLanguageName), title: Text(translations.settingsUtilsLanguageName),
subtitle: Text(translations.settingsUtilsLanguageDescription), subtitle: Text(translations.settingsUtilsLanguageDescription),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_getLocaleName(_settingsController.language.value)), leading: Text(_getLocaleName(_settingsController.language.value)),
items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem( items: AppLocalizations.supportedLocales.map((locale) => MenuFlyoutItem(
text: Text(_getLocaleName(locale.languageCode)), text: Text(_getLocaleName(locale.languageCode)),
@@ -60,6 +63,8 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
title: Text(translations.settingsUtilsThemeName), title: Text(translations.settingsUtilsThemeName),
subtitle: Text(translations.settingsUtilsThemeDescription), subtitle: Text(translations.settingsUtilsThemeDescription),
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.themeMode.value.title), leading: Text(_settingsController.themeMode.value.title),
items: ThemeMode.values.map((themeMode) => MenuFlyoutItem( items: ThemeMode.values.map((themeMode) => MenuFlyoutItem(
text: Text(themeMode.title), text: Text(themeMode.title),

View File

@@ -13,40 +13,51 @@ import 'package:reboot_launcher/src/util/translations.dart';
final UpdateController _updateController = Get.find<UpdateController>(); final UpdateController _updateController = Get.find<UpdateController>();
final Map<String, Future<void>> _operations = {}; final Map<String, Future<void>> _operations = {};
Future<void> downloadCriticalDllInteractive(String filePath) { Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
final old = _operations[filePath]; final old = _operations[filePath];
if(old != null) { if(old != null) {
return old; return old;
} }
final newRun = _downloadCriticalDllInteractive(filePath); final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun; _operations[filePath] = newRun;
return newRun; return newRun;
} }
Future<void> _downloadCriticalDllInteractive(String filePath) async { Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = path.basename(filePath).toLowerCase(); final fileName = path.basename(filePath).toLowerCase();
InfoBarEntry? entry; InfoBarEntry? entry;
try { try {
if (fileName == "reboot.dll") { if (fileName == "reboot.dll") {
await _updateController.updateReboot(true); await _updateController.updateReboot(
silent: silent
);
return;
}
if(File(filePath).existsSync()) {
return; return;
} }
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath); final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
if(!silent) {
entry = showInfoBar( entry = showInfoBar(
translations.downloadingDll(fileNameWithoutExtension), translations.downloadingDll(fileNameWithoutExtension),
loading: true, loading: true,
duration: null duration: null
); );
}
await downloadCriticalDll(fileName, filePath); await downloadCriticalDll(fileName, filePath);
entry.close(); entry?.close();
if(!silent) {
entry = await showInfoBar( entry = await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension), translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
}
}catch(message) { }catch(message) {
if(!silent) {
entry?.close(); entry?.close();
var error = message.toString(); var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
@@ -66,6 +77,7 @@ Future<void> _downloadCriticalDllInteractive(String filePath) async {
) )
); );
await completer.future; await completer.future;
}
}finally { }finally {
_operations.remove(fileName); _operations.remove(fileName);
} }

View File

@@ -17,7 +17,13 @@ File _createLoggingFile() {
} }
void log(String message) async { void log(String message) async {
try {
await _semaphore.acquire(); await _semaphore.acquire();
print(message);
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true); await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
}catch(error) {
print(error);
}finally {
_semaphore.release(); _semaphore.release();
} }
}

View File

@@ -305,10 +305,25 @@ final class Win32Process extends Struct {
external int HWndLength; external int HWndLength;
external Pointer<Uint32> HWnd; external Pointer<Uint32> HWnd;
external Pointer<Utf16> excluded;
} }
int _filter(int HWnd, int lParam) { int _filter(int HWnd, int lParam) {
final structure = Pointer.fromAddress(lParam).cast<Win32Process>(); final structure = Pointer.fromAddress(lParam).cast<Win32Process>();
if(structure.ref.excluded != nullptr) {
final excludedWindowName = structure.ref.excluded.toDartString();
final windowNameLength = GetWindowTextLength(HWnd);
if(windowNameLength > 0) {
final windowNamePointer = calloc<Uint16>(windowNameLength + 1).cast<Utf16>();
GetWindowText(HWnd, windowNamePointer, windowNameLength);
final windowName = windowNamePointer.toDartString(length: windowNameLength);
if(windowName.toLowerCase().contains(excludedWindowName.toLowerCase())) {
return TRUE;
}
}
}
final pidPointer = calloc<Uint32>(); final pidPointer = calloc<Uint32>();
GetWindowThreadProcessId(HWnd, pidPointer); GetWindowThreadProcessId(HWnd, pidPointer);
final pid = pidPointer.value; final pid = pidPointer.value;
@@ -330,9 +345,13 @@ int _filter(int HWnd, int lParam) {
return TRUE; return TRUE;
} }
List<int> _getHWnds(int pid) { List<int> _getHWnds(int pid, String? excludedWindowName) {
final result = calloc<Win32Process>(); final result = calloc<Win32Process>();
result.ref.pid = pid; result.ref.pid = pid;
if(excludedWindowName != null) {
result.ref.excluded = excludedWindowName.toNativeUtf16();
}
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address); EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address);
final length = result.ref.HWndLength; final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd; final HWndsPointer = result.ref.HWnd;
@@ -400,24 +419,26 @@ class VirtualDesktopManager {
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops(); List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
Future<void> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1)}) async { Future<bool> moveWindowToDesktop(int pid, IVirtualDesktop desktop, {Duration pollTime = const Duration(seconds: 1), int remainingPolls = 10, String? excludedWindowName}) async {
final hWNDs = _getHWnds(pid); for(final hWND in _getHWnds(pid, excludedWindowName)) {
if(hWNDs.isEmpty) {
await Future.delayed(pollTime);
await moveWindowToDesktop(pid, desktop, pollTime: pollTime);
return;
}
for(final hWND in hWNDs) {
final window = applicationViewCollection.getViewForHWnd(hWND); final window = applicationViewCollection.getViewForHWnd(hWND);
if(window != null) { if(window != null) {
windowManager.moveWindowToDesktop(window, desktop); windowManager.moveWindowToDesktop(window, desktop);
return; return true;
} }
} }
if(remainingPolls <= 0) {
return false;
}
await Future.delayed(pollTime); await Future.delayed(pollTime);
await moveWindowToDesktop(pid, desktop, pollTime: pollTime); return await moveWindowToDesktop(
pid,
desktop,
pollTime: pollTime,
remainingPolls: remainingPolls - 1
);
} }
IVirtualDesktop createDesktop() => windowManager.createDesktop(); IVirtualDesktop createDesktop() => windowManager.createDesktop();

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:dart_ipify/dart_ipify.dart'; import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@@ -12,7 +13,6 @@ import 'package:reboot_launcher/src/controller/backend_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/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_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/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart'; import 'package:reboot_launcher/src/dialog/abstract/dialog_button.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
@@ -27,10 +27,10 @@ import 'package:reboot_launcher/src/util/translations.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
final bool host; final bool host;
final String? startLabel; final String startLabel;
final String? stopLabel; final String stopLabel;
const LaunchButton({Key? key, required this.host, this.startLabel, this.stopLabel}) : super(key: key); const LaunchButton({Key? key, required this.host, required this.startLabel, required this.stopLabel}) : super(key: key);
@override @override
State<LaunchButton> createState() => _LaunchButtonState(); State<LaunchButton> createState() => _LaunchButtonState();
@@ -43,7 +43,6 @@ class _LaunchButtonState extends State<LaunchButton> {
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar; InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
@@ -60,52 +59,42 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()), onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()),
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text(_hasStarted ? _stopMessage : _startMessage) child: Text((widget.host ? _hostingController.started() : _gameController.started()) ? widget.stopLabel : widget.startLabel)
) )
), ),
)), )),
), ),
); );
bool get _hasStarted => widget.host ? _hostingController.started() : _gameController.started();
void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started; void _setStarted(bool hosting, bool started) => hosting ? _hostingController.started.value = started : _gameController.started.value = started;
String get _startMessage => widget.startLabel ?? (widget.host ? translations.startHosting : translations.startGame); Future<void> _toggle({bool? host, bool forceGUI = false}) async {
host ??= widget.host;
String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame); log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
if (host ? _hostingController.started() : _gameController.started()) {
Future<void> _toggle({bool forceGUI = false}) async { log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
log("[${widget.host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
if (_hasStarted) {
log("[${widget.host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop( _onStop(
reason: _StopReason.normal reason: _StopReason.normal
); );
return; return;
} }
if(_operation != null) {
log("[${widget.host ? 'HOST' : 'GAME'}] Already started, ignoring user action");
return;
}
final version = _gameController.selectedVersion; final version = _gameController.selectedVersion;
log("[${widget.host ? 'HOST' : 'GAME'}] Version data: $version"); log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){ if(version == null){
log("[${widget.host ? 'HOST' : 'GAME'}] No version selected"); log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop( _onStop(
reason: _StopReason.missingVersionError reason: _StopReason.missingVersionError
); );
return; return;
} }
log("[${widget.host ? 'HOST' : 'GAME'}] Setting started..."); log("[${host ? 'HOST' : 'GAME'}] Setting started...");
_setStarted(widget.host, true); _setStarted(host, true);
log("[${widget.host ? 'HOST' : 'GAME'}] Set started"); log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) { for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, widget.host) == null) { if(await _getDllFileOrStop(injectable, host) == null) {
return; return;
} }
} }
@@ -113,7 +102,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try { try {
final executable = version.gameExecutable; final executable = version.gameExecutable;
if(executable == null){ if(executable == null){
log("[${widget.host ? 'HOST' : 'GAME'}] No executable found"); log("[${host ? 'HOST' : 'GAME'}] No executable found");
_onStop( _onStop(
reason: _StopReason.missingExecutableError, reason: _StopReason.missingExecutableError,
error: version.location.path error: version.location.path
@@ -121,27 +110,27 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
log("[${widget.host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})..."); log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggleInteractive(); final backendResult = _backendController.started() || await _backendController.toggleInteractive();
if(!backendResult){ if(!backendResult){
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start backend"); log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop( _onStop(
reason: _StopReason.backendError reason: _StopReason.backendError
); );
return; return;
} }
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works"); log("[${host ? 'HOST' : 'GAME'}] Backend works");
final headless = !forceGUI && _hostingController.headless.value; final headless = !forceGUI && _hostingController.headless.value;
final virtualDesktop = _hostingController.virtualDesktop.value; final virtualDesktop = _hostingController.virtualDesktop.value;
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, headless, virtualDesktop, false); final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, virtualDesktop, false);
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
await _startGameProcesses(version, widget.host, headless, virtualDesktop, linkedHostingInstance); await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance);
if(!widget.host) { if(!host) {
_showLaunchingGameClientWidget(); _showLaunchingGameClientWidget();
} }
if(linkedHostingInstance != null || widget.host){ if(linkedHostingInstance != null || host){
_showLaunchingGameServerWidget(); _showLaunchingGameServerWidget();
} }
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
@@ -153,34 +142,34 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool headless, bool virtualDesktop, bool forceLinkedHosting) async { Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
log("[${widget.host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically..."); log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(widget.host){ if(host){
log("[${widget.host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary"); log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
return null; return null;
} }
if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) { if(_backendController.type.value != ServerType.embedded || !isLocalHost(_backendController.gameServerAddress.text)) {
log("[${widget.host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server"); log("[${host ? 'HOST' : 'GAME'}] Backend is not set to embedded and/or not pointing to the local game server");
return null; return null;
} }
if(_hostingController.started()){ if(_hostingController.started()){
log("[${widget.host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server"); log("[${host ? 'HOST' : 'GAME'}] The user has already manually started the hosting server");
return null; return null;
} }
final response = forceLinkedHosting || await _askForAutomaticGameServer(); final response = forceLinkedHosting || await _askForAutomaticGameServer();
if(!response) { if(!response) {
log("[${widget.host ? 'HOST' : 'GAME'}] The user disabled the automatic server"); log("[${host ? 'HOST' : 'GAME'}] The user disabled the automatic server");
return null; return null;
} }
log("[${widget.host ? 'HOST' : 'GAME'}] Starting implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null); final instance = await _startGameProcesses(version, true, headless, virtualDesktop, null);
log("[${widget.host ? 'HOST' : 'GAME'}] Started implicit game server..."); log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
_setStarted(true, true); _setStarted(true, true);
log("[${widget.host ? 'HOST' : 'GAME'}] Set implicit game server as started"); log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
return instance; return instance;
} }
@@ -245,11 +234,6 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async { Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, bool headless, bool virtualDesktop, GameInstance? linkedHosting) async {
if(!_hasStarted) {
log("[${host ? 'HOST' : 'GAME'}] Discarding start game process request as the state is no longer started");
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs( final gameArgs = createRebootArgs(
_gameController.username.text, _gameController.username.text,
@@ -265,40 +249,52 @@ class _LaunchButtonState extends State<LaunchButton> {
wrapProcess: false, wrapProcess: false,
name: "${version.name}-${host ? 'HOST' : 'GAME'}" name: "${version.name}-${host ? 'HOST' : 'GAME'}"
); );
gameProcess.stdOutput.listen((line) => _onGameOutput(line, version, host, virtualDesktop, false)); void onGameOutput(String line, bool error) {
gameProcess.stdError.listen((line) => _onGameOutput(line, version, host, virtualDesktop, true)); log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
watchProcess(gameProcess.pid).then((_) async {
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version, virtualDesktop),
onDisplayAttached: () => _onDisplayAttached(headless, virtualDesktop, version)
);
}
gameProcess.stdOutput.listen((line) => onGameOutput(line, false));
gameProcess.stdError.listen((line) => onGameOutput(line, true));
gameProcess.exitCode.then((_) async {
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance == null) { if(instance == null) {
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
return; return;
} }
if(!host || !headless || instance.launched) { if(!host || instance.launched) {
_onStop(reason: _StopReason.exitCode); log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): stop signal");
_onStop(
reason: _StopReason.exitCode,
host: host
);
return; return;
} }
await _restartGameServer(version, virtualDesktop, _StopReason.exitCode); log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): restart signal");
instance.launched = true;
await _onStop(
reason: _StopReason.exitCode,
host: true
);
await _toggle(
forceGUI: true,
host: true
);
}); });
return gameProcess.pid; return gameProcess.pid;
} }
Future<void> _restartGameServer(FortniteVersion version, bool virtualDesktop, _StopReason reason) async {
if (widget.host) {
await _onStop(reason: reason);
_toggle(forceGUI: true);
} else {
await _onStop(reason: reason, host: true);
final linkedHostingInstance =
await _startMatchMakingServer(version, false, virtualDesktop, true);
_gameController.instance.value?.child = linkedHostingInstance;
if (linkedHostingInstance != null) {
_setStarted(true, true);
_showLaunchingGameServerWidget();
}
}
}
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async { Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
if (file == null) { if (file == null) {
return null; return null;
@@ -314,20 +310,80 @@ class _LaunchButtonState extends State<LaunchButton> {
return pid; return pid;
} }
void _onGameOutput(String line, FortniteVersion version, bool host, bool virtualDesktop, bool error) async { Future<void> _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async {
if (line.contains(kShutdownLine)) { if(!headless && virtualDesktop) {
_onStop( final hostingInstance = _hostingController.instance.value;
reason: _StopReason.normal if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
try {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
var success = false;
try {
success = await windowManager.moveWindowToDesktop(
hostingInstance.gamePid,
_virtualDesktop!,
excludedWindowName: "Reboot"
); );
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ }catch(error) {
_onStop( log("[VIRTUAL_DESKTOP] $error");
reason: _StopReason.corruptedVersionError success = false;
}
if(!success) {
try {
windowManager.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}finally {
_virtualDesktop = null;
}
}
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}
}
}
}
void _onMatchEnd(FortniteVersion version, bool virtualDesktop) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
title: translations.gameServerEnd,
body: translations.gameServerRestart(_kRebootDelay.inSeconds),
); );
}else if(kCannotConnectErrors.any((element) => line.contains(element))){ notification.show();
_onStop( Future.delayed(_kRebootDelay).then((_) async {
reason: _StopReason.tokenError log("[RESTARTER] Stopping server...");
await _onStop(
reason: _StopReason.normal,
host: true
); );
}else if(kLoggedInLines.every((entry) => line.contains(entry))) { log("[RESTARTER] Stopped server");
log("[RESTARTER] Starting server...");
await _toggle(
host: true
);
log("[RESTARTER] Started server");
});
}else {
final notification = LocalNotification(
title: translations.gameServerEnd,
body: translations.gameServerShutdown(_kRebootDelay.inSeconds)
);
notification.show();
Future.delayed(_kRebootDelay).then((_) {
log("[RESTARTER] Stopping server...");
_onStop(
reason: _StopReason.normal,
host: true
);
log("[RESTARTER] Stopped server");
});
}
}
Future<void> _onLoggedIn(bool host) async {
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
instance.launched = true; instance.launched = true;
@@ -345,51 +401,6 @@ class _LaunchButtonState extends State<LaunchButton> {
_onGameServerInjected(); _onGameServerInjected();
} }
} }
}else if(line.contains(kGameFinishedLine) && host) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
title: translations.gameServerEnd,
body: translations.gameServerRestart(_kRebootDelay.inSeconds),
);
notification.show();
Future.delayed(_kRebootDelay).then((_) {
_restartGameServer(version, virtualDesktop, _StopReason.normal);
});
}else {
final notification = LocalNotification(
title: translations.gameServerEnd,
body: translations.gameServerShutdown(_kRebootDelay.inSeconds)
);
notification.show();
Future.delayed(_kRebootDelay).then((_) {
_onStop(reason: _StopReason.normal, host: true);
});
}
}else if(line.contains(kDisplayInitializedLine) && host && virtualDesktop) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
try {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
try {
await windowManager.moveWindowToDesktop(hostingInstance.gamePid, _virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
try {
windowManager.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}finally {
_virtualDesktop = null;
}
}
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}
}
}
} }
void _onGameClientInjected() { void _onGameClientInjected() {
@@ -411,11 +422,11 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final gameServerPort = _settingsController.gameServerPort.text; final gameServerPort = _settingsController.gameServerPort.text;
_gameServerInfoBar?.close();
final localPingResult = await pingGameServer( final localPingResult = await pingGameServer(
"127.0.0.1:$gameServerPort", "127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2) timeout: const Duration(minutes: 2)
); );
_gameServerInfoBar?.close();
if (!localPingResult) { if (!localPingResult) {
showInfoBar( showInfoBar(
translations.gameServerStartWarning, translations.gameServerStartWarning,
@@ -424,7 +435,6 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
return; return;
} }
_backendController.joinLocalHost(); _backendController.joinLocalHost();
final accessible = await _checkGameServer(theme, gameServerPort); final accessible = await _checkGameServer(theme, gameServerPort);
if (!accessible) { if (!accessible) {
@@ -487,6 +497,20 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
await _operation?.cancel();
_operation = null;
await _backendController.worker?.cancel();
}
host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){
_hostingController.instance.value = null;
}else {
_gameController.instance.value = null;
}
if(_virtualDesktop != null) { if(_virtualDesktop != null) {
try { try {
final instance = VirtualDesktopManager.getInstance(); final instance = VirtualDesktopManager.getInstance();
@@ -496,20 +520,12 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
if(host == null) {
await _operation?.cancel();
_operation = null;
await _backendController.worker?.cancel();
}
host = host ?? widget.host;
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace"); log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}"); log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
if(host) { if(host) {
_hostingController.discardServer(); _hostingController.discardServer();
} }
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null) { if(instance != null) {
if(reason == _StopReason.normal) { if(reason == _StopReason.normal) {
instance.launched = true; instance.launched = true;
@@ -518,25 +534,21 @@ class _LaunchButtonState extends State<LaunchButton> {
instance.kill(); instance.kill();
final child = instance.child; final child = instance.child;
if(child != null) { if(child != null) {
_onStop( await _onStop(
reason: reason, reason: reason,
host: child.hosting host: child.hosting
); );
} }
if(host){
_hostingController.instance.value = null;
}else {
_gameController.instance.value = null;
}
} }
_setStarted(host, false); _setStarted(host, false);
if(host) { WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
}else { }else {
_gameClientInfoBar?.close(); _gameClientInfoBar?.close();
} }
});
switch(reason) { switch(reason) {
case _StopReason.backendError: case _StopReason.backendError:
@@ -558,7 +570,6 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.exitCode: case _StopReason.exitCode:
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
showInfoBar( showInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError,
@@ -566,7 +577,6 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
} }
break; break;
case _StopReason.corruptedVersionError: case _StopReason.corruptedVersionError:
showInfoBar( showInfoBar(
@@ -584,7 +594,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
showInfoBar( showInfoBar(
translations.tokenError, translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
); );
@@ -622,6 +632,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}..."); log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
await injectDll(gameProcess, dllPath); await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}"); log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
} catch (error, stackTrace) { } catch (error, stackTrace) {
log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace"); log("[${hosting ? 'HOST' : 'GAME'}] Cannot inject ${injectable.name}: $error $stackTrace");

View File

@@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
class ServerTypeSelector extends StatefulWidget { class ServerTypeSelector extends StatefulWidget {
@@ -18,6 +19,7 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Obx(() => DropDownButton( return Obx(() => DropDownButton(
onOpen: () => inDialog = true,
leading: Text(_controller.type.value.label), leading: Text(_controller.type.value.label),
items: ServerType.values items: ServerType.values
.map((type) => _createItem(type)) .map((type) => _createItem(type))

View File

@@ -14,6 +14,7 @@ import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/add_local_version.dart'; import 'package:reboot_launcher/src/widget/add_local_version.dart';
import 'package:reboot_launcher/src/widget/add_server_version.dart'; import 'package:reboot_launcher/src/widget/add_server_version.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget { class VersionSelector extends StatefulWidget {
@@ -44,7 +45,13 @@ class _VersionSelectorState extends State<VersionSelector> {
child: FlyoutTarget( child: FlyoutTarget(
controller: _flyoutController, controller: _flyoutController,
child: DropDownButton( child: DropDownButton(
leading: Text(_gameController.selectedVersion?.name ?? translations.selectVersion), onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_gameController.selectedVersion?.name ?? translations.selectVersion,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
items: _createSelectorItems(context) items: _createSelectorItems(context)
), ),
) )

View File

@@ -10,5 +10,11 @@ SettingTile get versionSelectSettingTile => SettingTile(
), ),
title: Text(translations.selectFortniteName), title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription), subtitle: Text(translations.selectFortniteDescription),
content: const VersionSelector() contentWidth: null,
content: ConstrainedBox(
constraints: BoxConstraints(
minWidth: SettingTile.kDefaultContentWidth,
),
child: const VersionSelector()
)
); );

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "9.1.0" version: "9.1.3"
publish_to: 'none' publish_to: 'none'

View File

@@ -10,7 +10,7 @@ AppPublisher={{PUBLISHER_NAME}}
AppPublisherURL={{PUBLISHER_URL}} AppPublisherURL={{PUBLISHER_URL}}
AppSupportURL={{PUBLISHER_URL}} AppSupportURL={{PUBLISHER_URL}}
AppUpdatesURL={{PUBLISHER_URL}} AppUpdatesURL={{PUBLISHER_URL}}
DefaultDirName={autopf}\{{DISPLAY_NAME}}; DefaultDirName={autopf}\{{DISPLAY_NAME}}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
OutputBaseFilename={{OUTPUT_BASE_FILENAME}} OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
Compression=zip Compression=zip
@@ -28,8 +28,11 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Dirs]
Name: "{app}"; Permissions: everyone-full
[Files] [Files]
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full
[Run] [Run]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden