5 Commits
9.1.0 ... 9.1.3

Author SHA1 Message Date
Alessandro Autiero
0a775e2f3f Merge pull request #52 from Auties00/_onLoggedIn
9.1.3
2024-06-04 22:32:10 +02:00
Alessandro Autiero
2bf084d120 9.1.3 2024-06-04 20:31:06 +02:00
Alessandro Autiero
93c5d6c56b Stuff 2024-06-03 16:27:52 +02:00
Alessandro Autiero
46034aa1fa Stuff 2024-06-03 16:26:04 +02:00
Alessandro Autiero
3069f3aa05 9.1.0 2024-06-02 16:54:48 +02:00
45 changed files with 937 additions and 668 deletions

View File

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

View File

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

View File

@@ -24,7 +24,7 @@ Future<void> startGame() async {
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, "")) _gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
..exitCode.then((_) => _onClose()) ..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose)); ..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
_injectOrShowError("cobalt.dll"); _injectOrShowError("cobalt.dll");
} }
@@ -52,6 +52,17 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
stdout.writeln(line); stdout.writeln(line);
} }
handleGameOutput(
line: line,
host: hosting,
onDisplayAttached: () {}, // TODO: Support virtual desktops
onLoggedIn: onLoggedIn,
onMatchEnd: onMatchEnd,
onShutdown: onShutdown,
onTokenError: onTokenError,
onBuildCorrupted: onBuildCorrupted
);
if (line.contains(kShutdownLine)) { if (line.contains(kShutdownLine)) {
_onClose(); _onClose();
return; return;
@@ -70,7 +81,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
_injectOrShowError("console.dll"); _injectOrShowError("console.dll");
} }
_injectOrShowError("memoryleak.dll"); _injectOrShowError("memory.dll");
} }
} }
@@ -87,12 +98,12 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
try { try {
stdout.writeln("Injecting $binary..."); stdout.writeln("Injecting $binary...");
var dll = locate ? File("${assetsDirectory.path}\\dlls\\$binary") : File(binary); var dll = locate ? File("${dllsDirectory.path}\\$binary") : File(binary);
if(!dll.existsSync()){ if(!dll.existsSync()){
throw Exception("Cannot inject $dll: missing file"); throw Exception("Cannot inject $dll: missing file");
} }
await injectDll(_gameProcess!.pid, dll.path); await injectDll(_gameProcess!.pid, dll);
} catch (exception) { } catch (exception) {
throw Exception("Cannot inject binary: $binary"); throw Exception("Cannot inject binary: $binary");
} }

View File

@@ -7,12 +7,12 @@ import 'package:reboot_common/common.dart';
// TODO: Use github // TODO: Use github
const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll"; const String _baseDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968021373169674/cobalt.dll";
const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll"; const String _consoleDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968095033524234/console.dll";
const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memoryleak.dll"; const String _memoryFixDownload = "https://cdn.discordapp.com/attachments/1095351875961901057/1110968141556756581/memory.dll";
const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip"; const String _embeddedConfigDownload = "https://cdn.discordapp.com/attachments/1026121175878881290/1040679319351066644/embedded.zip";
Future<void> downloadRequiredDLLs() async { Future<void> downloadRequiredDLLs() async {
stdout.writeln("Downloading necessary components..."); stdout.writeln("Downloading necessary components...");
var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll"); var consoleDll = File("${dllsDirectory.path}\\console.dll");
if(!consoleDll.existsSync()){ if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload)); var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){ if(response.statusCode != 200){
@@ -22,7 +22,7 @@ Future<void> downloadRequiredDLLs() async {
await consoleDll.writeAsBytes(response.bodyBytes); await consoleDll.writeAsBytes(response.bodyBytes);
} }
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll"); var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
if(!craniumDll.existsSync()){ if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_baseDownload)); var response = await http.get(Uri.parse(_baseDownload));
if(response.statusCode != 200){ if(response.statusCode != 200){
@@ -32,11 +32,11 @@ Future<void> downloadRequiredDLLs() async {
await craniumDll.writeAsBytes(response.bodyBytes); await craniumDll.writeAsBytes(response.bodyBytes);
} }
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll"); var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
if(!memoryFixDll.existsSync()){ if(!memoryFixDll.existsSync()){
var response = await http.get(Uri.parse(_memoryFixDownload)); var response = await http.get(Uri.parse(_memoryFixDownload));
if(response.statusCode != 200){ if(response.statusCode != 200){
throw Exception("Cannot download memoryleak.dll"); throw Exception("Cannot download memory.dll");
} }
await memoryFixDll.writeAsBytes(response.bodyBytes); await memoryFixDll.writeAsBytes(response.bodyBytes);

View File

@@ -10,6 +10,7 @@ export 'package:reboot_common/src/model/server_result.dart';
export 'package:reboot_common/src/model/server_type.dart'; export 'package:reboot_common/src/model/server_type.dart';
export 'package:reboot_common/src/model/update_status.dart'; export 'package:reboot_common/src/model/update_status.dart';
export 'package:reboot_common/src/model/update_timer.dart'; export 'package:reboot_common/src/model/update_timer.dart';
export 'package:reboot_common/src/model/dll.dart';
export 'package:reboot_common/src/util/backend.dart'; export 'package:reboot_common/src/util/backend.dart';
export 'package:reboot_common/src/util/build.dart'; export 'package:reboot_common/src/util/build.dart';
export 'package:reboot_common/src/util/dll.dart'; export 'package:reboot_common/src/util/dll.dart';

View File

@@ -21,3 +21,4 @@ const List<String> kCannotConnectErrors = [
"UOnlineAccountCommon::ForceLogout" "UOnlineAccountCommon::ForceLogout"
]; ];
const String kGameFinishedLine = "PlayersLeft: 1"; const String kGameFinishedLine = "PlayersLeft: 1";
const String kDisplayInitializedLine = "Display";

View File

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

View File

@@ -0,0 +1,6 @@
enum InjectableDll {
console,
cobalt,
reboot,
memory
}

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
bool _watcher = false; bool _watcher = false;
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll"); final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
const String kRebootDownloadUrl = const String kRebootDownloadUrl =
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip"; "http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
@@ -18,7 +18,8 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
} }
Future<void> downloadCriticalDll(String name, String outputPath) async { Future<void> downloadCriticalDll(String name, String outputPath) async {
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/assets/dlls/$name")); print("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name");
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/$name"));
if(response.statusCode != 200) { if(response.statusCode != 200) {
throw Exception("Cannot download $name: status code ${response.statusCode}"); throw Exception("Cannot download $name: status code ${response.statusCode}");
} }

View File

@@ -34,10 +34,11 @@ final class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32() @Uint32()
external int dwNumEntries; external int dwNumEntries;
@Array(1) @Array(512)
external Array<_MIB_TCPROW_OWNER_PID> table; external Array<_MIB_TCPROW_OWNER_PID> table;
} }
bool isLocalHost(String host) => host.trim() == "127.0.0.1" bool isLocalHost(String host) => host.trim() == "127.0.0.1"
|| host.trim().toLowerCase() == "localhost" || host.trim().toLowerCase() == "localhost"
|| host.trim() == "0.0.0.0"; || host.trim() == "0.0.0.0";
@@ -46,7 +47,6 @@ bool killProcessByPort(int port) {
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(); var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
final dwSize = calloc<DWORD>(); final dwSize = calloc<DWORD>();
dwSize.value = 0; dwSize.value = 0;
int result = _getExtendedTcpTable( int result = _getExtendedTcpTable(
nullptr, nullptr,
dwSize, dwSize,
@@ -56,6 +56,7 @@ bool killProcessByPort(int port) {
0 0
); );
if (result == ERROR_INSUFFICIENT_BUFFER) { if (result == ERROR_INSUFFICIENT_BUFFER) {
free(pTcpTable);
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value); pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
result = _getExtendedTcpTable( result = _getExtendedTcpTable(
pTcpTable, pTcpTable,

View File

@@ -3,6 +3,8 @@ import 'dart:io';
Directory get installationDirectory => Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent; File(Platform.resolvedExecutable).parent;
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory { Directory get assetsDirectory {
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets"); var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) { if(directory.existsSync()) {

View File

@@ -33,7 +33,10 @@ final _CreateRemoteThread = _kernel32.lookupFunction<
Pointer<Uint32> lpThreadId)>('CreateRemoteThread'); Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
const chunkSize = 1024; const chunkSize = 1024;
Future<void> injectDll(int pid, String dll) async { Future<void> injectDll(int pid, File dll) async {
// Get the path to the file
final dllPath = dll.path;
final process = OpenProcess( final process = OpenProcess(
0x43A, 0x43A,
0, 0,
@@ -52,7 +55,7 @@ Future<void> injectDll(int pid, String dll) async {
final dllAddress = VirtualAllocEx( final dllAddress = VirtualAllocEx(
process, process,
nullptr, nullptr,
dll.length + 1, dllPath.length + 1,
0x3000, 0x3000,
0x4 0x4
); );
@@ -60,8 +63,8 @@ Future<void> injectDll(int pid, String dll) async {
final writeMemoryResult = WriteProcessMemory( final writeMemoryResult = WriteProcessMemory(
process, process,
dllAddress, dllAddress,
dll.toNativeUtf8(), dllPath.toNativeUtf8(),
dll.length, dllPath.length,
nullptr nullptr
); );
@@ -89,6 +92,18 @@ Future<void> injectDll(int pid, String dll) async {
} }
} }
Future<bool> startElevatedProcess({required String executable, required String args, bool window = false}) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async { Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
final argsOrEmpty = args ?? []; final argsOrEmpty = args ?? [];
if(wrapProcess) { if(wrapProcess) {
@@ -223,6 +238,31 @@ List<String> createRebootArgs(String username, String password, bool host, bool
return args; return args;
} }
void handleGameOutput({
required String line,
required bool host,
required void Function() onDisplayAttached,
required void Function() onLoggedIn,
required void Function() onMatchEnd,
required void Function() onShutdown,
required void Function() onTokenError,
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) {
onDisplayAttached();
}
}
String _parseUsername(String username, bool host) { String _parseUsername(String username, bool host) {
if(host) { if(host) {
return "Player${Random().nextInt(1000)}"; return "Player${Random().nextInt(1000)}";

View File

@@ -1,4 +1,3 @@
Some Fortnite versions support running this game server in the background without rendering the game: this type of server is called "headless" as the game is running, but you can't see it on your screen. Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
If headless is not supported by the Fortnite version you want to play, or if you disabled it manually from the "Configuration" section in the "Host" tab of the launcher, you will see an instance of Fortnite open on your screen. The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
For convenience, this window will be opened on a new Virtual Desktop, if your Windows version supports it. This feature can be disabled as well from from the "Configuration" section in the "Host" tab of the launcher. Both are open source on GitHub, anyone can easily contribute or audit the code!"
Just like in Minecraft, you need a game client to play the game and one to host the server."

View File

@@ -1,6 +1,6 @@
A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features. A backend is a piece of software that emulates the Epic Games server responsible for authentication and related features.
By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github. By default, the Reboot Launcher ships with a slightly customized version of LawinV1, an open source implementation available on Github.
If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue." If you are having any problems with the built in backend, enable the "Detached" option in the "Backend" tab of the Reboot Laucher to troubleshoot the issue.
LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend. LawinV1 was chosen to allow users to log into Fortnite and join games easily, but keep in mind that if you want to use features such as parties, voice chat or skins, you will need to use a custom backend.
Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user. Other popular options are LawinV2 and Momentum, both available on Github, but it's not recommended to use them if you are not an advanced user.
You can run these alternatives either either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher. You can run these alternatives either on your PC or on a server by selecting respectively "Local" or "Remote" from the "Type" section in the "Backend" tab of the Reboot Launcher.

View File

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

View File

@@ -17,14 +17,14 @@ import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/build_controller.dart'; import 'package:reboot_launcher/src/controller/build_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/info_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/dialog/implementation/error.dart'; import 'package:reboot_launcher/src/dialog/implementation/error.dart';
import 'package:reboot_launcher/src/dialog/implementation/server.dart'; import 'package:reboot_launcher/src/dialog/implementation/server.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/util/info.dart'; import 'package:reboot_launcher/src/page/implementation/info_page.dart';
import 'package:reboot_launcher/src/util/log.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
@@ -33,70 +33,107 @@ import 'package:system_theme/system_theme.dart';
import 'package:url_protocol/url_protocol.dart'; import 'package:url_protocol/url_protocol.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:win32/win32.dart';
const double kDefaultWindowWidth = 1536; const double kDefaultWindowWidth = 1164;
const double kDefaultWindowHeight = 1024; const double kDefaultWindowHeight = 864;
const String kCustomUrlSchema = "Reboot"; const String kCustomUrlSchema = "Reboot";
Version? appVersion; Version? appVersion;
bool appWithNoStorage = false;
class _MyHttpOverrides extends HttpOverrides { void main() {
@override log("[APP] Called");
HttpClient createHttpClient(SecurityContext? context){ runZonedGuarded(
return super.createHttpClient(context) () => _startApp(),
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
}
}
void main() => runZonedGuarded(
() async {
HttpOverrides.global = _MyHttpOverrides();
final errors = <Object>[];
try {
await installationDirectory.create(recursive: true);
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
await localNotifier.setup(
appName: 'Reboot Launcher',
shortcutPolicy: ShortcutPolicy.ignore
);
WidgetsFlutterBinding.ensureInitialized();
await SystemTheme.accentColor.load();
_initWindow();
initInfoTiles();
final versionError = await _initVersion();
if(versionError != null) {
errors.add(versionError);
}
final storageError = await _initStorage();
if(storageError != null) {
errors.add(storageError);
}
final urlError = await _initUrlHandler();
if(urlError != null) {
errors.add(urlError);
}
_checkGameServer();
}catch(uncaughtError) {
errors.add(uncaughtError);
} finally{
runApp(const RebootApplication());
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(errors));
}
},
(error, stack) => onError(error, stack, false), (error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification( zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false) handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
) )
); );
}
void _handleErrors(List<Object?> errors) { Future<void> _startApp() async {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false)); final errors = <Object>[];
try {
log("[APP] Starting application");
final pathError = await _initPath();
if(pathError != null) {
errors.add(pathError);
}
final databaseError = await _initDatabase();
if(databaseError != null) {
errors.add(databaseError);
}
final notificationsError = await _initNotifications();
if(notificationsError != null) {
errors.add(notificationsError);
}
final tilesError = InfoPage.initInfoTiles();
if(tilesError != null) {
errors.add(tilesError);
}
final versionError = await _initVersion();
if(versionError != null) {
errors.add(versionError);
}
final storageErrors = await _initStorage();
errors.addAll(storageErrors);
WidgetsFlutterBinding.ensureInitialized();
_initWindow();
final urlError = await _initUrlHandler();
if(urlError != null) {
errors.add(urlError);
}
}catch(uncaughtError) {
errors.add(uncaughtError);
} finally{
log("[APP] Started applications with errors: $errors");
runApp(RebootApplication(
errors: errors,
));
}
}
Future<Object?> _initNotifications() async {
try {
await localNotifier.setup(
appName: 'Reboot Launcher',
shortcutPolicy: ShortcutPolicy.ignore
);
return null;
}catch(error) {
return error;
}
}
Future<Object?> _initDatabase() async {
try {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
return null;
}catch(error) {
return error;
}
}
Future<Object?> _initPath() async {
try {
await installationDirectory.create(recursive: true);
return null;
}catch(error) {
return error;
}
} }
Future<Object?> _initVersion() async { Future<Object?> _initVersion() async {
@@ -109,67 +146,18 @@ Future<Object?> _initVersion() async {
} }
} }
Future<void> _checkGameServer() async {
try {
var backendController = Get.find<BackendController>();
var address = backendController.gameServerAddress.text;
if(isLocalHost(address)) {
return;
}
var result = await pingGameServer(address);
if(result) {
return;
}
var oldOwner = backendController.gameServerOwner.value;
backendController.joinLocalHost();
WidgetsBinding.instance.addPostFrameCallback((_) => showInfoBar(
oldOwner == null ? translations.serverNoLongerAvailableUnnamed : translations.serverNoLongerAvailable(oldOwner),
severity: InfoBarSeverity.warning,
duration: infoBarLongDuration
));
}catch(_) {
// Intended behaviour
// Just ignore the error
}
}
Future<Object?> _initUrlHandler() async { Future<Object?> _initUrlHandler() async {
try { try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
var appLinks = AppLinks();
var initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
return null; return null;
}catch(error) { }catch(error) {
return error; return error;
} }
} }
void _joinServer(Uri uri) {
var hostingController = Get.find<HostingController>();
var backendController = Get.find<BackendController>();
var uuid = _parseCustomUrl(uri);
var server = hostingController.findServerById(uuid);
if(server != null) {
backendController.joinServer(hostingController.uuid, server);
}else {
showInfoBar(
translations.noServerFound,
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
}
String _parseCustomUrl(Uri uri) => uri.host;
void _initWindow() => doWhenWindowReady(() async { void _initWindow() => doWhenWindowReady(() async {
try {
await SystemTheme.accentColor.load();
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await Window.initialize(); await Window.initialize();
var settingsController = Get.find<SettingsController>(); var settingsController = Get.find<SettingsController>();
@@ -193,32 +181,69 @@ void _initWindow() => doWhenWindowReady(() async {
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
); );
} }
}catch(error, stackTrace) {
onError(error, stackTrace, false);
}finally {
appWindow.show(); appWindow.show();
}
}); });
Future<Object?> _initStorage() async { Future<List<Object>> _initStorage() async {
final errors = <Object>[];
try { try {
await GetStorage("game", settingsDirectory.path).initStorage; await GetStorage("game", settingsDirectory.path).initStorage;
await GetStorage("backend", settingsDirectory.path).initStorage; await GetStorage("backend", settingsDirectory.path).initStorage;
await GetStorage("update", settingsDirectory.path).initStorage; await GetStorage("update", settingsDirectory.path).initStorage;
await GetStorage("settings", settingsDirectory.path).initStorage; await GetStorage("settings", settingsDirectory.path).initStorage;
await GetStorage("hosting", settingsDirectory.path).initStorage; await GetStorage("hosting", settingsDirectory.path).initStorage;
Get.put(GameController());
Get.put(BackendController());
Get.put(BuildController());
Get.put(SettingsController());
Get.put(HostingController());
Get.put(InfoController());
Get.put(UpdateController());
return null;
}catch(error) { }catch(error) {
return error; appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
} }
try {
Get.put(GameController());
}catch(error) {
errors.add(error);
}
try {
Get.put(BackendController());
}catch(error) {
errors.add(error);
}
try {
Get.put(BuildController());
}catch(error) {
errors.add(error);
}
try {
Get.put(HostingController());
}catch(error) {
errors.add(error);
}
try {
Get.put(UpdateController());
}catch(error) {
errors.add(error);
}
try {
Get.put(SettingsController());
}catch(error) {
errors.add(error);
}
return errors;
} }
class RebootApplication extends StatefulWidget { class RebootApplication extends StatefulWidget {
const RebootApplication({Key? key}) : super(key: key); final List<Object> errors;
const RebootApplication({Key? key, required this.errors}) : super(key: key);
@override @override
State<RebootApplication> createState() => _RebootApplicationState(); State<RebootApplication> createState() => _RebootApplicationState();
@@ -227,6 +252,16 @@ class RebootApplication extends StatefulWidget {
class _RebootApplicationState extends State<RebootApplication> { class _RebootApplicationState extends State<RebootApplication> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => _handleErrors(widget.errors));
}
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
}
@override @override
Widget build(BuildContext context) => Obx(() => FluentApp( Widget build(BuildContext context) => Obx(() => FluentApp(
locale: Locale(_settingsController.language.value), locale: Locale(_settingsController.language.value),

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ class SettingsController extends GetxController {
gameServerDll = _createController("game_server", "reboot.dll"); gameServerDll = _createController("game_server", "reboot.dll");
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll"); unrealEngineConsoleDll = _createController("unreal_engine_console", "console.dll");
backendDll = _createController("backend", "cobalt.dll"); backendDll = _createController("backend", "cobalt.dll");
memoryLeakDll = _createController("memory_leak", "memoryleak.dll"); memoryLeakDll = _createController("memory_leak", "memory.dll");
gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort); gameServerPort = TextEditingController(text: _storage.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text)); gameServerPort.addListener(() => _storage.write("game_server_port", gameServerPort.text));
width = _storage.read("width") ?? kDefaultWindowWidth; width = _storage.read("width") ?? kDefaultWindowWidth;
@@ -67,5 +67,5 @@ class SettingsController extends GetxController {
firstRun.value = true; firstRun.value = true;
} }
String _controllerDefaultPath(String name) => "${assetsDirectory.path}\\dlls\\$name"; String _controllerDefaultPath(String name) => "${dllsDirectory.path}\\$name";
} }

View File

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

View File

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

View File

@@ -53,6 +53,7 @@ extension ServerControllerDialog on BackendController {
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
case ServerResultType.startError: case ServerResultType.startError:
print(event.stackTrace);
return showInfoBar( return showInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,

View File

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

View File

@@ -1,15 +1,53 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/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';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/info.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/widget/info_tile.dart';
class InfoPage extends RebootPage { class InfoPage extends RebootPage {
static late final List<InfoTile> _infoTiles;
static Object? initInfoTiles() {
try {
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
final map = SplayTreeMap<int, InfoTile>();
for(final entry in directory.listSync()) {
if(entry is File) {
final name = Uri.decodeQueryComponent(path.basename(entry.path));
final splitter = name.indexOf(".");
if(splitter == -1) {
continue;
}
final index = int.tryParse(name.substring(0, splitter));
if(index == null) {
continue;
}
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
map[index] = InfoTile(
title: Text(questionName),
content: Text(entry.readAsStringSync())
);
}
}
_infoTiles = map.values.toList(growable: false);
return null;
}catch(error) {
_infoTiles = [];
return error;
}
}
const InfoPage({Key? key}) : super(key: key); const InfoPage({Key? key}) : super(key: key);
@override @override
@@ -30,10 +68,12 @@ class InfoPage extends RebootPage {
class _InfoPageState extends RebootPageState<InfoPage> { class _InfoPageState extends RebootPageState<InfoPage> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
RxInt _counter = RxInt(180); RxInt _counter = RxInt(kDebugMode ? 0 : 180);
late bool _showButton;
@override @override
void initState() { void initState() {
_showButton = _settingsController.firstRun.value;
if(_settingsController.firstRun.value) { if(_settingsController.firstRun.value) {
Timer.periodic(const Duration(seconds: 1), (timer) { Timer.periodic(const Duration(seconds: 1), (timer) {
if (_counter.value <= 0) { if (_counter.value <= 0) {
@@ -48,14 +88,15 @@ class _InfoPageState extends RebootPageState<InfoPage> {
} }
@override @override
List<Widget> get settings => infoTiles; List<Widget> get settings => InfoPage._infoTiles;
@override @override
Widget? get button => Obx(() { Widget? get button {
if(!_settingsController.firstRun.value) { if(!_showButton) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Obx(() {
final totalSecondsLeft = _counter.value; final totalSecondsLeft = _counter.value;
final minutesLeft = totalSecondsLeft ~/ 60; final minutesLeft = totalSecondsLeft ~/ 60;
final secondsLeft = totalSecondsLeft % 60; final secondsLeft = totalSecondsLeft % 60;
@@ -63,7 +104,10 @@ class _InfoPageState extends RebootPageState<InfoPage> {
width: double.infinity, width: double.infinity,
height: 48, height: 48,
child: Button( child: Button(
onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null, onPressed: totalSecondsLeft <= 0 ? () {
_showButton = false;
pageIndex.value = RebootPageType.play.index;
} : null,
child: Text( child: Text(
totalSecondsLeft <= 0 ? "I have read the instructions" totalSecondsLeft <= 0 ? "I have read the instructions"
: "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}" : "Read the instructions for at least ${secondsLeft == 0 ? '$minutesLeft minute${minutesLeft > 1 ? 's' : ''}' : minutesLeft == 0 ? '$secondsLeft second${secondsLeft > 1 ? 's' : ''}' : '$minutesLeft minute${minutesLeft > 1 ? 's' : ''} and $secondsLeft second${secondsLeft > 1 ? 's' : ''}'}"
@@ -72,3 +116,4 @@ class _InfoPageState extends RebootPageState<InfoPage> {
); );
}); });
} }
}

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.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_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/controller/update_controller.dart'; import 'package:reboot_launcher/src/controller/update_controller.dart';
import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart'; import 'package:reboot_launcher/src/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
@@ -11,40 +13,51 @@ import 'package:reboot_launcher/src/util/translations.dart';
final UpdateController _updateController = Get.find<UpdateController>(); final UpdateController _updateController = Get.find<UpdateController>();
final Map<String, Future<void>> _operations = {}; final Map<String, Future<void>> _operations = {};
Future<void> downloadCriticalDllInteractive(String filePath) { Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
final old = _operations[filePath]; final old = _operations[filePath];
if(old != null) { if(old != null) {
return old; return old;
} }
final newRun = _downloadCriticalDllInteractive(filePath); final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun; _operations[filePath] = newRun;
return newRun; return newRun;
} }
Future<void> _downloadCriticalDllInteractive(String filePath) async { Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = path.basename(filePath).toLowerCase(); final fileName = path.basename(filePath).toLowerCase();
InfoBarEntry? entry; InfoBarEntry? entry;
try { try {
if (fileName == "reboot.dll") { if (fileName == "reboot.dll") {
await _updateController.updateReboot(true); await _updateController.updateReboot(
silent: silent
);
return;
}
if(File(filePath).existsSync()) {
return; return;
} }
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath); final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
if(!silent) {
entry = showInfoBar( entry = showInfoBar(
translations.downloadingDll(fileNameWithoutExtension), translations.downloadingDll(fileNameWithoutExtension),
loading: true, loading: true,
duration: null duration: null
); );
}
await downloadCriticalDll(fileName, filePath); await downloadCriticalDll(fileName, filePath);
entry.close(); entry?.close();
if(!silent) {
entry = await showInfoBar( entry = await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension), translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
}
}catch(message) { }catch(message) {
if(!silent) {
entry?.close(); entry?.close();
var error = message.toString(); var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error; error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
@@ -64,7 +77,31 @@ Future<void> _downloadCriticalDllInteractive(String filePath) async {
) )
); );
await completer.future; await completer.future;
}
}finally { }finally {
_operations.remove(fileName); _operations.remove(fileName);
} }
} }
extension InjectableDllExtension on InjectableDll {
String get path {
final SettingsController settingsController = Get.find<SettingsController>();
switch(this){
case InjectableDll.reboot:
if(_updateController.customGameServer.value) {
final file = File(settingsController.gameServerDll.text);
if(file.existsSync()) {
return file.path;
}
}
return rebootDllFile.path;
case InjectableDll.console:
return settingsController.unrealEngineConsoleDll.text;
case InjectableDll.cobalt:
return settingsController.backendDll.text;
case InjectableDll.memory:
return settingsController.memoryLeakDll.text;
}
}
}

View File

@@ -1,41 +0,0 @@
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/log.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_tile.dart';
final _entries = SplayTreeMap<int, InfoTile>();
void initInfoTiles() {
try {
final directory = Directory("${assetsDirectory.path}\\info\\$currentLocale");
for(final entry in directory.listSync()) {
if(entry is File) {
final name = Uri.decodeQueryComponent(path.basename(entry.path));
final splitter = name.indexOf(".");
if(splitter == -1) {
continue;
}
final index = int.tryParse(name.substring(0, splitter));
if(index == null) {
continue;
}
final questionName = Uri.decodeQueryComponent(name.substring(splitter + 2));
_entries[index] = InfoTile(
title: Text(questionName),
content: Text(entry.readAsStringSync())
);
}
}
}catch(error) {
log("[INFO] Error occurred while initializing info tiles: $error");
}
}
List<InfoTile> get infoTiles => _entries.values.toList(growable: false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "9.1.0" version: "9.1.3"
publish_to: 'none' publish_to: 'none'
@@ -90,7 +90,6 @@ flutter:
uses-material-design: true uses-material-design: true
generate: true generate: true
assets: assets:
- assets/dlls/
- assets/icons/ - assets/icons/
- assets/images/ - assets/images/
- assets/backend/ - assets/backend/

View File

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