This commit is contained in:
Alessandro Autiero
2024-12-30 19:13:08 +01:00
parent 9e20ec86e6
commit d5e41ed646
49 changed files with 638 additions and 1070 deletions

View File

@@ -1,13 +1,25 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:clipboard/clipboard.dart';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.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/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
class BackendController extends GetxController {
static const String storageName = "v2_backend_storage";
@@ -22,6 +34,7 @@ class BackendController extends GetxController {
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started;
late final RxBool detached;
late final List<InfoBarEntry> _infoBars;
StreamSubscription? worker;
int? embeddedProcessPid;
HttpServer? localServer;
@@ -70,31 +83,30 @@ class BackendController extends GetxController {
}
});
gameServerAddressFocusNode = FocusNode();
consoleKey = Rx(_readConsoleKey());
consoleKey = Rx(() {
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;
}());
_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;
_infoBars = [];
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
@@ -103,6 +115,21 @@ class BackendController extends GetxController {
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
}
String _readHost() {
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
if (type.value != ServerType.remote) {
return kDefaultBackendHost;
}
return "";
}
String _readPort() => _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
void joinLocalhost() {
gameServerAddress.text = kDefaultGameServerHost;
}
@@ -121,22 +148,44 @@ class BackendController extends GetxController {
detached.value = false;
}
String _readHost() {
String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
Future<bool> toggleInteractive() async {
_cancel();
final stream = started.value ? stop() : start(
onExit: () {
_cancel();
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
},
onError: (errorMessage) {
_cancel();
_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) {
entry?.close();
entry = _handeEvent(event);
if(event.type.isError) {
completer.complete(false);
}else if(event.type.isSuccess) {
completer.complete(true);
}
});
if (type.value != ServerType.remote) {
return kDefaultBackendHost;
}
return "";
return await completer.future;
}
String _readPort() =>
_storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
try {
if(started.value) {
@@ -286,14 +335,267 @@ class BackendController extends GetxController {
}
}
Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* {
if(started()) {
yield* stop();
}else {
yield* start(
onExit: onExit,
onError: onError
);
void _cancel() {
worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
InfoBarEntry _handeEvent(ServerResult event) {
log("[BACKEND] Handling event: $event");
switch (event.type) {
case ServerResultType.starting:
return _showRebootInfoBar(
translations.startingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.startSuccess:
return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.startError:
print(event.stackTrace);
return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.stopping:
return _showRebootInfoBar(
translations.stoppingServer,
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.stopSuccess:
return _showRebootInfoBar(
translations.stoppedServer,
severity: InfoBarSeverity.success
);
case ServerResultType.stopError:
return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.missingHostError:
return _showRebootInfoBar(
translations.missingHostNameError,
severity: InfoBarSeverity.error
);
case ServerResultType.missingPortError:
return _showRebootInfoBar(
translations.missingPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.illegalPortError:
return _showRebootInfoBar(
translations.illegalPortError,
severity: InfoBarSeverity.error
);
case ServerResultType.freeingPort:
return _showRebootInfoBar(
translations.freeingPort,
loading: true,
duration: null
);
case ServerResultType.freePortSuccess:
return _showRebootInfoBar(
translations.freedPort,
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
case ServerResultType.freePortError:
return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration
);
case ServerResultType.pingingRemote:
return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingingLocal:
return _showRebootInfoBar(
translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info,
loading: true,
duration: null
);
case ServerResultType.pingError:
return _showRebootInfoBar(
translations.pingError(type.value.name),
severity: InfoBarSeverity.error
);
}
}
Future<void> joinServer(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) {
_showRebootInfoBar(
translations.joinSelfServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final version = Get.find<GameController>()
.getVersionByName(server.version.toString());
if(version == null) {
_showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final hashedPassword = server.password;
final hasPassword = hashedPassword != null;
final embedded = type.value == ServerType.embedded;
final author = server.author;
final encryptedIp = server.ip;
if(!hasPassword) {
final valid = await _isServerValid(encryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, encryptedIp, author, version);
return;
}
final confirmPassword = await _askForPassword();
if(confirmPassword == null) {
return;
}
if(!checkPassword(confirmPassword, hashedPassword)) {
_showRebootInfoBar(
translations.wrongServerPassword,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return;
}
final decryptedIp = aes256Decrypt(encryptedIp, confirmPassword);
final valid = await _isServerValid(decryptedIp);
if(!valid) {
return;
}
_onServerJoined(embedded, decryptedIp, author, version);
}
Future<bool> _isServerValid(String address) async {
final result = await pingGameServer(address);
if(result) {
return true;
}
_showRebootInfoBar(
translations.offlineServer,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
Future<String?> _askForPassword() async {
final confirmPasswordController = TextEditingController();
final showPassword = RxBool(false);
final showPasswordTrailing = RxBool(false);
return await showRebootDialog<String?>(
builder: (context) => FormDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.serverPassword,
child: Obx(() => TextFormBox(
placeholder: translations.serverPasswordPlaceholder,
controller: confirmPasswordController,
autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value,
enableSuggestions: false,
autofocus: true,
autocorrect: false,
onChanged: (text) => showPasswordTrailing.value = text.isNotEmpty,
suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle(
shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: WidgetStateProperty.all(Colors.transparent)
),
child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
),
)
))
),
const SizedBox(height: 8.0)
],
),
buttons: [
DialogButton(
text: translations.serverPasswordCancel,
type: ButtonType.secondary
),
DialogButton(
text: translations.serverPasswordConfirm,
type: ButtonType.primary,
onTap: () => Navigator.of(context).pop(confirmPasswordController.text)
)
]
)
);
}
void _onServerJoined(bool embedded, String decryptedIp, String author, FortniteVersion version) {
if(embedded) {
gameServerAddress.text = decryptedIp;
pageIndex.value = RebootPageType.play.index;
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>()
.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,
severity: InfoBarSeverity.success
));
}
InfoBarEntry _showRebootInfoBar(dynamic text, {
InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action
}) {
final result = showRebootInfoBar(
text,
severity: severity,
loading: loading,
duration: duration,
onDismissed: onDismissed,
action: action
);
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
_infoBars.add(result);
}
return result;
}
}

View File

@@ -7,9 +7,10 @@ import 'package:get_storage/get_storage.dart';
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/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:version/version.dart';
import 'package:path/path.dart' as path;
class DllController extends GetxController {
static const String storageName = "v2_dll_storage";
@@ -19,6 +20,7 @@ class DllController extends GetxController {
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 beforeS20Mirror;
@@ -27,13 +29,13 @@ class DllController extends GetxController {
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
gameServerDll = _createController("game_server", InjectableDll.gameServer);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.starfall);
backendDll = _createController("backend", InjectableDll.auth);
memoryLeakDll = _createController("memory_leak", InjectableDll.memoryLeak);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
@@ -57,9 +59,9 @@ class DllController extends GetxController {
}
void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
gameServerDll.text = getDefaultDllPath(InjectableDll.gameServer);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.starfall);
backendDll.text = getDefaultDllPath(InjectableDll.auth);
}
void resetServer() {
@@ -74,16 +76,6 @@ class DllController extends GetxController {
}
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;
@@ -148,15 +140,13 @@ class DllController extends GetxController {
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.reboot:
case InjectableDll.gameServer:
if(customGameServer.value) {
return (File(gameServerDll.text), true);
}
@@ -165,25 +155,47 @@ class DllController extends GetxController {
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.starfall:
case InjectableDll.auth:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memoryLeak:
final memoryFile = File(memoryLeakDll.text);
return (memoryFile, canonicalize(memoryFile.path) != defaultPath);
}
}
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return unrealEngineConsoleDll;
case InjectableDll.auth:
return backendDll;
case InjectableDll.gameServer:
return gameServerDll;
case InjectableDll.memoryLeak:
return memoryLeakDll;
}
}
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $filePath(silent: $silent)");
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
String getDefaultDllPath(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\starfall.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $dll at $filePath(silent: $silent, force: $force)");
InfoBarEntry? entry;
try {
if (fileName.contains("reboot")) {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
);
if (dll == InjectableDll.gameServer) {
return await updateGameServerDll(silent: silent);
}
if(!force && File(filePath).existsSync()) {
@@ -199,7 +211,7 @@ class DllController extends GetxController {
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
await downloadDependency(dll, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
@@ -218,13 +230,13 @@ class DllController extends GetxController {
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), fileName),
translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
await download(dll, filePath, silent: silent, force: force);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
@@ -234,6 +246,32 @@ class DllController extends GetxController {
return false;
}
}
void guardFiles() {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if (path.equals(controller.text, defaultPath)) {
download(injectable, controller.text);
}
controller.addListener(() async {
try {
if (!path.equals(controller.text, defaultPath)) {
return;
}
final filePath = controller.text;
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (path.equals(event.path, filePath)) {
await download(injectable, filePath);
}
}
} catch(_) {
// Ignore
}
});
}
}
}
extension _UpdateTimerExtension on UpdateTimer {

View File

@@ -6,7 +6,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
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/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:version/version.dart';