Released 9.2.5

This commit is contained in:
Alessandro Autiero
2024-08-18 20:29:09 +02:00
parent 582270849e
commit 4c3fe9bc65
21 changed files with 503 additions and 383 deletions

View File

@@ -118,9 +118,7 @@
"settingsUtilsResetDefaultsName": "Reset settings",
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values",
"settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
"settingsUtilsResetDefaultsContent": "Reset",
"settingsUtilsDialogSecondaryAction": "Close",
"settingsUtilsDialogPrimaryAction": "Reset",
"selectFortniteName": "Fortnite version",
"selectFortniteDescription": "Select the version of Fortnite you want to use",
"manageVersionsName": "Manage versions",
@@ -262,6 +260,7 @@
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
"fortniteCrashError": "The {name} crashed after being launched",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available",
"noServerFound": "No server found: invalid or expired link",
"settingsUtilsThemeName": "Theme",
@@ -321,6 +320,7 @@
"none": "none",
"openLog": "Open log",
"backendProcessError": "The backend shut down unexpectedly",
"backendErrorMessage": "The backend reported an unexpected error",
"welcomeTitle": "Welcome to Reboot Launcher",
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
"welcomeAction": "Take the tour",
@@ -364,5 +364,8 @@
"promptSettingsTabActionLabel": "Done",
"automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!",
"automaticGameServerDialogIgnore": "Ignore",
"automaticGameServerDialogStart": "Start server"
"automaticGameServerDialogStart": "Start server",
"gameResetDefaultsName": "Reset",
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset"
}

View File

@@ -12,6 +12,7 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_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/settings_controller.dart';
@@ -188,10 +189,11 @@ void _initWindow() => doWhenWindowReady(() async {
Future<List<Object>> _initStorage() async {
final errors = <Object>[];
try {
await GetStorage("game_storage", settingsDirectory.path).initStorage;
await GetStorage("backend_storage", settingsDirectory.path).initStorage;
await GetStorage("settings_storage", settingsDirectory.path).initStorage;
await GetStorage("hosting_storage", settingsDirectory.path).initStorage;
await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
}catch(error) {
appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
@@ -223,6 +225,12 @@ Future<List<Object>> _initStorage() async {
errors.add(error);
}
try {
Get.put(DllController());
}catch(error) {
errors.add(error);
}
return errors;
}

View File

@@ -2,18 +2,24 @@ import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
class BackendController extends GetxController {
late final GetStorage? storage;
static const String storageName = "backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage;
late final TextEditingController host;
late final TextEditingController port;
late final Rx<ServerType> type;
late final TextEditingController gameServerAddress;
late final FocusNode gameServerAddressFocusNode;
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started;
late final RxBool detached;
StreamSubscription? worker;
@@ -22,13 +28,13 @@ class BackendController extends GetxController {
HttpServer? remoteServer;
BackendController() {
storage = appWithNoStorage ? null : GetStorage("backend_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
started = RxBool(false);
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
port.text = _readPort();
storage?.write("type", value.index);
_storage?.write("type", value.index);
if (!started.value) {
return;
}
@@ -37,13 +43,13 @@ class BackendController extends GetxController {
});
host = TextEditingController(text: _readHost());
host.addListener(() =>
storage?.write("${type.value.name}_host", host.text));
_storage?.write("${type.value.name}_host", host.text));
port = TextEditingController(text: _readPort());
port.addListener(() =>
storage?.write("${type.value.name}_port", port.text));
detached = RxBool(storage?.read("detached") ?? false);
detached.listen((value) => storage?.write("detached", value));
final address = storage?.read("game_server_address");
_storage?.write("${type.value.name}_port", port.text));
detached = RxBool(_storage?.read("detached") ?? false);
detached.listen((value) => _storage?.write("detached", value));
final address = _storage?.read("game_server_address");
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue);
@@ -55,7 +61,7 @@ class BackendController extends GetxController {
lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
storage?.write("game_server_address", newValue);
_storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue);
});
watchMatchmakingIp().listen((event) {
@@ -64,6 +70,37 @@ class BackendController extends GetxController {
}
});
gameServerAddressFocusNode = FocusNode();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
await defaultInput.parent.create(recursive: true);
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
}
void joinLocalhost() {
@@ -73,18 +110,19 @@ class BackendController extends GetxController {
void reset() async {
type.value = ServerType.values.elementAt(0);
for (final type in ServerType.values) {
storage?.write("${type.name}_host", null);
storage?.write("${type.name}_port", null);
_storage?.write("${type.name}_host", null);
_storage?.write("${type.name}_port", null);
}
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
port.text = kDefaultBackendPort.toString();
gameServerAddress.text = "127.0.0.1";
consoleKey.value = _kDefaultConsoleKey;
detached.value = false;
}
String _readHost() {
String? value = storage?.read("${type.value.name}_host");
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
@@ -97,9 +135,9 @@ class BackendController extends GetxController {
}
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({required void Function() onExit, required void Function(String) onError}) async* {
try {
if(started.value) {
return;
@@ -144,7 +182,18 @@ class BackendController extends GetxController {
switch(serverType){
case ServerType.embedded:
final process = await startEmbeddedBackend(detached.value);
final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
if(started.value) {
started.value = false;
onError(errorMessage);
}
});
watchProcess(process.pid).then((_) {
if(started.value) {
started.value = false;
onExit();
}
});
embeddedProcessPid = process.pid;
break;
case ServerType.remote:
@@ -237,11 +286,14 @@ class BackendController extends GetxController {
}
}
Stream<ServerResult> toggle() async* {
Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* {
if(started()) {
yield* stop();
}else {
yield* start();
yield* start(
onExit: onExit,
onError: onError
);
}
}
}

View File

@@ -0,0 +1,265 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class DllController extends GetxController {
static const String storageName = "dll_storage";
late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController url;
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Map<String, Future<bool>> _operations;
late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.cobalt);
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
_operations = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void resetGame() {
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
}
void resetServer() {
gameServerPort.text = kDefaultGameServerPort;
timer.value = UpdateTimer.hour;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
timestamp.value = null;
updateGameServerDll();
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateGameServerDll(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateGameServerDll(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateGameServerDll(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(_getDefaultPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
log("[DLL] Asking for $filePath(silent: $silent)");
final old = _operations[filePath];
if(old != null) {
log("[DLL] Download task already exists");
return old;
}
log("[DLL] Creating new download task...");
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
);
}
if(File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}finally {
_operations.remove(fileName);
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -1,18 +1,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import '../../main.dart';
import 'package:reboot_launcher/main.dart';
class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
static const String storageName = "game_storage";
late final GetStorage? _storage;
late final TextEditingController username;
@@ -22,10 +18,9 @@ class GameController extends GetxController {
late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started;
late final Rxn<GameInstance> instance;
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() {
_storage = appWithNoStorage ? null : GetStorage("game_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
@@ -44,37 +39,6 @@ class GameController extends GetxController {
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
instance = Rxn();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
await defaultInput.parent.create(recursive: true);
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
}
void reset() {
@@ -82,6 +46,7 @@ class GameController extends GetxController {
password.text = "";
customLaunchArgs.text = "";
versions.value = [];
_selectedVersion.value = null;
instance.value = null;
}

View File

@@ -12,6 +12,8 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart';
class HostingController extends GetxController {
static const String storageName = "hosting_storage";
late final GetStorage? _storage;
late final String uuid;
late final TextEditingController name;
@@ -32,7 +34,7 @@ class HostingController extends GetxController {
late final Semaphore _semaphore;
HostingController() {
_storage = appWithNoStorage ? null : GetStorage("hosting_storage");
_storage = appWithNoStorage ? null : GetStorage(storageName);
uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage?.write("uuid", uuid);
name = TextEditingController(text: _storage?.read("name"));
@@ -138,10 +140,10 @@ class HostingController extends GetxController {
description.text = "";
showPassword.value = false;
discoverable.value = false;
started.value = false;
instance.value = null;
type.value = GameServerType.headless;
autoRestart.value = true;
customLaunchArgs.text = "";
}
FortniteServer? findServerById(String uuid) {

View File

@@ -15,37 +15,19 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class SettingsController extends GetxController {
static const String storageName = "settings_storage";
late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final RxString language;
late final Rx<ThemeMode> themeMode;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Rx<UpdateTimer> timer;
late final TextEditingController url;
late final RxBool customGameServer;
late final RxBool firstRun;
late final Map<String, Future<bool>> _operations;
late double width;
late double height;
late double? offsetX;
late double? offsetY;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
SettingsController() {
_storage = appWithNoStorage ? null : GetStorage("settings_storage");
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.cobalt);
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
_storage = appWithNoStorage ? null : GetStorage(storageName);
width = _storage?.read("width") ?? kDefaultWindowWidth;
height = _storage?.read("height") ?? kDefaultWindowHeight;
offsetX = _storage?.read("offset_x");
@@ -54,25 +36,8 @@ class SettingsController extends GetxController {
themeMode.listen((value) => _storage?.write("theme", value.index));
language = RxString(_storage?.read("language") ?? currentLocale);
language.listen((value) => _storage?.write("language", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
_operations = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void saveWindowSize(Size size) {
@@ -87,32 +52,18 @@ class SettingsController extends GetxController {
_storage?.write("offset_y", offsetY);
}
void reset(){
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
gameServerPort.text = kDefaultGameServerPort;
timestamp.value = null;
timer.value = UpdateTimer.never;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
updateReboot();
}
Future<void> notifyLauncherUpdate() async {
if(appVersion == null) {
if (appVersion == null) {
return;
}
final pubspec = await _getPubspecYaml();
if(pubspec == null) {
if (pubspec == null) {
return;
}
final latestVersion = Version.parse(pubspec["version"]);
if(latestVersion <= appVersion) {
if (latestVersion <= appVersion) {
return;
}
@@ -125,7 +76,8 @@ class SettingsController extends GetxController {
child: Text(translations.updateAvailableAction),
onPressed: () {
infoBar.close();
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases"));
launchUrl(Uri.parse(
"https://github.com/Auties00/reboot_launcher/releases"));
},
)
);
@@ -133,201 +85,16 @@ class SettingsController extends GetxController {
Future<dynamic> _getPubspecYaml() async {
try {
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if(pubspecResponse.statusCode != 200) {
final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) {
return null;
}
return loadYaml(pubspecResponse.body);
}catch(error) {
} catch (error) {
log("[UPDATER] Cannot check for updates: $error");
return null;
}
}
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateReboot(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateReboot(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(_getDefaultPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
log("[DLL] Asking for $filePath(silent: $silent)");
final old = _operations[filePath];
if(old != null) {
log("[DLL] Download task already exists");
return old;
}
log("[DLL] Creating new download task...");
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateReboot(
silent: silent
);
}
if(File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}finally {
_operations.remove(fileName);
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -15,6 +15,7 @@ import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
final List<InfoBarEntry> _infoBars = [];
@@ -27,7 +28,27 @@ extension ServerControllerDialog on BackendController {
Future<bool> toggleInteractive() async {
cancelInteractive();
final stream = toggle();
final stream = toggle(
onExit: () {
cancelInteractive();
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
},
onError: (errorMessage) {
cancelInteractive();
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
}
);
final completer = Completer<bool>();
InfoBarEntry? entry;
worker = stream.listen((event) {
@@ -54,19 +75,6 @@ extension ServerControllerDialog on BackendController {
duration: null
);
case ServerResultType.startSuccess:
final embeddedProcessPid = this.embeddedProcessPid;
if(embeddedProcessPid != null) {
watchProcess(embeddedProcessPid).then((_) {
if(started.value) {
started.value = false;
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
}
});
}
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success

View File

@@ -43,7 +43,6 @@ class BackendPage extends RebootPage {
}
class _BackendPageState extends RebootPageState<BackendPage> {
final GameController _gameController = Get.find<GameController>();
final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry;
@@ -56,7 +55,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
}
if(keyEvent.physicalKey.isUnrealEngineKey) {
_gameController.consoleKey.value = keyEvent.physicalKey;
_backendController.consoleKey.value = keyEvent.physicalKey;
}
_infoBarEntry?.close();
@@ -194,7 +193,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
duration: null
);
},
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""),
child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""),
),
)
);

View File

@@ -10,6 +10,7 @@ import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_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/messenger/abstract/dialog.dart';
@@ -44,6 +45,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
final BackendController _backendController = Get.find<BackendController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController();
@@ -134,9 +136,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}
for(final injectable in InjectableDll.values) {
final (file, custom) = _settingsController.getInjectableData(injectable);
final (file, custom) = _dllController.getInjectableData(injectable);
if(!custom) {
_settingsController.downloadCriticalDllInteractive(
_dllController.downloadCriticalDllInteractive(
file.path,
silent: true
);
@@ -144,7 +146,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
_settingsController.downloadCriticalDllInteractive(filePath);
_dllController.downloadCriticalDllInteractive(filePath);
}));
}

View File

@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_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/settings_controller.dart';
@@ -53,6 +54,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -257,7 +259,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: 64,
content: TextFormBox(
placeholder: translations.settingsServerPortName,
controller: _settingsController.gameServerPort,
controller: _dllController.gameServerPort,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
inputFormatters: [
@@ -284,22 +286,22 @@ class _HostingPageState extends RebootPageState<HostPage> {
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _settingsController.customGameServer.value;
final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_settingsController.customGameServer.value = entry.key;
_settingsController.infoBarEntry?.close();
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_settingsController.updateReboot(
_dllController.updateGameServerDll(
force: true
);
}
@@ -308,18 +310,18 @@ class _HostingPageState extends RebootPageState<HostPage> {
))
),
Obx(() {
if(!_settingsController.customGameServer.value) {
if(!_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return createFileSetting(
title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription,
controller: _settingsController.gameServerDll
controller: _dllController.gameServerDll
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
@@ -331,13 +333,13 @@ class _HostingPageState extends RebootPageState<HostPage> {
subtitle: Text(translations.settingsServerMirrorDescription),
content: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _settingsController.url,
controller: _dllController.url,
validator: _checkUpdateUrl
)
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
@@ -350,13 +352,13 @@ class _HostingPageState extends RebootPageState<HostPage> {
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.timer.value.text),
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_settingsController.timer.value = entry;
_settingsController.infoBarEntry?.close();
_settingsController.updateReboot(
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);
}
@@ -431,7 +433,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.hostResetName),
subtitle: Text(translations.hostResetDescription),
content: Button(
onPressed: () => showResetDialog(_hostingController.reset),
onPressed: () => showResetDialog(() {
_hostingController.reset();
_dllController.resetServer();
}),
child: Text(translations.hostResetContent),
)
);

View File

@@ -1,9 +1,11 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
@@ -37,7 +39,8 @@ class PlayPage extends RebootPage {
class _PlayPageState extends RebootPageState<PlayPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget build(BuildContext context) {
return Column(
@@ -94,6 +97,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
),
_options,
_internalFiles,
_resetDefaults
];
SettingTile get _internalFiles => SettingTile(
@@ -106,17 +110,17 @@ class _PlayPageState extends RebootPageState<PlayPage> {
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _settingsController.unrealEngineConsoleDll
controller: _dllController.unrealEngineConsoleDll
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _settingsController.backendDll
controller: _dllController.backendDll
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _settingsController.memoryLeakDll
controller: _dllController.memoryLeakDll
),
],
);
@@ -141,4 +145,19 @@ class _PlayPageState extends RebootPageState<PlayPage> {
)
]
);
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.gameResetDefaultsName),
subtitle: Text(translations.gameResetDefaultsDescription),
content: Button(
onPressed: () => showResetDialog(() {
_gameController.reset();
_dllController.resetGame();
}),
child: Text(translations.gameResetDefaultsContent),
)
);
}

View File

@@ -42,7 +42,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
List<Widget> get settings => [
_language,
_theme,
_resetDefaults,
_installationDirectory
];
@@ -88,18 +87,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
)).toList()
))
);
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.settingsUtilsResetDefaultsName),
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
content: Button(
onPressed: () => showResetDialog(_settingsController.reset),
child: Text(translations.settingsUtilsResetDefaultsContent),
)
);
SettingTile get _installationDirectory => SettingTile(
icon: Icon(

View File

@@ -9,6 +9,7 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_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/settings_controller.dart';
@@ -39,7 +40,7 @@ class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
@@ -263,15 +264,21 @@ class _LaunchButtonState extends State<LaunchButton> {
"OPENSSL_ia32cap": "~0x20000000"
}
);
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
onBuildCorrupted: () {
if(instance?.launched == false) {
_onStop(reason: _StopReason.corruptedVersionError);
}else {
_onStop(reason: _StopReason.crash);
}
},
onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
@@ -391,7 +398,7 @@ class _LaunchButtonState extends State<LaunchButton> {
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
}else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text);
final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) {
await killProcessByPort(gameServerPort);
}
@@ -424,7 +431,7 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true,
duration: null
);
final gameServerPort = _settingsController.gameServerPort.text;
final gameServerPort = _dllController.gameServerPort.text;
final localPingResult = await pingGameServer(
"127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2)
@@ -605,6 +612,7 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.tokenError:
_backendController.stop();
showRebootInfoBar(
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none),
severity: InfoBarSeverity.error,
@@ -615,6 +623,13 @@ class _LaunchButtonState extends State<LaunchButton> {
)
);
break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError:
showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError),
@@ -664,7 +679,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _settingsController.getInjectableData(injectable);
final (file, customDll) = _dllController.getInjectableData(injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) {
log("[${host ? 'HOST' : 'GAME'}] Path exists");
@@ -678,7 +693,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _settingsController.downloadCriticalDllInteractive(file.path);
await _dllController.downloadCriticalDllInteractive(file.path);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(injectable, host, true);
}
@@ -731,7 +746,8 @@ enum _StopReason {
matchmakerError,
tokenError,
unknownError,
exitCode;
exitCode,
crash;
bool get isError => name.contains("Error");
}