5 Commits
9.1.0 ... 9.1.4

Author SHA1 Message Date
Alessandro Autiero
0a775e2f3f Merge pull request #52 from Auties00/_onLoggedIn
9.1.3
2024-06-04 22:32:10 +02:00
Alessandro Autiero
2bf084d120 9.1.3 2024-06-04 20:31:06 +02:00
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)
![Screenshot (35)](https://github.com/Auties00/reboot_launcher/assets/28218457/de43d2b8-09fc-4d34-beb1-aa6f7fcaa479)
![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)
![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)
![Screenshot (33)](https://github.com/Auties00/reboot_launcher/assets/28218457/6c449aa6-e515-4680-9ee2-d219761f3268)
GUI and CLI Launcher for [Project Reboot](https://github.com/Milxnor/Project-Reboot-3.0/)
Join our discord at https://discord.gg/reboot
## Modules
- COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- 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/server.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/util/matchmaker.dart' as matchmaker;
late String? username;
late bool host;
@@ -82,7 +81,7 @@ void main(List<String> args) async {
return;
}
matchmaker.writeMatchmakingIp(result["matchmaking-address"]);
writeMatchmakingIp(result["matchmaking-address"]);
autoRestart = result["auto-restart"];
await startGame();
}

View File

@@ -24,7 +24,7 @@ Future<void> startGame() async {
_gameProcess = await Process.start(executable.path, createRebootArgs(username!, "", host, host, ""))
..exitCode.then((_) => _onClose())
..outLines.forEach((line) => _onGameOutput(line, dll, host, verbose));
..stdOutput.forEach((line) => _onGameOutput(line, dll, host, verbose));
_injectOrShowError("cobalt.dll");
}
@@ -52,6 +52,17 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
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)) {
_onClose();
return;
@@ -70,7 +81,7 @@ void _onGameOutput(String line, String dll, bool hosting, bool verbose) {
_injectOrShowError("console.dll");
}
_injectOrShowError("memoryleak.dll");
_injectOrShowError("memory.dll");
}
}
@@ -87,12 +98,12 @@ Future<void> _injectOrShowError(String binary, [bool locate = true]) async {
try {
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()){
throw Exception("Cannot inject $dll: missing file");
}
await injectDll(_gameProcess!.pid, dll.path);
await injectDll(_gameProcess!.pid, dll);
} catch (exception) {
throw Exception("Cannot inject binary: $binary");
}

View File

@@ -7,12 +7,12 @@ import 'package:reboot_common/common.dart';
// TODO: Use github
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 _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";
Future<void> downloadRequiredDLLs() async {
stdout.writeln("Downloading necessary components...");
var consoleDll = File("${assetsDirectory.path}\\dlls\\console.dll");
var consoleDll = File("${dllsDirectory.path}\\console.dll");
if(!consoleDll.existsSync()){
var response = await http.get(Uri.parse(_consoleDownload));
if(response.statusCode != 200){
@@ -22,7 +22,7 @@ Future<void> downloadRequiredDLLs() async {
await consoleDll.writeAsBytes(response.bodyBytes);
}
var craniumDll = File("${assetsDirectory.path}\\dlls\\cobalt.dll");
var craniumDll = File("${dllsDirectory.path}\\cobalt.dll");
if(!craniumDll.existsSync()){
var response = await http.get(Uri.parse(_baseDownload));
if(response.statusCode != 200){
@@ -32,11 +32,11 @@ Future<void> downloadRequiredDLLs() async {
await craniumDll.writeAsBytes(response.bodyBytes);
}
var memoryFixDll = File("${assetsDirectory.path}\\dlls\\memoryleak.dll");
var memoryFixDll = File("${dllsDirectory.path}\\memory.dll");
if(!memoryFixDll.existsSync()){
var response = await http.get(Uri.parse(_memoryFixDownload));
if(response.statusCode != 200){
throw Exception("Cannot download memoryleak.dll");
throw Exception("Cannot download memory.dll");
}
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/update_status.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/build.dart';
export 'package:reboot_common/src/util/dll.dart';

View File

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

View File

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

View File

@@ -145,9 +145,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
@@ -166,6 +168,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
@@ -183,10 +190,12 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(startTime, now, 100, true, options);
process?.kill(ProcessSignal.sigabrt);
return;
@@ -205,6 +214,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
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';
bool _watcher = false;
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll");
const String kRebootDownloadUrl =
"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 {
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) {
throw Exception("Cannot download $name: status code ${response.statusCode}");
}

View File

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

View File

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

View File

@@ -33,7 +33,10 @@ final _CreateRemoteThread = _kernel32.lookupFunction<
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
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(
0x43A,
0,
@@ -52,7 +55,7 @@ Future<void> injectDll(int pid, String dll) async {
final dllAddress = VirtualAllocEx(
process,
nullptr,
dll.length + 1,
dllPath.length + 1,
0x3000,
0x4
);
@@ -60,8 +63,8 @@ Future<void> injectDll(int pid, String dll) async {
final writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
dll.toNativeUtf8(),
dll.length,
dllPath.toNativeUtf8(),
dllPath.length,
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 {
final argsOrEmpty = args ?? [];
if(wrapProcess) {
@@ -223,6 +238,31 @@ List<String> createRebootArgs(String username, String password, bool host, bool
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) {
if(host) {
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.
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.
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.
Just like in Minecraft, you need a game client to play the game and one to host the server."
Project Reboot is a game server for Fortnite that aims to support as many seasons as possible.
The project was started on Discord by Milxnor, while the launcher is developed by Auties00.
Both are open source on GitHub, anyone can easily contribute or audit the code!"

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.
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.
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",
"corruptedVersionError": "Corrupted Fortnite installation: please download it again from the launcher or change version",
"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}",
"serverNoLongerAvailable": "{owner}'s 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/game_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/update_controller.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/server.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/os.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:version/version.dart';
import 'package:window_manager/window_manager.dart';
import 'package:win32/win32.dart';
const double kDefaultWindowWidth = 1536;
const double kDefaultWindowHeight = 1024;
const double kDefaultWindowWidth = 1164;
const double kDefaultWindowHeight = 864;
const String kCustomUrlSchema = "Reboot";
Version? appVersion;
bool appWithNoStorage = false;
class _MyHttpOverrides extends HttpOverrides {
@override
HttpClient createHttpClient(SecurityContext? context){
return super.createHttpClient(context)
..badCertificateCallback = ((X509Certificate cert, String host, int port) => true);
void main() {
log("[APP] Called");
runZonedGuarded(
() => _startApp(),
(error, stack) => onError(error, stack, false),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
}
Future<void> _startApp() async {
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,
));
}
}
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);
}
Future<Object?> _initNotifications() async {
try {
await localNotifier.setup(
appName: 'Reboot Launcher',
shortcutPolicy: ShortcutPolicy.ignore
);
return null;
}catch(error) {
return error;
}
}
final storageError = await _initStorage();
if(storageError != null) {
errors.add(storageError);
}
Future<Object?> _initDatabase() async {
try {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseAnonKey
);
return null;
}catch(error) {
return error;
}
}
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),
zoneSpecification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stacktrace) => onError(error, stacktrace, false)
)
);
void _handleErrors(List<Object?> errors) {
errors.where((element) => element != null).forEach((element) => onError(element!, null, false));
Future<Object?> _initPath() async {
try {
await installationDirectory.create(recursive: true);
return null;
}catch(error) {
return error;
}
}
Future<Object?> _initVersion() async {
@@ -109,116 +146,104 @@ 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 {
try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']);
var appLinks = AppLinks();
var initialUrl = await appLinks.getInitialLink();
if(initialUrl != null) {
_joinServer(initialUrl);
}
appLinks.uriLinkStream.listen(_joinServer);
return null;
}catch(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 {
await windowManager.ensureInitialized();
await Window.initialize();
var settingsController = Get.find<SettingsController>();
var size = Size(settingsController.width, settingsController.height);
appWindow.size = size;
var offsetX = settingsController.offsetX;
var offsetY = settingsController.offsetY;
if(offsetX != null && offsetY != null){
appWindow.position = Offset(
offsetX,
offsetY
);
}else {
appWindow.alignment = Alignment.center;
}
try {
await SystemTheme.accentColor.load();
await windowManager.ensureInitialized();
await Window.initialize();
var settingsController = Get.find<SettingsController>();
var size = Size(settingsController.width, settingsController.height);
appWindow.size = size;
var offsetX = settingsController.offsetX;
var offsetY = settingsController.offsetY;
if(offsetX != null && offsetY != null){
appWindow.position = Offset(
offsetX,
offsetY
);
}else {
appWindow.alignment = Alignment.center;
}
if(isWin11) {
await Window.setEffect(
effect: WindowEffect.acrylic,
color: Colors.transparent,
dark: SchedulerBinding.instance.platformDispatcher.platformBrightness.isDark
);
if(isWin11) {
await Window.setEffect(
effect: WindowEffect.acrylic,
color: Colors.transparent,
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 {
await GetStorage("game", settingsDirectory.path).initStorage;
await GetStorage("backend", settingsDirectory.path).initStorage;
await GetStorage("update", settingsDirectory.path).initStorage;
await GetStorage("settings", 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) {
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 {
const RebootApplication({Key? key}) : super(key: key);
final List<Object> errors;
const RebootApplication({Key? key, required this.errors}) : super(key: key);
@override
State<RebootApplication> createState() => _RebootApplicationState();
@@ -227,6 +252,16 @@ class RebootApplication extends StatefulWidget {
class _RebootApplicationState extends State<RebootApplication> {
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
Widget build(BuildContext context) => Obx(() => FluentApp(
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_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
class BackendController extends GetxController {
late final GetStorage storage;
late final GetStorage? storage;
late final TextEditingController host;
late final TextEditingController port;
late final Rx<ServerType> type;
@@ -21,13 +22,13 @@ class BackendController extends GetxController {
HttpServer? remoteServer;
BackendController() {
storage = GetStorage("backend");
storage = appWithNoStorage ? null : GetStorage("backend");
started = RxBool(false);
type = Rx(ServerType.values.elementAt(storage.read("type") ?? 0));
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0));
type.listen((value) {
host.text = _readHost();
port.text = _readPort();
storage.write("type", value.index);
storage?.write("type", value.index);
if (!started.value) {
return;
}
@@ -36,13 +37,13 @@ class BackendController extends GetxController {
});
host = TextEditingController(text: _readHost());
host.addListener(() =>
storage.write("${type.value.name}_host", host.text));
storage?.write("${type.value.name}_host", host.text));
port = TextEditingController(text: _readPort());
port.addListener(() =>
storage.write("${type.value.name}_port", port.text));
detached = RxBool(storage.read("detached") ?? false);
detached.listen((value) => storage.write("detached", value));
gameServerAddress = TextEditingController(text: storage.read("game_server_address") ?? "127.0.0.1");
storage?.write("${type.value.name}_port", port.text));
detached = RxBool(storage?.read("detached") ?? false);
detached.listen((value) => storage?.write("detached", value));
gameServerAddress = TextEditingController(text: storage?.read("game_server_address") ?? "127.0.0.1");
var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue);
gameServerAddress.addListener(() {
@@ -53,7 +54,7 @@ class BackendController extends GetxController {
lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
storage.write("game_server_address", newValue);
storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue);
});
watchMatchmakingIp().listen((event) {
@@ -62,15 +63,15 @@ class BackendController extends GetxController {
}
});
gameServerAddressFocusNode = FocusNode();
gameServerOwner = RxnString(storage.read("game_server_owner"));
gameServerOwner.listen((value) => storage.write("game_server_owner", value));
gameServerOwner = RxnString(storage?.read("game_server_owner"));
gameServerOwner.listen((value) => storage?.write("game_server_owner", value));
}
void reset() async {
type.value = ServerType.values.elementAt(0);
for (final type in ServerType.values) {
storage.write("${type.name}_host", null);
storage.write("${type.name}_port", null);
storage?.write("${type.name}_host", null);
storage?.write("${type.name}_port", null);
}
host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
@@ -79,7 +80,7 @@ class BackendController extends GetxController {
}
String _readHost() {
String? value = storage.read("${type.value.name}_host");
String? value = storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) {
return value;
}
@@ -92,7 +93,7 @@ class BackendController extends GetxController {
}
String _readPort() =>
storage.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start() async* {
try {

View File

@@ -9,10 +9,12 @@ import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
import '../../main.dart';
class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage _storage;
late final GetStorage? _storage;
late final TextEditingController username;
late final TextEditingController password;
late final TextEditingController customLaunchArgs;
@@ -23,38 +25,37 @@ class GameController extends GetxController {
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() {
_storage = GetStorage("game");
Iterable decodedVersionsJson = jsonDecode(
_storage.read("versions") ?? "[]");
var decodedVersions = decodedVersionsJson
_storage = appWithNoStorage ? null : GetStorage("game");
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
.toList();
versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions());
var decodedSelectedVersionName = _storage.read("version");
var decodedSelectedVersion = decodedVersions.firstWhereOrNull((
final decodedSelectedVersionName = _storage?.read("version");
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((
element) => element.name == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion);
username = TextEditingController(
text: _storage.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage.write("username", username.text));
password = TextEditingController(text: _storage.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage.read("custom_launch_args") ?? "");
text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage?.write("username", username.text));
password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage?.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() =>
_storage.write("custom_launch_args", customLaunchArgs.text));
_storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false);
instance = Rxn();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage.write("console_key", newValue.usbHidUsage);
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage.read("console_key");
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
@@ -113,7 +114,7 @@ class GameController extends GetxController {
Future<void> _saveVersions() async {
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;
@@ -124,7 +125,7 @@ class GameController extends GetxController {
set selectedVersion(FortniteVersion? version) {
_selectedVersion.value = version;
_storage.write("version", version?.name);
_storage?.write("version", version?.name);
}
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_storage/get_storage.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
class HostingController extends GetxController {
late final GetStorage _storage;
late final GetStorage? _storage;
late final String uuid;
late final TextEditingController name;
late final TextEditingController description;
@@ -22,23 +23,23 @@ class HostingController extends GetxController {
late final Rxn<Set<Map<String, dynamic>>> servers;
HostingController() {
_storage = GetStorage("hosting");
uuid = _storage.read("uuid") ?? const Uuid().v4();
_storage.write("uuid", uuid);
name = TextEditingController(text: _storage.read("name"));
name.addListener(() => _storage.write("name", name.text));
description = TextEditingController(text: _storage.read("description"));
description.addListener(() => _storage.write("description", description.text));
password = TextEditingController(text: _storage.read("password") ?? "");
password.addListener(() => _storage.write("password", password.text));
discoverable = RxBool(_storage.read("discoverable") ?? false);
discoverable.listen((value) => _storage.write("discoverable", value));
headless = RxBool(_storage.read("headless") ?? true);
headless.listen((value) => _storage.write("headless", value));
virtualDesktop = RxBool(_storage.read("virtual_desktop") ?? true);
virtualDesktop.listen((value) => _storage.write("virtual_desktop", value));
autoRestart = RxBool(_storage.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage.write("auto_restart", value));
_storage = appWithNoStorage ? null : GetStorage("hosting");
uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage?.write("uuid", uuid);
name = TextEditingController(text: _storage?.read("name"));
name.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage?.read("description"));
description.addListener(() => _storage?.write("description", description.text));
password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage?.write("password", password.text));
discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value));
headless = RxBool(_storage?.read("headless") ?? true);
headless.listen((value) => _storage?.write("headless", value));
virtualDesktop = RxBool(_storage?.read("virtual_desktop") ?? true);
virtualDesktop.listen((value) => _storage?.write("virtual_desktop", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false);
published = 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");
unrealEngineConsoleDll = _createController("unreal_engine_console", "console.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.addListener(() => _storage.write("game_server_port", gameServerPort.text));
width = _storage.read("width") ?? kDefaultWindowWidth;
@@ -67,5 +67,5 @@ class SettingsController extends GetxController {
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';
class UpdateController {
late final GetStorage _storage;
late final GetStorage? _storage;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Rx<UpdateTimer> timer;
@@ -21,17 +21,17 @@ class UpdateController {
Future? _updater;
UpdateController() {
_storage = GetStorage("update");
timestamp = RxnInt(_storage.read("ts"));
timestamp.listen((value) => _storage.write("ts", value));
var timerIndex = _storage.read("timer");
_storage = appWithNoStorage ? null : GetStorage("update");
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
var timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage.write("timer", value.index));
url = TextEditingController(text: _storage.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage.write("update_url", url.text));
timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage.write("custom_game_server", value));
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
}
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) {
return await _updater;
}
final result = _updateReboot(force);
final result = _updateReboot(force, silent);
_updater = result;
return await result;
}
Future<void> _updateReboot([bool force = false]) async {
Future<void> _updateReboot(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
@@ -92,34 +92,44 @@ class UpdateController {
return;
}
infoBarEntry = showInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
if(!silent) {
infoBarEntry = showInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
infoBarEntry = showInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
if(!silent) {
infoBarEntry = showInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(true),
child: Text(translations.downloadDllRetry),
)
);
if(!silent) {
infoBarEntry?.close();
var error = message.toString();
error =
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
}
}finally {
_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/util/translations.dart';
import '../../util/log.dart';
String? lastError;
void onError(Object exception, StackTrace? stackTrace, bool framework) {
if(!kDebugMode) {
return;
}
log("[ERROR] $exception");
log("[STACKTRACE] $stackTrace");
if(pageKey.currentContext == null || pageKey.currentState?.mounted == false){
return;
}

View File

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

View File

@@ -2,20 +2,26 @@ import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_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/info_bar.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_suggestion.dart';
import 'package:reboot_launcher/src/page/pages.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/translations.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 {
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 UpdateController _updateController = Get.find<UpdateController>();
final GlobalKey _searchKey = GlobalKey();
@@ -45,15 +53,81 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override
void initState() {
super.initState();
windowManager.addListener(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
_updateController.notifyLauncherUpdate();
_updateController.updateReboot();
watchDlls().listen((filePath) => showDllDeletedDialog(() {
downloadCriticalDllInteractive(filePath);
}));
_checkUpdates();
_initAppLink();
_checkGameServer();
});
super.initState();
}
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();
if(!dllsDirectory.existsSync()) {
dllsDirectory.createSync(recursive: true);
}
for(final injectable in InjectableDll.values) {
downloadCriticalDllInteractive(
injectable.path,
silent: true
);
}
watchDlls().listen((filePath) => showDllDeletedDialog(() {
downloadCriticalDllInteractive(filePath);
}));
}
@override
@@ -147,54 +221,55 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
super.build(context);
_settingsController.language.value;
loadTranslations(context);
return Obx(() => NavigationPaneTheme(
data: NavigationPaneThemeData(
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
),
child: NavigationView(
paneBodyBuilder: (pane, body) => _PaneBody(
padding: _kDefaultPadding,
controller: pagesController,
body: body
),
appBar: NavigationAppBar(
height: 32,
title: _draggableArea,
actions: WindowTitleBar(focused: _focused()),
leading: _backButton,
automaticallyImplyLeading: false,
),
pane: NavigationPane(
selected: pageIndex.value,
onChanged: (index) {
final lastPageIndex = pageIndex.value;
if(lastPageIndex != index) {
pageIndex.value = index;
}else if(pageStack.isNotEmpty) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
pagesController.add(null);
}
},
menuButton: const SizedBox(),
displayMode: PaneDisplayMode.open,
items: _items,
customPane: _CustomPane(_settingsController),
header: const ProfileWidget(),
autoSuggestBox: _autoSuggestBox,
indicator: const StickyNavigationIndicator(
duration: Duration(milliseconds: 500),
curve: Curves.easeOut,
indicatorSize: 3.25
)
),
contentShape: const RoundedRectangleBorder(),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
)
),
);
return Obx(() {
return NavigationPaneTheme(
data: NavigationPaneThemeData(
backgroundColor: FluentTheme.of(context).micaBackgroundColor.withOpacity(0.93),
),
child: NavigationView(
paneBodyBuilder: (pane, body) => _PaneBody(
padding: _kDefaultPadding,
controller: pagesController,
body: body
),
appBar: NavigationAppBar(
height: 32,
title: _draggableArea,
actions: WindowTitleBar(focused: _focused()),
leading: _backButton,
automaticallyImplyLeading: false,
),
pane: NavigationPane(
selected: pageIndex.value,
onChanged: (index) {
final lastPageIndex = pageIndex.value;
if(lastPageIndex != index) {
pageIndex.value = index;
}else if(pageStack.isNotEmpty) {
Navigator.of(pageKey.currentContext!).pop();
final element = pageStack.removeLast();
appStack.remove(element);
pagesController.add(null);
}
},
menuButton: const SizedBox(),
displayMode: PaneDisplayMode.open,
items: _items,
customPane: _CustomPane(_settingsController),
header: const ProfileWidget(),
autoSuggestBox: _autoSuggestBox,
indicator: const StickyNavigationIndicator(
duration: Duration(milliseconds: 500),
curve: Curves.easeOut,
indicatorSize: 3.25
)
),
contentShape: const RoundedRectangleBorder(),
onOpenSearch: () => _searchFocusNode.requestFocus(),
transitionBuilder: (child, animation) => child
)
);
});
}
Widget get _backButton => StreamBuilder(

View File

@@ -1,15 +1,53 @@
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:flutter/foundation.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/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.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:path/path.dart' as path;
import 'package:reboot_launcher/src/widget/info_tile.dart';
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);
@override
@@ -30,10 +68,12 @@ class InfoPage extends RebootPage {
class _InfoPageState extends RebootPageState<InfoPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
RxInt _counter = RxInt(180);
RxInt _counter = RxInt(kDebugMode ? 0 : 180);
late bool _showButton;
@override
void initState() {
_showButton = _settingsController.firstRun.value;
if(_settingsController.firstRun.value) {
Timer.periodic(const Duration(seconds: 1), (timer) {
if (_counter.value <= 0) {
@@ -48,27 +88,32 @@ class _InfoPageState extends RebootPageState<InfoPage> {
}
@override
List<Widget> get settings => infoTiles;
List<Widget> get settings => InfoPage._infoTiles;
@override
Widget? get button => Obx(() {
if(!_settingsController.firstRun.value) {
Widget? get button {
if(!_showButton) {
return const SizedBox.shrink();
}
final totalSecondsLeft = _counter.value;
final minutesLeft = totalSecondsLeft ~/ 60;
final secondsLeft = totalSecondsLeft % 60;
return SizedBox(
width: double.infinity,
height: 48,
child: Button(
onPressed: totalSecondsLeft <= 0 ? () => pageIndex.value = RebootPageType.play.index : null,
child: Text(
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' : ''}'}"
),
)
);
});
return Obx(() {
final totalSecondsLeft = _counter.value;
final minutesLeft = totalSecondsLeft ~/ 60;
final secondsLeft = totalSecondsLeft % 60;
return SizedBox(
width: double.infinity,
height: 48,
child: Button(
onPressed: totalSecondsLeft <= 0 ? () {
_showButton = false;
pageIndex.value = RebootPageType.play.index;
} : null,
child: Text(
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' : ''}'}"
),
)
);
});
}
}

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

View File

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

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' as path;
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/dialog/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
@@ -11,60 +13,95 @@ import 'package:reboot_launcher/src/util/translations.dart';
final UpdateController _updateController = Get.find<UpdateController>();
final Map<String, Future<void>> _operations = {};
Future<void> downloadCriticalDllInteractive(String filePath) {
Future<void> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
final old = _operations[filePath];
if(old != null) {
return old;
}
final newRun = _downloadCriticalDllInteractive(filePath);
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<void> _downloadCriticalDllInteractive(String filePath) async {
Future<void> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = path.basename(filePath).toLowerCase();
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
await _updateController.updateReboot(true);
await _updateController.updateReboot(
silent: silent
);
return;
}
if(File(filePath).existsSync()) {
return;
}
final fileNameWithoutExtension = path.basenameWithoutExtension(filePath);
entry = showInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
if(!silent) {
entry = showInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry.close();
entry = await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}catch(message) {
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
if(!silent) {
entry = await showInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
}catch(message) {
if(!silent) {
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
}
}finally {
_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 {
await _semaphore.acquire();
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
_semaphore.release();
try {
await _semaphore.acquire();
print(message);
await _loggingFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
}catch(error) {
print(error);
}finally {
_semaphore.release();
}
}

View File

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

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:async/async.dart';
import 'package:dart_ipify/dart_ipify.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:local_notifier/local_notifier.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/hosting_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_button.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 {
final bool host;
final String? startLabel;
final String? stopLabel;
final String startLabel;
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
State<LaunchButton> createState() => _LaunchButtonState();
@@ -43,7 +43,6 @@ class _LaunchButtonState extends State<LaunchButton> {
final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>();
final UpdateController _updateController = Get.find<UpdateController>();
InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
@@ -60,52 +59,42 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () => _operation = CancelableOperation.fromFuture(_toggle()),
child: Align(
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;
String get _startMessage => widget.startLabel ?? (widget.host ? translations.startHosting : translations.startGame);
String get _stopMessage => widget.stopLabel ?? (widget.host ? translations.stopHosting : translations.stopGame);
Future<void> _toggle({bool forceGUI = false}) async {
log("[${widget.host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
if (_hasStarted) {
log("[${widget.host ? 'HOST' : 'GAME'}] User asked to close the current instance");
Future<void> _toggle({bool? host, bool forceGUI = false}) async {
host ??= widget.host;
log("[${host ? 'HOST' : 'GAME'}] Toggling state(forceGUI: $forceGUI)");
if (host ? _hostingController.started() : _gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(
reason: _StopReason.normal
);
return;
}
if(_operation != null) {
log("[${widget.host ? 'HOST' : 'GAME'}] Already started, ignoring user action");
return;
}
final version = _gameController.selectedVersion;
log("[${widget.host ? 'HOST' : 'GAME'}] Version data: $version");
log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){
log("[${widget.host ? 'HOST' : 'GAME'}] No version selected");
log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop(
reason: _StopReason.missingVersionError
);
return;
}
log("[${widget.host ? 'HOST' : 'GAME'}] Setting started...");
_setStarted(widget.host, true);
log("[${widget.host ? 'HOST' : 'GAME'}] Set started");
log("[${widget.host ? 'HOST' : 'GAME'}] Checking dlls: ${_Injectable.values}");
for (final injectable in _Injectable.values) {
if(await _getDllFileOrStop(injectable, widget.host) == null) {
log("[${host ? 'HOST' : 'GAME'}] Setting started...");
_setStarted(host, true);
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, host) == null) {
return;
}
}
@@ -113,7 +102,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try {
final executable = version.gameExecutable;
if(executable == null){
log("[${widget.host ? 'HOST' : 'GAME'}] No executable found");
log("[${host ? 'HOST' : 'GAME'}] No executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: version.location.path
@@ -121,27 +110,27 @@ class _LaunchButtonState extends State<LaunchButton> {
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();
if(!backendResult){
log("[${widget.host ? 'HOST' : 'GAME'}] Cannot start backend");
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
reason: _StopReason.backendError
);
return;
}
log("[${widget.host ? 'HOST' : 'GAME'}] Backend works");
log("[${host ? 'HOST' : 'GAME'}] Backend works");
final headless = !forceGUI && _hostingController.headless.value;
final virtualDesktop = _hostingController.virtualDesktop.value;
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, headless, virtualDesktop, false);
log("[${widget.host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
await _startGameProcesses(version, widget.host, headless, virtualDesktop, linkedHostingInstance);
if(!widget.host) {
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, virtualDesktop, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
await _startGameProcesses(version, host, headless, virtualDesktop, linkedHostingInstance);
if(!host) {
_showLaunchingGameClientWidget();
}
if(linkedHostingInstance != null || widget.host){
if(linkedHostingInstance != null || host){
_showLaunchingGameServerWidget();
}
} catch (exception, stackTrace) {
@@ -153,34 +142,34 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
log("[${widget.host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(widget.host){
log("[${widget.host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, bool virtualDesktop, bool forceLinkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Checking if a server needs to be started automatically...");
if(host){
log("[${host ? 'HOST' : 'GAME'}] The user clicked on Start hosting, so it's not necessary");
return null;
}
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;
}
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;
}
final response = forceLinkedHosting || await _askForAutomaticGameServer();
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;
}
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);
log("[${widget.host ? 'HOST' : 'GAME'}] Started implicit game server...");
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
_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;
}
@@ -239,17 +228,12 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{
_gameController.instance.value = instance;
}
await _injectOrShowError(_Injectable.sslBypassV2, host);
await _injectOrShowError(InjectableDll.cobalt, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance;
}
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...");
final gameArgs = createRebootArgs(
_gameController.username.text,
@@ -265,40 +249,52 @@ class _LaunchButtonState extends State<LaunchButton> {
wrapProcess: false,
name: "${version.name}-${host ? 'HOST' : 'GAME'}"
);
gameProcess.stdOutput.listen((line) => _onGameOutput(line, version, host, virtualDesktop, false));
gameProcess.stdError.listen((line) => _onGameOutput(line, version, host, virtualDesktop, true));
watchProcess(gameProcess.pid).then((_) async {
void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError),
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;
if(instance == null) {
log("[${host ? 'HOST' : 'GAME'}] Called exit code, but the game process is no longer running");
return;
}
if(!host || !headless || instance.launched) {
_onStop(reason: _StopReason.exitCode);
if(!host || instance.launched) {
log("[${host ? 'HOST' : 'GAME'}] Called exit code(headless: $headless, launched: ${instance.launched}): stop signal");
_onStop(
reason: _StopReason.exitCode,
host: host
);
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;
}
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 {
if (file == null) {
return null;
@@ -314,74 +310,8 @@ class _LaunchButtonState extends State<LaunchButton> {
return pid;
}
void _onGameOutput(String line, FortniteVersion version, bool host, bool virtualDesktop, bool error) async {
if (line.contains(kShutdownLine)) {
_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) {
Future<void> _onDisplayAttached(bool headless, bool virtualDesktop, FortniteVersion version) async {
if(!headless && virtualDesktop) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
@@ -389,10 +319,18 @@ class _LaunchButtonState extends State<LaunchButton> {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.name} Server (Reboot Launcher)");
var success = false;
try {
await windowManager.moveWindowToDesktop(hostingInstance.gamePid, _virtualDesktop!);
success = await windowManager.moveWindowToDesktop(
hostingInstance.gamePid,
_virtualDesktop!,
excludedWindowName: "Reboot"
);
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
success = false;
}
if(!success) {
try {
windowManager.removeDesktop(_virtualDesktop!);
}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() {
_gameClientInfoBar?.close();
showInfoBar(
@@ -427,11 +422,11 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
final gameServerPort = _settingsController.gameServerPort.text;
_gameServerInfoBar?.close();
final localPingResult = await pingGameServer(
"127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2)
);
_gameServerInfoBar?.close();
if (!localPingResult) {
showInfoBar(
translations.gameServerStartWarning,
@@ -440,7 +435,6 @@ class _LaunchButtonState extends State<LaunchButton> {
);
return;
}
_backendController.joinLocalHost();
final accessible = await _checkGameServer(theme, gameServerPort);
if (!accessible) {
@@ -503,6 +497,20 @@ class _LaunchButtonState extends State<LaunchButton> {
}
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) {
try {
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'}] Caller: ${StackTrace.current}");
if(host) {
_hostingController.discardServer();
}
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null) {
if(reason == _StopReason.normal) {
instance.launched = true;
@@ -534,25 +534,21 @@ class _LaunchButtonState extends State<LaunchButton> {
instance.kill();
final child = instance.child;
if(child != null) {
_onStop(
await _onStop(
reason: reason,
host: child.hosting
);
}
if(host){
_hostingController.instance.value = null;
}else {
_gameController.instance.value = null;
}
}
_setStarted(host, false);
if(host) {
_gameServerInfoBar?.close();
}else {
_gameClientInfoBar?.close();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if(host == true) {
_gameServerInfoBar?.close();
}else {
_gameClientInfoBar?.close();
}
});
switch(reason) {
case _StopReason.backendError:
@@ -574,7 +570,6 @@ class _LaunchButtonState extends State<LaunchButton> {
);
break;
case _StopReason.exitCode:
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(instance != null && !instance.launched) {
showInfoBar(
translations.corruptedVersionError,
@@ -582,7 +577,6 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration,
);
}
break;
case _StopReason.corruptedVersionError:
showInfoBar(
@@ -600,7 +594,7 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.tokenError:
showInfoBar(
translations.tokenError,
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? "none"),
severity: InfoBarSeverity.error,
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;
if (instance == null) {
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}...");
await injectDll(gameProcess, dllPath.path);
await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
} catch (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) {
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 {
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final path = _getDllPath(injectable);
final path = injectable.path;
log("[${host ? 'HOST' : 'GAME'}] Path: $path");
final file = File(path);
if(await file.exists()) {
@@ -712,11 +687,4 @@ enum _StopReason {
exitCode;
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:reboot_common/common.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';
class ServerTypeSelector extends StatefulWidget {
@@ -18,6 +19,7 @@ class _ServerTypeSelectorState extends State<ServerTypeSelector> {
@override
Widget build(BuildContext context) {
return Obx(() => DropDownButton(
onOpen: () => inDialog = true,
leading: Text(_controller.type.value.label),
items: ServerType.values
.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_server_version.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';
class VersionSelector extends StatefulWidget {
@@ -39,16 +40,22 @@ class _VersionSelectorState extends State<VersionSelector> {
@override
Widget build(BuildContext context) => Obx(() {
return _createOptionsMenu(
version: _gameController.selectedVersion,
close: false,
child: FlyoutTarget(
controller: _flyoutController,
child: DropDownButton(
leading: Text(_gameController.selectedVersion?.name ?? translations.selectVersion),
items: _createSelectorItems(context)
),
)
);
version: _gameController.selectedVersion,
close: false,
child: FlyoutTarget(
controller: _flyoutController,
child: DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_gameController.selectedVersion?.name ?? translations.selectVersion,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
items: _createSelectorItems(context)
),
)
);
});
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {

View File

@@ -10,5 +10,11 @@ SettingTile get versionSelectSettingTile => SettingTile(
),
title: Text(translations.selectFortniteName),
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
description: Graphical User Interface for Project Reboot
version: "9.1.0"
version: "9.1.3"
publish_to: 'none'
@@ -90,7 +90,6 @@ flutter:
uses-material-design: true
generate: true
assets:
- assets/dlls/
- assets/icons/
- assets/images/
- assets/backend/

View File

@@ -10,10 +10,10 @@ AppPublisher={{PUBLISHER_NAME}}
AppPublisherURL={{PUBLISHER_URL}}
AppSupportURL={{PUBLISHER_URL}}
AppUpdatesURL={{PUBLISHER_URL}}
DefaultDirName={{INSTALL_DIR_NAME}}
DefaultDirName={autopf}\{{DISPLAY_NAME}}
DisableProgramGroupPage=yes
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
Compression=lzma
Compression=zip
SolidCompression=yes
SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
@@ -28,11 +28,15 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Dirs]
Name: "{app}"; Permissions: everyone-full
[Files]
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full
[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]
Name: "{autoprograms}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"