9 Commits
9.2.4 ... 9.2.6

Author SHA1 Message Date
Alessandro Autiero
62dae468bf Merge pull request #98 from Auties00/_onLoggedIn
Released 9.2.6
2024-09-12 17:49:12 +02:00
Alessandro Autiero
a9af28273a Released 9.2.6 2024-09-12 15:46:24 +02:00
Alessandro Autiero
232bf8fbfc Update README.md 2024-08-18 22:35:17 +02:00
Alessandro Autiero
a787c4efc9 Merge pull request #86 from Auties00/_onLoggedIn
Release 9.2.5
2024-08-18 22:34:36 +02:00
Alessandro Autiero
4c3fe9bc65 Released 9.2.5 2024-08-18 20:29:09 +02:00
Alessandro Autiero
3f88d5ed80 Create .gitattributes 2024-07-31 11:54:02 +02:00
Alessandro Autiero
582270849e Released 9.2.4 2024-07-10 15:40:52 +02:00
Alessandro Autiero
1ef4e76768 Small fix to display errors and warnings from backend 2024-07-10 15:19:20 +02:00
Alessandro Autiero
cd8c8e6dd9 Release 9.2.3 2024-07-10 15:11:49 +02:00
30 changed files with 917 additions and 568 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
backend/**/* linguist-vendored

View File

@@ -8,6 +8,8 @@ Join our discord at https://discord.gg/reboot
- COMMON: Shared business logic for CLI and GUI modules - COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart - 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 - GUI: Stable graphical user interface to play and host Fortnite S0-14
![image](https://github.com/user-attachments/assets/7ff5d49e-8920-41ad-a805-188d84ad6ec4)
## Installation ## Installation

2
backend/index.js vendored
View File

@@ -35,7 +35,7 @@ express.use(require("./structure/matchmaking.js"));
express.use(require("./structure/cloudstorage.js")); express.use(require("./structure/cloudstorage.js"));
express.use(require("./structure/mcp.js")); express.use(require("./structure/mcp.js"));
const port = process.env.PORT || 3551; const port = 3551;
express.listen(port, () => { express.listen(port, () => {
console.log("LawinServer started listening on port", port); console.log("LawinServer started listening on port", port);

View File

@@ -11,7 +11,7 @@ const List<String> kCorruptedBuildErrors = [
"Critical error", "Critical error",
"when 0 bytes remain", "when 0 bytes remain",
"Pak chunk signature verification failed!", "Pak chunk signature verification failed!",
"Couldn't find pak signature file" "LogWindows:Error: Fatal error!"
]; ];
const List<String> kCannotConnectErrors = [ const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused", "port 3551 failed: Connection refused",

View File

@@ -9,9 +9,22 @@ extension FortniteVersionExtension on FortniteVersion {
static File? findFile(Directory directory, String name) { static File? findFile(Directory directory, String name) {
try{ try{
final result = directory.listSync(recursive: true) for(final child in directory.listSync()) {
.firstWhere((element) => path.basename(element.path) == name); if(child is Directory) {
return File(result.path); if(!path.basename(child.path).startsWith("\.")) {
final result = findFile(child, name);
if(result != null) {
return result;
}
}
}else if(child is File) {
if(path.basename(child.path) == name) {
return child;
}
}
}
return null;
}catch(_){ }catch(_){
return null; return null;
} }

View File

@@ -15,10 +15,19 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp; String? _lastIp;
String? _lastPort; String? _lastPort;
Future<Process> startEmbeddedBackend(bool detached) async => startProcess( Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
final process = await startProcess(
executable: backendStartExecutable, executable: backendStartExecutable,
window: detached, window: detached,
); );
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
process.stdError.listen((error) {
log("[BACKEND] Error: $error");
onError?.call(error);
});
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
return process;
}
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort); Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);

View File

@@ -7,7 +7,7 @@ final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1); final Semaphore _semaphore = Semaphore(1);
File _createLoggingFile() { File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log"); final file = File("${installationDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true); file.parent.createSync(recursive: true);
if(file.existsSync()) { if(file.existsSync()) {
file.deleteSync(); file.deleteSync();

View File

@@ -14,9 +14,6 @@ Directory get assetsDirectory {
return installationDirectory; return installationDirectory;
} }
Directory get logsDirectory =>
Directory("${installationDirectory.path}\\logs");
Directory get settingsDirectory => Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings"); Directory("${installationDirectory.path}\\settings");

View File

@@ -1,6 +1,7 @@
// ignore_for_file: non_constant_identifier_names // ignore_for_file: non_constant_identifier_names
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
@@ -9,6 +10,7 @@ import 'dart:math';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/log.dart';
import 'package:sync/semaphore.dart'; import 'package:sync/semaphore.dart';
import 'package:win32/win32.dart'; import 'package:win32/win32.dart';
@@ -100,58 +102,49 @@ Future<bool> startElevatedProcess({required String executable, required String a
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>(); shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput); return ShellExecuteEx(shellInput) == 1;
return shellResult == 1;
} }
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async { Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? []; final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) { if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process"); final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}/process.bat"); final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}'; final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true); await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start( final process = await Process.start(
tempScriptFile.path, tempScriptFile.path,
[], [],
workingDirectory: executable.parent.path, workingDirectory: workingDirectory,
environment: environment, environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window runInShell: window
); );
return _withLogger(name, executable, process, window); return _ExtendedProcess(process, true);
} }
final process = await Process.start( final process = await Process.start(
executable.path, executable.path,
args ?? [], args ?? [],
workingDirectory: executable.parent.path, workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window runInShell: window
); );
return _withLogger(name, executable, process, window); return _ExtendedProcess(process, true);
} }
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) { String? _getWorkingDirectory(File executable) {
final extendedProcess = _ExtendedProcess(process, true); try {
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log"); log("[PROCESS] Calculating working directory for $executable");
loggingFile.parent.createSync(recursive: true); final workingDirectory = executable.parent.resolveSymbolicLinksSync();
if(loggingFile.existsSync()) { log("[PROCESS] Using working directory: $workingDirectory");
loggingFile.deleteSync(); return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
} }
final semaphore = Semaphore(1);
void logEvent(String event) async {
await semaphore.acquire();
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
semaphore.release();
}
extendedProcess.stdOutput.listen(logEvent);
extendedProcess.stdError.listen(logEvent);
if(!window) {
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
}
return extendedProcess;
} }
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd), final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
@@ -203,47 +196,61 @@ Future<bool> watchProcess(int pid) async {
return await completer.future; return await completer.future;
} }
// TODO: Template List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) { log("[PROCESS] Generating reboot args");
if(password.isEmpty) { if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev'; username = '${_parseUsername(username, host)}@projectreboot.dev';
} }
password = password.isNotEmpty ? password : "Rebooted"; password = password.isNotEmpty ? password : "Rebooted";
final args = [ final args = LinkedHashMap<String, String>(
"-epicapp=Fortnite", equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
"-epicenv=Prod", hashCode: (a) => a.toUpperCase().hashCode
"-epiclocale=en-us", );
"-epicportal", args.addAll({
"-skippatchcheck", "-epicapp": "Fortnite",
"-nobe", "-epicenv": "Prod",
"-fromfl=eac", "-epiclocale": "en-us",
"-fltoken=3db3ba5dcbd2e16703f3978d", "-epicportal": "",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", "-skippatchcheck": "",
"-AUTH_LOGIN=$username", "-nobe": "",
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}", "-fromfl": "eac",
"-AUTH_TYPE=epic" "-fltoken": "3db3ba5dcbd2e16703f3978d",
]; "-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(log) { if(logging) {
args.add("-log"); args["-log"] = "";
} }
if(host) { if(host) {
args.addAll([ args["-nosplash"] = "";
"-nosplash", args["-nosound"] = "";
"-nosound"
]);
if(hostType == GameServerType.headless){ if(hostType == GameServerType.headless){
args.add("-nullrhi"); args["-nullrhi"] = "";
} }
} }
if(additionalArgs.isNotEmpty){ log("[PROCESS] Default args: $args");
args.addAll(additionalArgs.split(" ")); log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
} }
return args; log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
} }
void handleGameOutput({ void handleGameOutput({
@@ -257,16 +264,22 @@ void handleGameOutput({
required void Function() onBuildCorrupted, required void Function() onBuildCorrupted,
}) { }) {
if (line.contains(kShutdownLine)) { if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown(); onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ }else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted(); onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){ }else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError(); onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) { }else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn(); onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) { }else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd(); onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) { }else if(line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached(); onDisplayAttached();
} }
} }
@@ -299,7 +312,14 @@ final class _ExtendedProcess implements Process {
@override @override
Future<int> get exitCode => _delegate.exitCode; Future<int> get exitCode {
try {
return _delegate.exitCode;
}catch(_) {
return watchProcess(_delegate.pid)
.then((_) => -1);
}
}
@override @override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal); bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);

Binary file not shown.

View File

@@ -79,7 +79,7 @@
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite", "settingsClientDescription": "Configure the internal files used by the launcher for Fortnite",
"settingsClientOptionsName": "Options", "settingsClientOptionsName": "Options",
"settingsClientOptionsDescription": "Configure additional options for Fortnite", "settingsClientOptionsDescription": "Configure additional options for Fortnite",
"settingsClientConsoleName": "Unreal engine console", "settingsClientConsoleName": "Unreal engine patcher",
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console", "settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
"settingsClientConsoleKeyName": "Unreal engine console key", "settingsClientConsoleKeyName": "Unreal engine console key",
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console", "settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
@@ -88,7 +88,7 @@
"settingsClientMemoryName": "Memory patcher", "settingsClientMemoryName": "Memory patcher",
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak", "settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
"settingsClientArgsName": "Custom launch arguments", "settingsClientArgsName": "Custom launch arguments",
"settingsClientArgsDescription": "Additional arguments to use when launching the game", "settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
"settingsClientArgsPlaceholder": "Arguments...", "settingsClientArgsPlaceholder": "Arguments...",
"settingsServerName": "Internal files", "settingsServerName": "Internal files",
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
@@ -118,9 +118,7 @@
"settingsUtilsResetDefaultsName": "Reset settings", "settingsUtilsResetDefaultsName": "Reset settings",
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values", "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", "settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
"settingsUtilsResetDefaultsContent": "Reset",
"settingsUtilsDialogSecondaryAction": "Close", "settingsUtilsDialogSecondaryAction": "Close",
"settingsUtilsDialogPrimaryAction": "Reset",
"selectFortniteName": "Fortnite version", "selectFortniteName": "Fortnite version",
"selectFortniteDescription": "Select the version of Fortnite you want to use", "selectFortniteDescription": "Select the version of Fortnite you want to use",
"manageVersionsName": "Manage versions", "manageVersionsName": "Manage versions",
@@ -147,6 +145,7 @@
"defaultServerName": "Reboot Game Server", "defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server", "defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...", "downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully", "downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}", "downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllRetry": "Retry", "downloadDllRetry": "Retry",
@@ -156,6 +155,7 @@
"launchingGameClientAndServer": "Launching the game client and server...", "launchingGameClientAndServer": "Launching the game client and server...",
"startGameServer": "Start a game server", "startGameServer": "Start a game server",
"usernameOrEmail": "Username/Email", "usernameOrEmail": "Username/Email",
"invalidEmail": "Invalid email",
"usernameOrEmailPlaceholder": "Type your username or email", "usernameOrEmailPlaceholder": "Type your username or email",
"password": "Password", "password": "Password",
"passwordPlaceholder": "Type your password, if you want to use one", "passwordPlaceholder": "Type your password, if you want to use one",
@@ -261,6 +261,7 @@
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "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}",
"fortniteCrashError": "The {name} crashed after being launched",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available",
"noServerFound": "No server found: invalid or expired link", "noServerFound": "No server found: invalid or expired link",
"settingsUtilsThemeName": "Theme", "settingsUtilsThemeName": "Theme",
@@ -320,6 +321,7 @@
"none": "none", "none": "none",
"openLog": "Open log", "openLog": "Open log",
"backendProcessError": "The backend shut down unexpectedly", "backendProcessError": "The backend shut down unexpectedly",
"backendErrorMessage": "The backend reported an unexpected error",
"welcomeTitle": "Welcome to Reboot Launcher", "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", "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", "welcomeAction": "Take the tour",
@@ -359,9 +361,12 @@
"promptBackendDetachedActionLabel": "Next", "promptBackendDetachedActionLabel": "Next",
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support", "promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
"promptInfoTabActionLabel": "Next", "promptInfoTabActionLabel": "Next",
"promptSettingsTabText": "The Settings tab contains options to customize and reset the launcher", "promptSettingsTabText": "The Settings tab contains options to customize the launcher",
"promptSettingsTabActionLabel": "Done", "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!", "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", "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:package_info_plus/package_info_plus.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/controller/dll_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';
@@ -188,10 +189,11 @@ void _initWindow() => doWhenWindowReady(() async {
Future<List<Object>> _initStorage() async { Future<List<Object>> _initStorage() async {
final errors = <Object>[]; final errors = <Object>[];
try { try {
await GetStorage("game_storage", settingsDirectory.path).initStorage; await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
await GetStorage("backend_storage", settingsDirectory.path).initStorage; await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
await GetStorage("settings_storage", settingsDirectory.path).initStorage; await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
await GetStorage("hosting_storage", settingsDirectory.path).initStorage; await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
}catch(error) { }catch(error) {
appWithNoStorage = true; appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); 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); errors.add(error);
} }
try {
Get.put(DllController());
}catch(error) {
errors.add(error);
}
return errors; return errors;
} }

View File

@@ -2,18 +2,24 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.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:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
class BackendController extends GetxController { 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 host;
late final TextEditingController port; late final TextEditingController port;
late final Rx<ServerType> type; late final Rx<ServerType> type;
late final TextEditingController gameServerAddress; late final TextEditingController gameServerAddress;
late final FocusNode gameServerAddressFocusNode; late final FocusNode gameServerAddressFocusNode;
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started; late final RxBool started;
late final RxBool detached; late final RxBool detached;
StreamSubscription? worker; StreamSubscription? worker;
@@ -22,13 +28,13 @@ class BackendController extends GetxController {
HttpServer? remoteServer; HttpServer? remoteServer;
BackendController() { BackendController() {
storage = appWithNoStorage ? null : GetStorage("backend_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
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;
} }
@@ -37,13 +43,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));
final address = storage?.read("game_server_address"); final address = _storage?.read("game_server_address");
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address); gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
var lastValue = gameServerAddress.text; var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue); writeMatchmakingIp(lastValue);
@@ -55,7 +61,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) {
@@ -64,6 +70,37 @@ class BackendController extends GetxController {
} }
}); });
gameServerAddressFocusNode = FocusNode(); 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() { void joinLocalhost() {
@@ -73,18 +110,19 @@ class BackendController extends GetxController {
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 : "";
port.text = kDefaultBackendPort.toString(); port.text = kDefaultBackendPort.toString();
gameServerAddress.text = "127.0.0.1"; gameServerAddress.text = "127.0.0.1";
consoleKey.value = _kDefaultConsoleKey;
detached.value = false; detached.value = false;
} }
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;
} }
@@ -97,24 +135,20 @@ 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({required void Function() onExit, required void Function(String) onError}) async* {
try { try {
if(started.value) { if(started.value) {
return; return;
} }
final serverType = type.value;
final hostData = this.host.text.trim(); final hostData = this.host.text.trim();
final portData = this.port.text.trim(); final portData = this.port.text.trim();
if(type() != ServerType.local) {
started.value = true; started.value = true;
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting); yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
} }
if (hostData.isEmpty) { if (hostData.isEmpty) {
@@ -136,7 +170,7 @@ class BackendController extends GetxController {
return; return;
} }
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.freeingPort); yield ServerResult(ServerResultType.freeingPort);
final result = await freeBackendPort(); final result = await freeBackendPort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
@@ -146,9 +180,20 @@ class BackendController extends GetxController {
} }
} }
switch(type()){ switch(serverType){
case ServerType.embedded: 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; embeddedProcessPid = process.pid;
break; break;
case ServerType.remote: case ServerType.remote:
@@ -173,6 +218,10 @@ class BackendController extends GetxController {
} }
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData")); localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
}else {
// If the local server is running on port 3551 there is no reverse proxy running
// We only need to check if everything is working
started.value = false;
} }
break; break;
@@ -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()) { if(started()) {
yield* stop(); yield* stop();
}else { }else {
yield* start(); yield* start(
onExit: onExit,
onError: onError
);
} }
} }
} }

View File

@@ -0,0 +1,249 @@
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 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));
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.cobalt);
memoryLeakDll.text = getDefaultDllPath(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(getDefaultDllPath(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 getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
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");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
);
}
if(!force && 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;
}
}
}
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:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.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/src/util/keyboard.dart'; import 'package:reboot_launcher/main.dart';
import '../../main.dart';
class GameController extends GetxController { class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); static const String storageName = "game_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final TextEditingController username; late final TextEditingController username;
@@ -22,10 +18,9 @@ class GameController extends GetxController {
late final Rxn<FortniteVersion> _selectedVersion; late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started; late final RxBool started;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() { GameController() {
_storage = appWithNoStorage ? null : GetStorage("game_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry)) .map((entry) => FortniteVersion.fromJson(entry))
@@ -41,41 +36,9 @@ class GameController extends GetxController {
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());
_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() { void reset() {
@@ -83,6 +46,7 @@ class GameController extends GetxController {
password.text = ""; password.text = "";
customLaunchArgs.text = ""; customLaunchArgs.text = "";
versions.value = []; versions.value = [];
_selectedVersion.value = null;
instance.value = null; instance.value = null;
} }

View File

@@ -12,6 +12,8 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class HostingController extends GetxController { class HostingController extends GetxController {
static const String storageName = "hosting_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String uuid; late final String uuid;
late final TextEditingController name; late final TextEditingController name;
@@ -28,10 +30,11 @@ class HostingController extends GetxController {
late final RxBool published; late final RxBool published;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rxn<Set<FortniteServer>> servers; late final Rxn<Set<FortniteServer>> servers;
late final TextEditingController customLaunchArgs;
late final Semaphore _semaphore; late final Semaphore _semaphore;
HostingController() { HostingController() {
_storage = appWithNoStorage ? null : GetStorage("hosting_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
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"));
@@ -62,6 +65,8 @@ class HostingController extends GetxController {
servers.value = event; servers.value = event;
published.value = event.any((element) => element.id == uuid); published.value = event.any((element) => element.id == uuid);
}); });
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore(); _semaphore = Semaphore();
} }
@@ -135,10 +140,10 @@ class HostingController extends GetxController {
description.text = ""; description.text = "";
showPassword.value = false; showPassword.value = false;
discoverable.value = false; discoverable.value = false;
started.value = false;
instance.value = null; instance.value = null;
type.value = GameServerType.headless; type.value = GameServerType.headless;
autoRestart.value = true; autoRestart.value = true;
customLaunchArgs.text = "";
} }
FortniteServer? findServerById(String uuid) { FortniteServer? findServerById(String uuid) {

View File

@@ -15,37 +15,20 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {
static const String storageName = "settings_storage";
late final GetStorage? _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 RxString language;
late final Rx<ThemeMode> themeMode; 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 RxBool firstRun;
late final Map<String, Future<bool>> _operations; late final RxBool debug;
late double width; late double width;
late double height; late double height;
late double? offsetX; late double? offsetX;
late double? offsetY; late double? offsetY;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
SettingsController() { SettingsController() {
_storage = appWithNoStorage ? null : GetStorage("settings_storage"); _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));
width = _storage?.read("width") ?? kDefaultWindowWidth; width = _storage?.read("width") ?? kDefaultWindowWidth;
height = _storage?.read("height") ?? kDefaultWindowHeight; height = _storage?.read("height") ?? kDefaultWindowHeight;
offsetX = _storage?.read("offset_x"); offsetX = _storage?.read("offset_x");
@@ -54,25 +37,9 @@ class SettingsController extends GetxController {
themeMode.listen((value) => _storage?.write("theme", value.index)); themeMode.listen((value) => _storage?.write("theme", value.index));
language = RxString(_storage?.read("language") ?? currentLocale); language = RxString(_storage?.read("language") ?? currentLocale);
language.listen((value) => _storage?.write("language", value)); 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 = RxBool(_storage?.read("first_run_tutorial") ?? true);
firstRun.listen((value) => _storage?.write("first_run_tutorial", value)); firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
_operations = {}; debug = RxBool(false);
}
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) { void saveWindowSize(Size size) {
@@ -87,20 +54,6 @@ class SettingsController extends GetxController {
_storage?.write("offset_y", offsetY); _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 { Future<void> notifyLauncherUpdate() async {
if (appVersion == null) { if (appVersion == null) {
return; return;
@@ -125,7 +78,8 @@ class SettingsController extends GetxController {
child: Text(translations.updateAvailableAction), child: Text(translations.updateAvailableAction),
onPressed: () { onPressed: () {
infoBar.close(); infoBar.close();
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); launchUrl(Uri.parse(
"https://github.com/Auties00/reboot_launcher/releases"));
}, },
) )
); );
@@ -133,7 +87,8 @@ class SettingsController extends GetxController {
Future<dynamic> _getPubspecYaml() async { Future<dynamic> _getPubspecYaml() async {
try { try {
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); final pubspecResponse = await http.get(Uri.parse(
"https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) { if (pubspecResponse.statusCode != 200) {
return null; return null;
} }
@@ -144,190 +99,4 @@ class SettingsController extends GetxController {
return null; 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

@@ -23,19 +23,18 @@ InfoBarEntry showRebootInfoBar(dynamic text, {
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox( Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: _height minHeight: _height
), ),
child: Mica( child: Mica(
elevation: 1, elevation: 1,
child: InfoBar( child: InfoBar(
title: Row( title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if(text is Widget) Expanded(
text, child: text is Widget ? text : Text(text)
if(text is String) ),
Text(text),
if(action != null) if(action != null)
action action
], ],

View File

@@ -1,3 +1,4 @@
import 'package:email_validator/email_validator.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons; import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -23,6 +24,17 @@ Future<bool> showProfileForm(BuildContext context) async{
label: translations.usernameOrEmail, label: translations.usernameOrEmail,
child: TextFormBox( child: TextFormBox(
placeholder: translations.usernameOrEmailPlaceholder, placeholder: translations.usernameOrEmailPlaceholder,
validator: (text) {
if(_gameController.password.text.isEmpty) {
return null;
}
if(EmailValidator.validate(_gameController.username.text)) {
return null;
}
return translations.invalidEmail;
},
controller: _gameController.username, controller: _gameController.username,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
enableSuggestions: true, enableSuggestions: true,

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/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
final List<InfoBarEntry> _infoBars = []; final List<InfoBarEntry> _infoBars = [];
@@ -27,7 +28,27 @@ extension ServerControllerDialog on BackendController {
Future<bool> toggleInteractive() async { Future<bool> toggleInteractive() async {
cancelInteractive(); 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>(); final completer = Completer<bool>();
InfoBarEntry? entry; InfoBarEntry? entry;
worker = stream.listen((event) { worker = stream.listen((event) {
@@ -54,19 +75,6 @@ extension ServerControllerDialog on BackendController {
duration: null duration: null
); );
case ServerResultType.startSuccess: 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( return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer, type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
@@ -307,7 +315,9 @@ extension ServerControllerDialog on BackendController {
onDismissed: onDismissed, onDismissed: onDismissed,
action: action action: action
); );
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
_infoBars.add(result); _infoBars.add(result);
}
return result; return result;
} }
} }

View File

@@ -1,5 +1,10 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
abstract class RebootPage extends StatefulWidget { abstract class RebootPage extends StatefulWidget {
const RebootPage({super.key}); const RebootPage({super.key});
@@ -19,15 +24,32 @@ abstract class RebootPage extends StatefulWidget {
} }
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> { abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
var buttonWidget = button; var buttonWidget = button;
if(buttonWidget == null) { if(buttonWidget == null) {
return _listView; return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
_buildDebugInfo(),
Expanded(
child: _listView
)
],
);
} }
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
_buildDebugInfo(),
Expanded(
child: Column(
children: [ children: [
Expanded( Expanded(
child: _listView, child: _listView,
@@ -42,9 +64,70 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
child: buttonWidget child: buttonWidget
) )
], ],
),
),
],
); );
} }
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
Widget _buildDebugInfo() => Obx(() {
if(!_settingsController.debug.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text("Debug mode is enabled"),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text( "• Automatic dll injection is disabled\n"
"• The game server cannot start automatically\n"
"• The game server runs in a normal window")
),
onClose: () {
_settingsController.debug.value = false;
},
),
)
);
});
ListView get _listView => ListView.builder( ListView get _listView => ListView.builder(
itemCount: settings.length, itemCount: settings.length,
itemBuilder: (context, index) => settings[index], itemBuilder: (context, index) => settings[index],

View File

@@ -43,7 +43,6 @@ class BackendPage extends RebootPage {
} }
class _BackendPageState extends RebootPageState<BackendPage> { class _BackendPageState extends RebootPageState<BackendPage> {
final GameController _gameController = Get.find<GameController>();
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry; InfoBarEntry? _infoBarEntry;
@@ -56,7 +55,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
} }
if(keyEvent.physicalKey.isUnrealEngineKey) { if(keyEvent.physicalKey.isUnrealEngineKey) {
_gameController.consoleKey.value = keyEvent.physicalKey; _backendController.consoleKey.value = keyEvent.physicalKey;
} }
_infoBarEntry?.close(); _infoBarEntry?.close();
@@ -194,7 +193,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
duration: null 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: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/controller/dll_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/messenger/abstract/dialog.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 BackendController _backendController = Get.find<BackendController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey(); final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode(); final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
@@ -134,9 +136,9 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
} }
for(final injectable in InjectableDll.values) { for(final injectable in InjectableDll.values) {
final (file, custom) = _settingsController.getInjectableData(injectable); final (file, custom) = _dllController.getInjectableData(injectable);
if(!custom) { if(!custom) {
_settingsController.downloadCriticalDllInteractive( _dllController.downloadCriticalDllInteractive(
file.path, file.path,
silent: true silent: true
); );
@@ -144,7 +146,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
} }
watchDlls().listen((filePath) => showDllDeletedDialog(() { 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:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.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/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';
@@ -53,6 +54,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -199,6 +201,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.settingsServerOptionsName), title: Text(translations.settingsServerOptionsName),
subtitle: Text(translations.settingsServerOptionsSubtitle), subtitle: Text(translations.settingsServerOptionsSubtitle),
children: [ children: [
SettingTile(
icon: Icon(
FluentIcons.options_24_regular
),
title: Text(translations.settingsClientArgsName),
subtitle: Text(translations.settingsClientArgsDescription),
content: TextFormBox(
placeholder: translations.settingsClientArgsPlaceholder,
controller: _hostingController.customLaunchArgs,
)
),
SettingTile( SettingTile(
icon: Icon( icon: Icon(
FluentIcons.window_console_20_regular FluentIcons.window_console_20_regular
@@ -208,11 +221,12 @@ class _HostingPageState extends RebootPageState<HostPage> {
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true, onOpen: () => inDialog = true,
onClose: () => inDialog = false, onClose: () => inDialog = false,
leading: Text(_hostingController.type.value.translatedName), leading: Text(_settingsController.debug.value ? GameServerType.window.translatedName : _hostingController.type.value.translatedName),
items: GameServerType.values.map((entry) => MenuFlyoutItem( items: GameServerType.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName), text: Text(entry.translatedName),
onPressed: () => _hostingController.type.value = entry onPressed: () => _hostingController.type.value = entry
)).toList() )).toList(),
disabled: _settingsController.debug.value
)), )),
), ),
SettingTile( SettingTile(
@@ -246,14 +260,14 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: 64, contentWidth: 64,
content: TextFormBox( content: TextFormBox(
placeholder: translations.settingsServerPortName, placeholder: translations.settingsServerPortName,
controller: _settingsController.gameServerPort, controller: _dllController.gameServerPort,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textAlign: TextAlign.center, textAlign: TextAlign.center,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly
] ]
) )
), )
], ],
); );
@@ -273,22 +287,22 @@ class _HostingPageState extends RebootPageState<HostPage> {
content: Obx(() => DropDownButton( content: Obx(() => DropDownButton(
onOpen: () => inDialog = true, onOpen: () => inDialog = true,
onClose: () => inDialog = false, onClose: () => inDialog = false,
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName), leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: { items: {
false: translations.settingsServerTypeEmbeddedName, false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem( }.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value), text: Text(entry.value),
onPressed: () { onPressed: () {
final oldValue = _settingsController.customGameServer.value; final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) { if(oldValue == entry.key) {
return; return;
} }
_settingsController.customGameServer.value = entry.key; _dllController.customGameServer.value = entry.key;
_settingsController.infoBarEntry?.close(); _dllController.infoBarEntry?.close();
if(!entry.key) { if(!entry.key) {
_settingsController.updateReboot( _dllController.updateGameServerDll(
force: true force: true
); );
} }
@@ -297,18 +311,23 @@ class _HostingPageState extends RebootPageState<HostPage> {
)) ))
), ),
Obx(() { Obx(() {
if(!_settingsController.customGameServer.value) { if(!_dllController.customGameServer.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return createFileSetting( return createFileSetting(
title: translations.settingsServerFileName, title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription, description: translations.settingsServerFileDescription,
controller: _settingsController.gameServerDll controller: _dllController.gameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
_dllController.gameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path);
}
); );
}), }),
Obx(() { Obx(() {
if(_settingsController.customGameServer.value) { if(_dllController.customGameServer.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -318,15 +337,34 @@ class _HostingPageState extends RebootPageState<HostPage> {
), ),
title: Text(translations.settingsServerMirrorName), title: Text(translations.settingsServerMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription), subtitle: Text(translations.settingsServerMirrorDescription),
content: TextFormBox( content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder, placeholder: translations.settingsServerMirrorPlaceholder,
controller: _settingsController.url, controller: _dllController.url,
validator: _checkUpdateUrl validator: _checkUpdateUrl
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: () => _dllController.url.text = kRebootDownloadUrl,
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
) )
); );
}), }),
Obx(() { Obx(() {
if(_settingsController.customGameServer.value) { if(_dllController.customGameServer.value) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -336,23 +374,44 @@ 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: Row(
children: [
Expanded(
child: Obx(() => DropDownButton(
onOpen: () => inDialog = true, onOpen: () => inDialog = true,
onClose: () => inDialog = false, onClose: () => inDialog = false,
leading: Text(_settingsController.timer.value.text), leading: Text(_dllController.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: () {
_settingsController.timer.value = entry; _dllController.timer.value = entry;
_settingsController.infoBarEntry?.close(); _dllController.infoBarEntry?.close();
_settingsController.updateReboot( _dllController.updateGameServerDll(
force: true force: true
); );
} }
)).toList() )).toList()
)) )),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
)
],
)
); );
}), })
], ],
); );
@@ -420,7 +479,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.hostResetName), title: Text(translations.hostResetName),
subtitle: Text(translations.hostResetDescription), subtitle: Text(translations.hostResetDescription),
content: Button( content: Button(
onPressed: () => showResetDialog(_hostingController.reset), onPressed: () => showResetDialog(() {
_hostingController.reset();
_dllController.resetServer();
}),
child: Text(translations.hostResetContent), child: Text(translations.hostResetContent),
) )
); );

View File

@@ -1,9 +1,12 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.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/game_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/messenger/abstract/overlay.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/messenger/implementation/onboard.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';
@@ -37,48 +40,7 @@ class PlayPage extends RebootPage {
class _PlayPageState extends RebootPageState<PlayPage> { class _PlayPageState extends RebootPageState<PlayPage> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final DllController _dllController = Get.find<DllController>();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: super.build(context),
)
],
);
}
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
@override @override
Widget? get button => LaunchButton( Widget? get button => LaunchButton(
@@ -94,6 +56,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
), ),
_options, _options,
_internalFiles, _internalFiles,
_resetDefaults
]; ];
SettingTile get _internalFiles => SettingTile( SettingTile get _internalFiles => SettingTile(
@@ -106,17 +69,32 @@ class _PlayPageState extends RebootPageState<PlayPage> {
createFileSetting( createFileSetting(
title: translations.settingsClientConsoleName, title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription, description: translations.settingsClientConsoleDescription,
controller: _settingsController.unrealEngineConsoleDll controller: _dllController.unrealEngineConsoleDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
), ),
createFileSetting( createFileSetting(
title: translations.settingsClientAuthName, title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription, description: translations.settingsClientAuthDescription,
controller: _settingsController.backendDll controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.cobalt);
_dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
), ),
createFileSetting( createFileSetting(
title: translations.settingsClientMemoryName, title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription, description: translations.settingsClientMemoryDescription,
controller: _settingsController.memoryLeakDll controller: _dllController.memoryLeakDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.memory);
_dllController.memoryLeakDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
), ),
], ],
); );
@@ -141,4 +119,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,8 +42,8 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
List<Widget> get settings => [ List<Widget> get settings => [
_language, _language,
_theme, _theme,
_resetDefaults, _debugMode,
_installationDirectory _installationDirectory,
]; ];
SettingTile get _language => SettingTile( SettingTile get _language => SettingTile(
@@ -89,18 +89,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
)) ))
); );
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( SettingTile get _installationDirectory => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.folder_24_regular FluentIcons.folder_24_regular
@@ -112,6 +100,29 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
child: Text(translations.settingsUtilsInstallationDirectoryContent), child: Text(translations.settingsUtilsInstallationDirectoryContent),
) )
); );
SettingTile get _debugMode => SettingTile(
icon: Icon(
FluentIcons.developer_board_24_regular
),
title: Text("Debug mode"),
subtitle: Text("Whether the launcher should disable automatic features for troubleshooting"),
contentWidth: null,
content: Row(
children: [
Text(
_settingsController.debug.value ? translations.on : translations.off
),
const SizedBox(
width: 16.0
),
Obx(() => ToggleSwitch(
checked: _settingsController.debug.value,
onChanged: (value) => _settingsController.debug.value = value
))
],
)
);
} }
extension _ThemeModeExtension on ThemeMode { extension _ThemeModeExtension on ThemeMode {

View File

@@ -36,7 +36,6 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
} }
} }
final start = DateTime.now(); final start = DateTime.now();
var firstTime = true; var firstTime = true;
final split = address.split(":"); final split = address.split(":");

View File

@@ -2,17 +2,24 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.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:reboot_launcher/src/widget/setting_tile.dart';
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
final obx = RxString(controller.text);
controller.addListener(() => obx.value = controller.text);
return SettingTile(
icon: Icon( icon: Icon(
FluentIcons.document_24_regular FluentIcons.document_24_regular
), ),
title: Text(title), title: Text(title),
subtitle: Text(description), subtitle: Text(description),
content: FileSelector( content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder, placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle, windowTitle: translations.selectPathWindowTitle,
controller: controller, controller: controller,
@@ -20,8 +27,30 @@ SettingTile createFileSetting({required String title, required String descriptio
extension: "dll", extension: "dll",
folder: false, folder: false,
validatorMode: AutovalidateMode.always validatorMode: AutovalidateMode.always
),
),
const SizedBox(width: 8.0),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
child: Button(
style: ButtonStyle(
padding: ButtonState.all(EdgeInsets.zero)
),
onPressed: onReset,
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
),
))
],
) )
); );
}
String? _checkDll(String? text) { String? _checkDll(String? text) {
if (text == null || text.isEmpty) { if (text == null || text.isEmpty) {

View File

@@ -9,6 +9,7 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.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/controller/dll_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';
@@ -39,11 +40,13 @@ class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
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 DllController _dllController = Get.find<DllController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar; InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
CancelableOperation? _pingOperation;
IVirtualDesktop? _virtualDesktop; IVirtualDesktop? _virtualDesktop;
@override @override
@@ -93,10 +96,6 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${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, host) == null) { if(await _getDllFileOrStop(injectable, host) == null) {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return; return;
} }
} }
@@ -122,7 +121,7 @@ class _LaunchButtonState extends State<LaunchButton> {
return; return;
} }
log("[${host ? 'HOST' : 'GAME'}] Backend works"); log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value; final serverType = _settingsController.debug.value ? GameServerType.window : _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false); final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance"); log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
@@ -138,6 +137,12 @@ class _LaunchButtonState extends State<LaunchButton> {
}else { }else {
_showLaunchingGameServerWidget(); _showLaunchingGameServerWidget();
} }
} on ProcessException catch (exception, stackTrace) {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace
);
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
_onStop( _onStop(
reason: _StopReason.unknownError, reason: _StopReason.unknownError,
@@ -154,6 +159,11 @@ class _LaunchButtonState extends State<LaunchButton> {
return null; return null;
} }
if(_settingsController.debug.value) {
log("[${host ? 'HOST' : 'GAME'}] The user is on debug mode, not asking for auto server");
return null;
}
if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) { if(!forceLinkedHosting && _backendController.type.value == ServerType.embedded && !isLocalHost(_backendController.gameServerAddress.text)) {
log("[${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;
@@ -251,7 +261,7 @@ class _LaunchButtonState extends State<LaunchButton> {
host, host,
hostType, hostType,
false, false,
"" host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
); );
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}"); log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess( final gameProcess = await startProcess(
@@ -263,15 +273,29 @@ class _LaunchButtonState extends State<LaunchButton> {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
); );
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
void onGameOutput(String line, bool error) { void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line"); log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput( handleGameOutput(
line: line, line: line,
host: host, host: host,
onShutdown: () => _onStop(reason: _StopReason.normal), onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError), onTokenError: () {
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError), if(_settingsController.debug.value) {
log("[PROCESS] Ignoring token error because debug mode is on");
}else {
_onStop(reason: _StopReason.tokenError);
}
},
onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError);
}else {
_onStop(reason: _StopReason.crash);
}
},
onLoggedIn: () =>_onLoggedIn(host), onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version), onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version) onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
@@ -391,7 +415,7 @@ class _LaunchButtonState extends State<LaunchButton> {
await _injectOrShowError(InjectableDll.console, host); await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected(); _onGameClientInjected();
}else { }else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) { if(gameServerPort != null) {
await killProcessByPort(gameServerPort); await killProcessByPort(gameServerPort);
} }
@@ -424,11 +448,12 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true, loading: true,
duration: null duration: null
); );
final gameServerPort = _settingsController.gameServerPort.text; final gameServerPort = _dllController.gameServerPort.text;
final localPingResult = await pingGameServer( this._pingOperation = await CancelableOperation.fromFuture(pingGameServer(
"127.0.0.1:$gameServerPort", "127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2) timeout: const Duration(minutes: 2)
); ));
final localPingResult = (await _pingOperation?.value) ?? false;
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
if (!localPingResult) { if (!localPingResult) {
showRebootInfoBar( showRebootInfoBar(
@@ -471,16 +496,18 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final publicIp = await Ipify.ipv4(); final publicIp = await Ipify.ipv4();
final externalResult = await pingGameServer("$publicIp:$gameServerPort"); this._pingOperation = CancelableOperation.fromFuture(pingGameServer("$publicIp:$gameServerPort"));
final externalResult = (await _pingOperation?.value) ?? false;
if (externalResult) { if (externalResult) {
return true; return true;
} }
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
final future = pingGameServer( this._pingOperation = CancelableOperation.fromFuture(pingGameServer(
"$publicIp:$gameServerPort", "$publicIp:$gameServerPort",
timeout: const Duration(days: 365) timeout: const Duration(days: 365)
); ));
final future = await _pingOperation?.value ?? false;
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkGameServerFixMessage(gameServerPort), translations.checkGameServerFixMessage(gameServerPort),
action: Button( action: Button(
@@ -499,6 +526,8 @@ 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) { if(host == null) {
await _pingOperation?.cancel();
_pingOperation = null;
await _operation?.cancel(); await _operation?.cancel();
_operation = null; _operation = null;
_backendController.cancelInteractive(); _backendController.cancelInteractive();
@@ -506,6 +535,10 @@ class _LaunchButtonState extends State<LaunchButton> {
host = host ?? widget.host; host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance == null) {
return;
}
if(host){ if(host){
_hostingController.instance.value = null; _hostingController.instance.value = null;
}else { }else {
@@ -527,7 +560,6 @@ class _LaunchButtonState extends State<LaunchButton> {
_hostingController.discardServer(); _hostingController.discardServer();
} }
if(instance != null) {
if(reason == _StopReason.normal) { if(reason == _StopReason.normal) {
instance.launched = true; instance.launched = true;
} }
@@ -540,7 +572,6 @@ class _LaunchButtonState extends State<LaunchButton> {
host: child.serverType != null host: child.serverType != null
); );
} }
}
_setStarted(host, false); _setStarted(host, false);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -571,7 +602,7 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.exitCode: case _StopReason.exitCode:
if(instance != null && !instance.launched) { if(!instance.launched) {
showRebootInfoBar( showRebootInfoBar(
translations.corruptedVersionError, translations.corruptedVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
@@ -605,8 +636,9 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop();
showRebootInfoBar( showRebootInfoBar(
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none), translations.tokenError(instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( action: Button(
@@ -615,6 +647,13 @@ class _LaunchButtonState extends State<LaunchButton> {
) )
); );
break; break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError: case _StopReason.unknownError:
showRebootInfoBar( showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError), translations.unknownFortniteError(error ?? translations.unknownError),
@@ -648,6 +687,10 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}..."); log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
if(_settingsController.debug.value) {
return;
}
await injectDll(gameProcess, dllPath); await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable); instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}"); log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -664,7 +707,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); 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"); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) { if(await file.exists()) {
log("[${host ? 'HOST' : 'GAME'}] Path exists"); log("[${host ? 'HOST' : 'GAME'}] Path exists");
@@ -672,13 +715,17 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist"); log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
if(customDll || isRetry) { if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return null; return null;
} }
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _settingsController.downloadCriticalDllInteractive(file.path); await _dllController.downloadCriticalDllInteractive(file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(injectable, host, true); return _getDllFileOrStop(injectable, host, true);
} }
@@ -695,7 +742,7 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true, loading: true,
duration: null, duration: null,
action: Obx(() { action: Obx(() {
if(_hostingController.started.value || linkedHosting) { if(_settingsController.debug.value || _hostingController.started.value || linkedHosting) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -731,7 +778,8 @@ enum _StopReason {
matchmakerError, matchmakerError,
tokenError, tokenError,
unknownError, unknownError,
exitCode; exitCode,
crash;
bool get isError => name.contains("Error"); bool get isError => name.contains("Error");
} }

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.2.2" version: "9.2.6"
publish_to: 'none' publish_to: 'none'
@@ -74,6 +74,9 @@ dependencies:
package_info_plus: ^8.0.0 package_info_plus: ^8.0.0
version: ^3.0.2 version: ^3.0.2
# Validate profile
email_validator: ^3.0.0
dependency_overrides: dependency_overrides:
xml: ^6.3.0 xml: ^6.3.0
http: ^0.13.5 http: ^0.13.5
@@ -98,4 +101,9 @@ flutter:
- assets/backend/profiles/ - assets/backend/profiles/
- assets/backend/public/ - assets/backend/public/
- assets/backend/responses/ - assets/backend/responses/
- assets/backend/responses/Athena/
- assets/backend/responses/Athena/BattlePass/
- assets/backend/responses/Athena/Discovery/
- assets/backend/responses/Campaign/
- assets/backend/responses/CloudDir/
- assets/build/ - assets/build/