This commit is contained in:
Alessandro Autiero
2025-03-23 18:25:47 +01:00
parent 4327541ac6
commit 9a000db3b7
68 changed files with 5459 additions and 3542 deletions

View File

@@ -2,13 +2,13 @@
[OnlineSubsystemMcp.Xmpp]
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80
ServerPort=8080
# Do not remove/change, this redirects epicgames xmpp to lawinserver xmpp
[OnlineSubsystemMcp.Xmpp Prod]
bUseSSL=false
ServerAddr="ws://127.0.0.1"
ServerPort=80
ServerPort=8080
# Forces fortnite to use the v1 party system to support lawinserver xmpp
[OnlineSubsystemMcp]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 0 B

View File

@@ -63,10 +63,24 @@
"favorite": false
},
{
"accountId": "Player231",
"accountId": "Player809",
"status": "ACCEPTED",
"direction": "OUTBOUND",
"created": "2024-05-23T20:05:07.478Z",
"created": "2024-05-31T19:09:47.089Z",
"favorite": false
},
{
"accountId": "Player153",
"status": "ACCEPTED",
"direction": "OUTBOUND",
"created": "2024-05-31T19:50:04.738Z",
"favorite": false
},
{
"accountId": "Player724",
"status": "ACCEPTED",
"direction": "OUTBOUND",
"created": "2024-06-24T20:15:48.062Z",
"favorite": false
}
]

View File

@@ -82,13 +82,31 @@
"created": "2024-05-23T19:36:22.635Z"
},
{
"accountId": "Player231",
"accountId": "Player809",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"favorite": false,
"created": "2024-05-23T20:05:07.478Z"
"created": "2024-05-31T19:09:47.089Z"
},
{
"accountId": "Player153",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"favorite": false,
"created": "2024-05-31T19:50:04.738Z"
},
{
"accountId": "Player724",
"groups": [],
"mutual": 0,
"alias": "",
"note": "",
"favorite": false,
"created": "2024-06-24T20:15:48.062Z"
}
],
"incoming": [],

File diff suppressed because it is too large Load Diff

View File

@@ -128,10 +128,11 @@
"importVersionDescription": "Import a new version of Fortnite into the launcher",
"addLocalBuildName": "Add a version from this PC's local storage",
"addLocalBuildDescription": "Versions coming from your local disk are not guaranteed to work",
"addVersion": "New version",
"addVersion": "Import",
"downloadBuildName": "Download any version from the cloud",
"downloadBuildDescription": "Download any Fortnite build easily from the cloud",
"downloadBuildContent": "Download build",
"downloadVersion": "Download",
"cannotUpdateGameServer": "An error occurred while updating the game server: {error}",
"launchFortnite": "Launch Fortnite",
"closeFortnite": "Close Fortnite",
@@ -146,9 +147,9 @@
"defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllAntivirus": "The {name} dll was deleted: your antivirus({antivirus}) might have flagged it",
"downloadDllRetry": "Retry",
"uncaughtErrorMessage": "An uncaught error was thrown: {error}",
"launchingGameServer": "Launching the game server...",
@@ -212,6 +213,7 @@
"selectBuild": "Select a fortnite version",
"fetchingBuilds": "Fetching builds and disks...",
"unknownError": "Unknown error",
"unknown": "unknown",
"downloadVersionError": "Cannot download version: {error}",
"downloadedVersion": "The download was completed successfully!",
"download": "Download",
@@ -258,7 +260,8 @@
"emptyURL": "Empty update URL",
"missingVersionError": "Download or select a version before starting Fortnite",
"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",
"multipleExecutablesError": "There must be only one executable named {name} in the game directory",
"corruptedVersionError": "Fortnite crashed while starting: either the game installation is corrupted or an injected dll({dlls}) tried to access memory illegally",
"corruptedDllError": "Cannot inject dll: {error}",
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
@@ -282,9 +285,9 @@
"infoVideoName": "Tutorial",
"infoVideoDescription": "Show the tutorial again in the launcher",
"infoVideoContent": "Start Tutorial",
"dllDeletedTitle": "A critical dll was deleted. If you didn't delete it, your Antivirus probably flagged it. This is a false positive: please disable your Antivirus and try again",
"dllDeletedTitle": "A critical dll was deleted and couldn't be reinstalled",
"dllDeletedSecondaryAction": "Close",
"dllDeletedPrimaryAction": "Try again",
"dllDeletedPrimaryAction": "Disable Antivirus",
"clickKey": "Waiting for a key to be registered",
"settingsLogsName": "Export logs",
"settingsLogsDescription": "Exports an archive containing all the logs produced by the launcher",
@@ -306,11 +309,8 @@
"quizZeroTriesLeft": "zero tries",
"quizOneTryLeft": "one try",
"quizTwoTriesLeft": "two tries",
"gameServerTypeName": "Type",
"gameServerTypeDescription": "The type of game server to use",
"gameServerTypeHeadless": "Background process",
"gameServerTypeVirtualWindow": "Virtual window",
"gameServerTypeWindow": "Normal window",
"gameServerTypeName": "Headless",
"gameServerTypeDescription": "Disables game rendering to save resources",
"localBuild": "This PC",
"githubArchive": "Cloud archive",
"all": "All",
@@ -374,5 +374,10 @@
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset",
"selectFile": "Select a file",
"reset": "Reset"
"reset": "Reset",
"importingVersion": "Looking for Fortnite game files...",
"importedVersion": "Successfully imported version",
"importVersionMissingShippingExeError": "Cannot import version: {name} should exist in the directory",
"importVersionMultipleShippingExesError": "Cannot import version: only one {name} should exist in the directory",
"importVersionUnsupportedVersionError": "This version of Fortnite is not supported by the launcher"
}

View File

@@ -21,8 +21,10 @@ import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
import 'hosting_controller.dart';
class BackendController extends GetxController {
static const String storageName = "v2_backend_storage";
static const String storageName = "v3_backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage;
@@ -162,6 +164,14 @@ class BackendController extends GetxController {
detached: detached.value,
onError: (errorMessage) {
stop(interactive: false);
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
@@ -508,8 +518,7 @@ class BackendController extends GetxController {
}else {
FlutterClipboard.controlC(decryptedIp);
}
Get.find<GameController>()
.selectedVersion = version;
Get.find<GameController>().selectedVersion.value = version;
WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration,

View File

@@ -9,11 +9,14 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/page/settings_page.dart';
import 'package:version/version.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
class DllController extends GetxController {
static const String storageName = "v2_dll_storage";
static const String storageName = "v3_dll_storage";
late final GetStorage? _storage;
late final TextEditingController customGameServerDll;
@@ -27,6 +30,7 @@ class DllController extends GetxController {
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Map<InjectableDll, StreamSubscription?> _subscriptions;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
@@ -39,15 +43,16 @@ class DllController extends GetxController {
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
beforeS20Mirror = TextEditingController(text: _storage?.read("before_s20_update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("before_s20_update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("after_s20_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("after_s20_update_url", aboveS20Mirror.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
_subscriptions = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
@@ -78,6 +83,7 @@ class DllController extends GetxController {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -88,6 +94,7 @@ class DllController extends GetxController {
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
_listenToFileEvents(InjectableDll.gameServer);
return true;
}
@@ -116,6 +123,7 @@ class DllController extends GetxController {
duration: infoBarShortDuration
);
}
_listenToFileEvents(InjectableDll.gameServer);
return true;
}catch(message) {
infoBarEntry?.close();
@@ -123,26 +131,29 @@ class DllController extends GetxController {
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
final completer = Completer<bool>();
infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
infoBarEntry?.close();
updateGameServerDll(
final result = updateGameServerDll(
force: true,
silent: silent
);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
return false;
return completer.future;
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
(File, bool) getInjectableData(String version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.gameServer:
@@ -150,7 +161,7 @@ class DllController extends GetxController {
return (File(customGameServerDll.text), true);
}
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
return (_isS20(version) ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
@@ -163,6 +174,14 @@ class DllController extends GetxController {
}
}
bool _isS20(String version) {
try {
return Version.parse(version).major >= 20;
} on FormatException catch(_) {
return version.trim().startsWith("20.");
}
}
TextEditingController getDllEditingController(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
@@ -177,16 +196,16 @@ class DllController extends GetxController {
}
String getDefaultDllPath(InjectableDll dll) {
switch(dll) {
case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
switch(dll) {
case InjectableDll.console:
return "${dllsDirectory.path}\\console.dll";
case InjectableDll.auth:
return "${dllsDirectory.path}\\cobalt.dll";
case InjectableDll.gameServer:
return "${dllsDirectory.path}\\reboot.dll";
case InjectableDll.memoryLeak:
return "${dllsDirectory.path}\\memory.dll";
}
}
Future<bool> download(InjectableDll dll, String filePath, {bool silent = false, bool force = false}) async {
@@ -198,77 +217,136 @@ class DllController extends GetxController {
}
if(!force && File(filePath).existsSync()) {
log("[DLL] File already exists");
log("[DLL] $dll already exists");
_listenToFileEvents(dll);
return true;
}
log("[DLL] Downloading $dll...");
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
log("[DLL] Showing dialog while downloading $dll...");
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}else {
log("[DLL] Not showing dialog while downloading $dll...");
}
await downloadDependency(dll, filePath);
final result = await downloadDependency(dll, filePath);
if(!result) {
entry?.close();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
return false;
}
log("[DLL] Downloaded $dll");
entry?.close();
if(!silent) {
log("[DLL] Showing success dialog for $dll");
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}else {
log("[DLL] Not showing success dialog for $dll");
}
_listenToFileEvents(dll);
return true;
}catch(message) {
log("[DLL] Error: $message");
log("[DLL] An error occurred while downloading $dll: $message");
entry?.close();
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();
final completer = Completer();
final completer = Completer<bool>();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), dll.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
onDismissed: () => completer.complete(false),
action: Button(
onPressed: () async {
await download(dll, filePath, silent: silent, force: force);
completer.complete(null);
final result = await download(dll, filePath, silent: silent, force: force);
completer.complete(result);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
return completer.future;
}
}
void guardFiles() {
Future<void> downloadAndGuardDependencies() async {
for(final injectable in InjectableDll.values) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
if (path.equals(controller.text, defaultPath)) {
download(injectable, controller.text);
}
controller.addListener(() async {
try {
if (!path.equals(controller.text, defaultPath)) {
return;
}
final filePath = controller.text;
await for(final event in File(filePath).parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (path.equals(event.path, filePath)) {
await download(injectable, filePath);
}
}
} catch(_) {
// Ignore
}
});
if(path.equals(controller.text, defaultPath)) {
await download(injectable, controller.text);
}
}
}
void _listenToFileEvents(InjectableDll injectable) {
final controller = getDllEditingController(injectable);
final defaultPath = getDefaultDllPath(injectable);
void onFileEvent(FileSystemEvent event, String filePath) {
if (!path.equals(event.path, filePath)) {
return;
}
if(path.equals(filePath, defaultPath)) {
Get.find<GameController>()
.instance
.value
?.kill();
Get.find<HostingController>()
.instance
.value
?.kill();
showRebootInfoBar(
translations.downloadDllAntivirus(antiVirusName ?? defaultAntiVirusName, injectable.name),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error
);
}
_updateInput(injectable);
}
StreamSubscription subscribe(String filePath) => File(filePath)
.parent
.watch(events: FileSystemEvent.delete | FileSystemEvent.move)
.listen((event) => onFileEvent(event, filePath));
controller.addListener(() {
_subscriptions[injectable]?.cancel();
_subscriptions[injectable] = subscribe(controller.text);
});
_subscriptions[injectable] = subscribe(controller.text);
}
void _updateInput(InjectableDll injectable) {
switch(injectable) {
case InjectableDll.console:
settingsConsoleDllInputKey.currentState?.validate();
break;
case InjectableDll.auth:
settingsAuthDllInputKey.currentState?.validate();
break;
case InjectableDll.gameServer:
settingsGameServerDllInputKey.currentState?.validate();
break;
case InjectableDll.memoryLeak:
settingsMemoryDllInputKey.currentState?.validate();
break;
}
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
@@ -8,14 +9,14 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
class GameController extends GetxController {
static const String storageName = "v2_game_storage";
static const String storageName = "v3_game_storage";
late final GetStorage? _storage;
late final TextEditingController username;
late final TextEditingController password;
late final TextEditingController customLaunchArgs;
late final Rx<List<FortniteVersion>> versions;
late final Rxn<FortniteVersion> _selectedVersion;
late final Rxn<FortniteVersion> selectedVersion;
late final RxBool started;
late final Rxn<GameInstance> instance;
@@ -28,8 +29,8 @@ class GameController extends GetxController {
versions = Rx(decodedVersions);
versions.listen((data) => _saveVersions());
final decodedSelectedVersionName = _storage?.read("version");
final decodedSelectedVersion = decodedVersions.firstWhereOrNull((element) => element.content.toString() == decodedSelectedVersionName);
_selectedVersion = Rxn(decodedSelectedVersion);
selectedVersion = Rxn(decodedVersions.firstWhereOrNull((element) => element.name == decodedSelectedVersionName));
selectedVersion.listen((version) => _storage?.write("version", version?.name));
username = TextEditingController(
text: _storage?.read("username") ?? kDefaultPlayerName);
username.addListener(() => _storage?.write("username", username.text));
@@ -46,26 +47,27 @@ class GameController extends GetxController {
password.text = "";
customLaunchArgs.text = "";
versions.value = [];
_selectedVersion.value = null;
selectedVersion.value = null;
instance.value = null;
}
FortniteVersion? getVersionByName(String name) {
return versions.value.firstWhereOrNull((element) => element.content.toString() == name);
name = name.trim();
return versions.value.firstWhereOrNull((element) => element.name == name);
}
void addVersion(FortniteVersion version) {
var empty = versions.value.isEmpty;
versions.update((val) => val?.add(version));
if(empty){
selectedVersion = version;
}
selectedVersion.value = version;
}
void removeVersion(FortniteVersion version) {
versions.update((val) => val?.remove(version));
if (selectedVersion == version || hasNoVersions) {
selectedVersion = null;
final index = versions.value.indexOf(version);
versions.update((val) => val?.removeAt(index));
if(hasNoVersions) {
selectedVersion.value = null;
}else {
selectedVersion.value = versions.value.elementAt(max(0, index - 1));
}
}
@@ -78,14 +80,5 @@ class GameController extends GetxController {
bool get hasNoVersions => versions.value.isEmpty;
FortniteVersion? get selectedVersion => _selectedVersion();
set selectedVersion(FortniteVersion? version) {
_selectedVersion.value = version;
_storage?.write("version", version?.content.toString());
}
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) {
versions.update((val) => function(version));
}
void updateVersion(FortniteVersion version, Function(FortniteVersion) function) => versions.update((val) => function(version));
}

View File

@@ -12,7 +12,7 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart';
class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage";
static const String storageName = "v3_hosting_storage";
late final GetStorage? _storage;
late final String uuid;
@@ -26,7 +26,7 @@ class HostingController extends GetxController {
late final FocusNode passwordFocusNode;
late final RxBool showPassword;
late final RxBool discoverable;
late final Rx<GameServerType> type;
late final RxBool headless;
late final RxBool autoRestart;
late final RxBool started;
late final RxBool published;
@@ -54,8 +54,8 @@ class HostingController extends GetxController {
passwordFocusNode = FocusNode();
discoverable = RxBool(_storage?.read("discoverable") ?? false);
discoverable.listen((value) => _storage?.write("discoverable", value));
type = Rx(GameServerType.values.elementAt(_storage?.read("type") ?? GameServerType.headless.index));
type.listen((value) => _storage?.write("type", value.index));
headless = RxBool(_storage?.read("headless") ?? true);
headless.listen((value) => _storage?.write("headless", value));
autoRestart = RxBool(_storage?.read("auto_restart") ?? true);
autoRestart.listen((value) => _storage?.write("auto_restart", value));
started = RxBool(false);
@@ -165,7 +165,7 @@ class HostingController extends GetxController {
showPassword.value = false;
discoverable.value = false;
instance.value = null;
type.value = GameServerType.headless;
headless.value = true;
autoRestart.value = true;
customLaunchArgs.text = "";
}

View File

@@ -13,7 +13,7 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart';
class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage";
static const String storageName = "v3_settings_storage";
late final GetStorage? _storage;
late final RxString language;

View File

@@ -126,7 +126,7 @@ class ProgressDialog extends AbstractDialog {
header: InfoLabel(
label: text,
child: Container(
padding: const EdgeInsets.only(bottom: 16.0),
padding: const EdgeInsets.symmetric(vertical: 16.0),
width: double.infinity,
child: const ProgressBar()
),

View File

@@ -78,417 +78,6 @@ class _ServiceProvider10 extends IUnknown {
}
}
class IVirtualDesktop extends IUnknown {
static const String _CLSID = "{3F07F4BE-B107-441A-AF0F-39D82529072C}";
IVirtualDesktop._internal(super.ptr);
String getName() {
final result = calloc<HSTRING>();
final code = (ptr.ref.vtable + 5)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<HSTRING>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<HSTRING>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return _convertFromHString(result.value);
}
}
class IApplicationView extends IUnknown {
// static const String _CLSID = "{372E1D3B-38D3-42E4-A15B-8AB2B178F513}";
IApplicationView._internal(super.ptr);
}
class _IObjectArray extends IUnknown {
_IObjectArray(super.ptr);
int getCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
Pointer<COMObject> getAt(int index, String guid) {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Int32 index, Pointer<GUID>,
Pointer<COMObject>)>>>()
.value
.asFunction<
int Function(
Pointer, int index, Pointer<GUID>, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, index, GUIDFromString(guid), result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result;
}
}
typedef _IObjectMapper<T> = T Function(Pointer<COMObject>);
class _IObjectArrayList<T> extends ListBase<T> {
final _IObjectArray _array;
final String _guid;
final _IObjectMapper<T> _mapper;
_IObjectArrayList(
{required _IObjectArray array,
required String guid,
required _IObjectMapper<T> mapper})
: _array = array,
_guid = guid,
_mapper = mapper;
@override
int get length => _array.getCount();
@override
set length(int newLength) {
throw UnsupportedError("Immutable list");
}
@override
T operator [](int index) => _mapper(_array.getAt(index, _guid));
@override
void operator []=(int index, T value) {
throw UnsupportedError("Immutable list");
}
}
class _IVirtualDesktopManagerInternal extends IUnknown {
static const String _CLSID = "{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}";
static const String _IID_WIN10 = "{F31574D6-B682-4CDC-BD56-1827860ABEC6}";
static const String _IID_WIN_21H2 = "{B2F925B9-5A0F-4D2E-9F4D-2B1507593C10}";
static const String _IID_WIN_23H2 = "{A3175F2D-239C-4BD2-8AA0-EEBA8B0B138E}";
static const String _IID_WIN_23H2_3085 = "{53F5CA0B-158F-4124-900C-057158060B27}";
_IVirtualDesktopManagerInternal._internal(super.ptr);
int getDesktopsCount() {
final result = calloc<Int32>();
final code = (ptr.ref.vtable + 3)
.cast<
Pointer<
NativeFunction<HRESULT Function(Pointer, Pointer<Int32>)>>>()
.value
.asFunction<
int Function(Pointer, Pointer<Int32>)>()(ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return result.value;
}
List<IVirtualDesktop> getDesktops() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 7)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
final array = _IObjectArray(result);
return _IObjectArrayList(
array: array,
guid: IVirtualDesktop._CLSID,
mapper: (comObject) => IVirtualDesktop._internal(comObject));
}
void moveWindowToDesktop(IApplicationView view, IVirtualDesktop desktop) {
final code = (ptr.ref.vtable + 4)
.cast<
Pointer<
NativeFunction<
Int32 Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, view.ptr.ref, desktop.ptr.ref);
if (code != 0) {
throw WindowsException(code, message: "Cannot move window");
}
}
IVirtualDesktop createDesktop() {
final result = calloc<COMObject>();
final code = (ptr.ref.vtable + 10)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, result);
if (code != 0) {
free(result);
throw WindowsException(code);
}
return IVirtualDesktop._internal(result);
}
void removeDesktop(IVirtualDesktop desktop, IVirtualDesktop fallback) {
final code = (ptr.ref.vtable + 12)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, COMObject)>>>()
.value
.asFunction<int Function(Pointer, COMObject, COMObject)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, fallback.ptr.ref);
if (code != 0) {
throw WindowsException(code);
}
}
void setDesktopName(IVirtualDesktop desktop, String newName) {
final code =
(ptr.ref.vtable + 15)
.cast<
Pointer<
NativeFunction<
HRESULT Function(Pointer, COMObject, Int8)>>>()
.value
.asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
if (code != 0) {
throw WindowsException(code);
}
}
}
class _IApplicationViewCollection extends IUnknown {
static const String _CLSID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
static const String _IID = "{1841C6D7-4F9D-42C0-AF41-8747538F10E5}";
_IApplicationViewCollection._internal(super.ptr);
IApplicationView? getViewForHWnd(int HWnd) {
final result = calloc<COMObject>();
final code =
(ptr.ref.vtable + 6)
.cast<
Pointer<
NativeFunction<
HRESULT Function(
Pointer, IntPtr, Pointer<COMObject>)>>>()
.value
.asFunction<int Function(Pointer, int, Pointer<COMObject>)>()(
ptr.ref.lpVtbl, HWnd, result);
if (code != 0) {
free(result);
return null;
}
return IApplicationView._internal(result);
}
}
final class Win32Process extends Struct {
@Uint32()
external int pid;
@Uint32()
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;
if (pid == structure.ref.pid) {
final length = structure.ref.HWndLength;
final newLength = length + 1;
final ptr = malloc.allocate<Uint32>(sizeOf<Uint32>() * newLength);
final list = structure.ref.HWnd.asTypedList(length);
for (var i = 0; i < list.length; i++) {
(ptr + i).value = list[i];
}
ptr[list.length] = HWnd;
structure.ref.HWndLength = newLength;
free(structure.ref.HWnd);
structure.ref.HWnd = ptr;
}
free(pidPointer);
return TRUE;
}
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<WNDENUMPROC>(_filter, TRUE), result.address);
final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) {
calloc.free(result);
return [];
}
final HWnds = HWndsPointer.asTypedList(length)
.toList(growable: false);
calloc.free(result);
return HWnds;
}
class VirtualDesktopManager {
static VirtualDesktopManager? _instance;
final _IVirtualDesktopManagerInternal windowManager;
final _IApplicationViewCollection applicationViewCollection;
VirtualDesktopManager._internal(this.windowManager, this.applicationViewCollection);
factory VirtualDesktopManager.getInstance() {
if (_instance != null) {
return _instance!;
}
final hr = CoInitializeEx(
nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) {
throw WindowsException(hr);
}
final shell = _ServiceProvider10.createInstance();
final windowManager = _createWindowManager(shell);
final applicationViewCollection = _IApplicationViewCollection._internal(
shell.queryService(_IApplicationViewCollection._CLSID,
_IApplicationViewCollection._IID));
return _instance =
VirtualDesktopManager._internal(windowManager, applicationViewCollection);
}
static _IVirtualDesktopManagerInternal _createWindowManager(_ServiceProvider10 shell) {
final build = windowsBuild;
if(build == null || build < 19044) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN10));
}else if(build >= 19044 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_21H2));
}else if(build >= 22631 && build < 22631) {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2));
}else {
return _IVirtualDesktopManagerInternal._internal(
shell.queryService(_IVirtualDesktopManagerInternal._CLSID,
_IVirtualDesktopManagerInternal._IID_WIN_23H2_3085));
}
}
int getDesktopsCount() => windowManager.getDesktopsCount();
List<IVirtualDesktop> getDesktops() => windowManager.getDesktops();
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 true;
}
}
if(remainingPolls <= 0) {
return false;
}
await Future.delayed(pollTime);
return await moveWindowToDesktop(
pid,
desktop,
pollTime: pollTime,
remainingPolls: remainingPolls - 1
);
}
IVirtualDesktop createDesktop() => windowManager.createDesktop();
void removeDesktop(IVirtualDesktop desktop, [IVirtualDesktop? fallback]) {
fallback ??= getDesktops().first;
return windowManager.removeDesktop(desktop, fallback);
}
void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName);
}
String _convertFromHString(int hstring) =>
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
int _convertToHString(String string) {
final hString = calloc<HSTRING>();
final stringPtr = string.toNativeUtf16();
try {
final hr = WindowsCreateString(stringPtr, string.length, hString);
if (FAILED(hr)) throw WindowsException(hr);
return hString.value;
} finally {
free(stringPtr);
free(hString);
}
}
extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
}

View File

@@ -19,17 +19,4 @@ void loadTranslations(BuildContext context) {
_init = true;
}
String get currentLocale => Intl.getCurrentLocale().split("_")[0];
extension GameServerTypeExtension on GameServerType {
String get translatedName {
switch(this) {
case GameServerType.headless:
return translations.gameServerTypeHeadless;
case GameServerType.virtualWindow:
return translations.gameServerTypeVirtualWindow;
case GameServerType.window:
return translations.gameServerTypeWindow;
}
}
}
String get currentLocale => Intl.getCurrentLocale().split("_")[0];

View File

@@ -1,28 +1,35 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/os.dart';
typedef FileSelectorValidator = String? Function(String?);
class FileSelector extends StatefulWidget {
final String placeholder;
final String windowTitle;
final bool allowNavigator;
final TextEditingController controller;
final String? Function(String?) validator;
final FileSelectorValidator? validator;
final AutovalidateMode? validatorMode;
final Key? validatorKey;
final String? extension;
final String? label;
final bool folder;
final void Function(String)? onSelected;
const FileSelector(
{required this.placeholder,
required this.windowTitle,
required this.controller,
required this.validator,
required this.folder,
required this.allowNavigator,
this.validator,
this.validatorKey,
this.label,
this.extension,
this.validatorMode,
this.onSelected,
Key? key})
: assert(folder || extension != null, "Missing extension for file selector"),
super(key: key);
@@ -47,6 +54,7 @@ class _FileSelectorState extends State<FileSelector> {
placeholder: widget.placeholder,
validator: widget.validator,
autovalidateMode: widget.validatorMode ?? AutovalidateMode.onUserInteraction,
key: widget.validatorKey,
suffix: !widget.allowNavigator ? null : Button(
onPressed: _onPressed,
child: const Icon(FluentIcons.open_folder_horizontal)
@@ -72,6 +80,10 @@ class _FileSelectorState extends State<FileSelector> {
}
void _updateText(String? value) {
if(value != null) {
widget.onSelected?.call(value);
}
var text = value ?? widget.controller.text;
widget.controller.text = value ?? widget.controller.text;
widget.controller.selection = TextSelection.collapsed(offset: text.length);

View File

@@ -13,10 +13,14 @@ import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;
// FIXME: If the user clicks on the reset button, the text field checker won't be called
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
final obx = RxString(controller.text);
controller.addListener(() => obx.value = controller.text);
SettingTile createFileSetting({
required GlobalKey<TextFormBoxState> key,
required String title,
required String description,
required TextEditingController controller,
required void Function() onReset
}) {
final obx = RxnString();
final selecting = RxBool(false);
return SettingTile(
icon: Icon(
@@ -32,17 +36,23 @@ SettingTile createFileSetting({required String title, required String descriptio
placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle,
controller: controller,
validator: _checkDll,
validator: (text) {
final result = _checkDll(text);
print("Called validator: $result");
obx.value = result;
return result;
},
extension: "dll",
folder: false,
validatorMode: AutovalidateMode.always,
allowNavigator: false,
validatorKey: key
),
),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
bottom: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.selectFile,
@@ -63,7 +73,7 @@ SettingTile createFileSetting({required String title, required String descriptio
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
bottom: obx.value == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.reset,
@@ -109,7 +119,9 @@ String? _checkDll(String? text) {
}
final file = File(text);
if (!file.existsSync()) {
try {
file.readAsBytesSync();
}catch(_) {
return translations.dllDoesNotExist;
}

View File

@@ -16,7 +16,6 @@ import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/page/pages.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:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -45,7 +44,6 @@ class _LaunchButtonState extends State<LaunchButton> {
InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation;
Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop;
@override
Widget build(BuildContext context) => Align(
@@ -73,17 +71,19 @@ class _LaunchButtonState extends State<LaunchButton> {
if (host ? _hostingController.started() : _gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(
reason: _StopReason.normal
reason: _StopReason.normal,
host: host
);
return;
}
final version = _gameController.selectedVersion;
final version = _gameController.selectedVersion.value;
log("[${host ? 'HOST' : 'GAME'}] Version data: $version");
if(version == null){
log("[${host ? 'HOST' : 'GAME'}] No version selected");
_onStop(
reason: _StopReason.missingVersionError
reason: _StopReason.missingVersionError,
host: host
);
return;
}
@@ -93,37 +93,28 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(version.content, injectable, host) == null) {
if(await _getDllFileOrStop(version.gameVersion, injectable, host) == null) {
return;
}
}
try {
final executable = await version.shippingExecutable;
if(executable == null){
log("[${host ? 'HOST' : 'GAME'}] No executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: version.location.path
);
return;
}
log("[${host ? 'HOST' : 'GAME'}] Checking backend(port: ${_backendController.type.value.name}, type: ${_backendController.type.value.name})...");
final backendResult = _backendController.started() || await _backendController.toggle();
if(!backendResult){
log("[${host ? 'HOST' : 'GAME'}] Cannot start backend");
_onStop(
reason: _StopReason.backendError
reason: _StopReason.backendError,
host: host
);
return;
}
log("[${host ? 'HOST' : 'GAME'}] Backend works");
final serverType = _hostingController.type.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($serverType)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, serverType, false);
final headless = _hostingController.headless.value;
log("[${host ? 'HOST' : 'GAME'}] Implicit game server metadata: headless($headless)");
final linkedHostingInstance = await _startMatchMakingServer(version, host, headless, false);
log("[${host ? 'HOST' : 'GAME'}] Implicit game server result: $linkedHostingInstance");
final result = await _startGameProcesses(version, host, serverType, linkedHostingInstance);
final result = await _startGameProcesses(version, host, headless, linkedHostingInstance);
final started = host ? _hostingController.started() : _gameController.started();
if(!started) {
result?.kill();
@@ -131,7 +122,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
if(!host) {
_showLaunchingGameClientWidget(version, serverType, linkedHostingInstance != null);
_showLaunchingGameClientWidget(version, headless, linkedHostingInstance != null);
}else {
_showLaunchingGameServerWidget();
}
@@ -139,18 +130,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace
stackTrace: stackTrace,
host: host
);
} catch (exception, stackTrace) {
_onStop(
reason: _StopReason.unknownError,
error: exception.toString(),
stackTrace: stackTrace
stackTrace: stackTrace,
host: host
);
}
}
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, GameServerType hostType, bool forceLinkedHosting) async {
Future<GameInstance?> _startMatchMakingServer(FortniteVersion version, bool host, bool headless, 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");
@@ -174,7 +167,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}
log("[${host ? 'HOST' : 'GAME'}] Starting implicit game server...");
final instance = await _startGameProcesses(version, true, hostType, null);
final instance = await _startGameProcesses(version, true, headless, null);
log("[${host ? 'HOST' : 'GAME'}] Started implicit game server...");
_setStarted(true, true);
log("[${host ? 'HOST' : 'GAME'}] Set implicit game server as started");
@@ -184,7 +177,7 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<bool> _askForAutomaticGameServer(bool host) async {
if (host ? !_hostingController.started() : !_gameController.started()) {
log("[${host ? 'HOST' : 'GAME'}] User asked to close the current instance");
_onStop(reason: _StopReason.normal);
_onStop(reason: _StopReason.normal, host: host);
return false;
}
@@ -208,19 +201,10 @@ class _LaunchButtonState extends State<LaunchButton> {
return result;
}
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
log("[${host ? 'HOST' : 'GAME'}] Starting paused launcher...");
final launcherProcess = await _createPausedProcess(version, version.launcherExecutable);
log("[${host ? 'HOST' : 'GAME'}] Started paused launcher: $launcherProcess");
log("[${host ? 'HOST' : 'GAME'}] Starting paused eac...");
final eacProcess = await _createPausedProcess(version, version.eacExecutable);
log("[${host ? 'HOST' : 'GAME'}] Started paused eac: $eacProcess");
final executable = await version.shippingExecutable;
log("[${host ? 'HOST' : 'GAME'}] Using game path: ${executable?.path}");
final gameProcess = await _createGameProcess(version, executable!, host, hostType, linkedHosting);
Future<GameInstance?> _startGameProcesses(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
final launcherProcess = await _createPausedProcess(version, host, kLauncherExe);
final eacProcess = await _createPausedProcess(version, host, kEacExe);
final gameProcess = await _createGameProcess(version, host, headless, linkedHosting);
if(gameProcess == null) {
log("[${host ? 'HOST' : 'GAME'}] No game process was created");
return null;
@@ -228,11 +212,11 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance(
version: version.content,
version: version.gameVersion,
gamePid: gameProcess,
launcherPid: launcherProcess,
eacPid: eacProcess,
serverType: host ? hostType : null,
headless: host && headless,
child: linkedHosting
);
if(host){
@@ -246,22 +230,44 @@ class _LaunchButtonState extends State<LaunchButton> {
return instance;
}
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
Future<int?> _createGameProcess(FortniteVersion version, bool host, bool headless, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Starting game process...");
final shippingExecutables = await findFiles(version.location, kShippingExe);
if(shippingExecutables.isEmpty){
log("[${host ? 'HOST' : 'GAME'}] No game executable found");
_onStop(
reason: _StopReason.missingExecutableError,
error: kShippingExe,
host: host
);
return null;
}
if(shippingExecutables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many game executables found");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: kShippingExe,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs(
host ? _hostingController.accountUsername.text : _gameController.username.text,
host ? _hostingController.accountPassword.text : _gameController.password.text,
host,
hostType,
headless,
false,
host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
);
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess(
executable: executable,
executable: shippingExecutables.first,
args: gameArgs,
useTempBatch: false,
name: "${version.content}-${host ? 'HOST' : 'GAME'}",
name: "${version.gameVersion}-${host ? 'HOST' : 'GAME'}",
environment: {
"OPENSSL_ia32cap": "~0x20000000"
}
@@ -272,26 +278,26 @@ class _LaunchButtonState extends State<LaunchButton> {
handleGameOutput(
line: line,
host: host,
onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError),
onShutdown: () => _onStop(reason: _StopReason.normal, host: host),
onTokenError: () => _onStop(reason: _StopReason.tokenError, host: host),
onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError);
_onStop(reason: _StopReason.corruptedVersionError, host: host);
}else {
_onStop(reason: _StopReason.crash);
_onStop(reason: _StopReason.crash, host: host);
}
},
onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
onMatchEnd: () => _onMatchEnd(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;
instance?.killed = true;
log("[${host ? 'HOST' : 'GAME'}] Called exit code(launched: ${instance?.launched}): stop signal");
_onStop(
reason: _StopReason.exitCode,
@@ -301,60 +307,37 @@ class _LaunchButtonState extends State<LaunchButton> {
return gameProcess.pid;
}
Future<int?> _createPausedProcess(FortniteVersion version, File? file) async {
if (file == null) {
Future<int?> _createPausedProcess(FortniteVersion version, bool host, String executableName) async {
log("[${host ? 'HOST' : 'GAME'}] Starting $executableName...");
final executables = await findFiles(version.location, executableName);
if(executables.isEmpty){
return null;
}
if(executables.length != 1) {
log("[${host ? 'HOST' : 'GAME'}] Too many $executableName found: $executables");
_onStop(
reason: _StopReason.multipleExecutablesError,
error: executableName,
host: host
);
return null;
}
final process = await startProcess(
executable: file,
executable: executables.first,
useTempBatch: false,
name: "${version.content}-${basenameWithoutExtension(file.path)}",
name: "${version.gameVersion}-${basenameWithoutExtension(executables.first.path)}",
environment: {
"OPENSSL_ia32cap": "~0x20000000"
}
);
log("[${host ? 'HOST' : 'GAME'}] Started paused $executableName: $process");
final pid = process.pid;
suspend(pid);
return pid;
}
Future<void> _onDisplayAttached(bool host, GameServerType type, FortniteVersion version) async {
if(host && type == GameServerType.virtualWindow) {
final hostingInstance = _hostingController.instance.value;
if(hostingInstance != null && !hostingInstance.movedToVirtualDesktop) {
hostingInstance.movedToVirtualDesktop = true;
try {
final windowManager = VirtualDesktopManager.getInstance();
_virtualDesktop = windowManager.createDesktop();
windowManager.setDesktopName(_virtualDesktop!, "${version.content} Server (Reboot Launcher)");
var success = false;
try {
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) {
log("[VIRTUAL_DESKTOP] $error");
}finally {
_virtualDesktop = null;
}
}
}catch(error) {
log("[VIRTUAL_DESKTOP] $error");
}
}
}
}
void _onMatchEnd(FortniteVersion version) {
if(_hostingController.autoRestart.value) {
final notification = LocalNotification(
@@ -397,7 +380,9 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) {
instance.launched = true;
instance.tokenError = false;
await _injectOrShowError(InjectableDll.memoryLeak, host);
if(_isChapterOne(instance.version)) {
await _injectOrShowError(InjectableDll.memoryLeak, host);
}
if(!host){
await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected();
@@ -412,6 +397,14 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
bool _isChapterOne(String version) {
try {
return Version.parse(version).major < 10;
} on FormatException catch(_) {
return true;
}
}
void _onGameClientInjected() {
_gameClientInfoBar?.close();
showRebootInfoBar(
@@ -515,21 +508,20 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) {
Future<void> _onStop({required _StopReason reason, required bool host, String? error, StackTrace? stackTrace}) async {
if(host) {
try {
_pingOperation?.complete(false);
}catch(_) {
} catch (_) {
// Ignore: might be running, don't bother checking
} finally {
_pingOperation = null;
}
await _operation?.cancel();
_operation = null;
_backendController.stop(interactive: false);
}
host = host ?? widget.host;
await _operation?.cancel();
_operation = null;
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){
@@ -538,15 +530,6 @@ class _LaunchButtonState extends State<LaunchButton> {
_gameController.instance.value = null;
}
if(_virtualDesktop != null) {
try {
final instance = VirtualDesktopManager.getInstance();
instance.removeDesktop(_virtualDesktop!);
}catch(error) {
log("[VIRTUAL_DESKTOP] Cannot close virtual desktop: $error");
}
}
log("[${host ? 'HOST' : 'GAME'}] Called stop with reason $reason, error data $error $stackTrace");
log("[${host ? 'HOST' : 'GAME'}] Caller: ${StackTrace.current}");
if(host) {
@@ -562,7 +545,7 @@ class _LaunchButtonState extends State<LaunchButton> {
if(child != null) {
await _onStop(
reason: reason,
host: child.serverType != null
host: host
);
}
@@ -594,10 +577,18 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: infoBarLongDuration,
);
break;
case _StopReason.multipleExecutablesError:
showRebootInfoBar(
translations.multipleExecutablesError(error ?? translations.unknown),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.exitCode:
if(instance != null && !instance.launched) {
final injectedDlls = instance.injectedDlls;
showRebootInfoBar(
translations.corruptedVersionError,
translations.corruptedVersionError(injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
@@ -630,8 +621,9 @@ class _LaunchButtonState extends State<LaunchButton> {
break;
case _StopReason.tokenError:
_backendController.stop(interactive: false);
final injectedDlls = instance?.injectedDlls;
showRebootInfoBar(
translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
translations.tokenError(injectedDlls == null || injectedDlls.isEmpty ? translations.none : injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
@@ -669,17 +661,13 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
if(dllPath == null) {
if (dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: hosting
);
return;
}
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable.name}...");
log("[${hosting ? 'HOST' : 'GAME'}] Trying to inject ${injectable
.name}...");
await injectDll(gameProcess, dllPath);
instance.injectedDlls.add(injectable);
log("[${hosting ? 'HOST' : 'GAME'}] Injected ${injectable.name}");
@@ -694,13 +682,16 @@ class _LaunchButtonState extends State<LaunchButton> {
}
}
Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
Future<File?> _getDllFileOrStop(String version, InjectableDll injectable, bool host) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) {
try {
await file.readAsBytes();
log("[${host ? 'HOST' : 'GAME'}] Path exists");
return file;
}catch(_) {
}
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
@@ -709,14 +700,20 @@ class _LaunchButtonState extends State<LaunchButton> {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
host: host
);
return null;
}
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _dllController.download(injectable, file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(version, injectable, host, true);
final result = await _dllController.download(injectable, file.path, force: true);
if(result) {
log("[${host ? 'HOST' : 'GAME'}] Downloaded critical dll");
return file;
}
_onStop(reason: _StopReason.normal, host: host);
return null;
}
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -725,7 +722,7 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null
);
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, GameServerType hostType, bool linkedHosting) {
InfoBarEntry _showLaunchingGameClientWidget(FortniteVersion version, bool headless, bool linkedHosting) {
return _gameClientInfoBar = showRebootInfoBar(
linkedHosting ? translations.launchingGameClientAndServer : translations.launchingGameClientOnly,
loading: true,
@@ -743,9 +740,9 @@ class _LaunchButtonState extends State<LaunchButton> {
onPressed: () async {
_backendController.joinLocalhost();
if(!_hostingController.started.value) {
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, hostType, true);
_gameController.instance.value?.child = await _startMatchMakingServer(version, false, headless, true);
_gameClientInfoBar?.close();
_showLaunchingGameClientWidget(version, hostType, true);
_showLaunchingGameClientWidget(version, headless, true);
}
},
child: Text(translations.startGameServer),
@@ -760,6 +757,7 @@ enum _StopReason {
normal,
missingVersionError,
missingExecutableError,
multipleExecutablesError,
corruptedVersionError,
missingCustomDllError,
corruptedDllError,

View File

@@ -2,7 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
Future<void> showDllDeletedDialog() => showRebootDialog(
builder: (context) => InfoDialog(
text: translations.dllDeletedTitle,
buttons: [
@@ -15,7 +15,7 @@ Future<void> showDllDeletedDialog(Function() onConfirm) => showRebootDialog(
text: translations.dllDeletedPrimaryAction,
onTap: () {
Navigator.pop(context);
onConfirm();
},
),
],

View File

@@ -134,7 +134,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true);
}
_dllController.guardFiles();
_dllController.downloadAndGuardDependencies();
}
@override

View File

@@ -5,12 +5,10 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/message/data.dart';
@@ -19,7 +17,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
final GlobalKey<OverlayTargetState> hostVersionOverlayTargetKey = GlobalKey();
final GlobalKey<OverlayTargetState> hostInfoOverlayTargetKey = GlobalKey();
@@ -78,7 +76,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
@override
List<Widget> get settings => [
_information,
buildVersionSelector(
VersionSelector.buildTile(
key: hostVersionOverlayTargetKey
),
_options,
@@ -171,7 +169,7 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null,
content: Obx(() => Row(
children: [
Obx(() => Text(
Obx(() => Text(
_hostingController.discoverable.value ? translations.on : translations.off
)),
const SizedBox(
@@ -214,15 +212,21 @@ class _HostingPageState extends RebootPageState<HostPage> {
),
title: Text(translations.gameServerTypeName),
subtitle: Text(translations.gameServerTypeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_hostingController.type.value.translatedName),
items: GameServerType.values.map((entry) => MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => _hostingController.type.value = entry
)).toList()
)),
contentWidth: null,
content: Row(
children: [
Obx(() => Text(
_hostingController.headless.value ? translations.on : translations.off
)),
const SizedBox(
width: 16.0
),
Obx(() => ToggleSwitch(
checked: _hostingController.headless.value,
onChanged: (value) => _hostingController.headless.value = value
)),
],
),
),
SettingTile(
icon: Icon(

View File

@@ -10,7 +10,7 @@ import 'package:reboot_launcher/src/page/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/game/game_start_button.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
final GlobalKey<OverlayTargetState> gameVersionOverlayTargetKey = GlobalKey();
@@ -46,7 +46,7 @@ class _PlayPageState extends RebootPageState<PlayPage> {
@override
List<SettingTile> get settings => [
buildVersionSelector(
VersionSelector.buildTile(
key: gameVersionOverlayTargetKey
),
_options,

View File

@@ -17,6 +17,11 @@ import 'package:reboot_launcher/src/widget/file/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart';
final GlobalKey<TextFormBoxState> settingsConsoleDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsAuthDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsMemoryDllInputKey = GlobalKey();
final GlobalKey<TextFormBoxState> settingsGameServerDllInputKey = GlobalKey();
class SettingsPage extends RebootPage {
const SettingsPage({Key? key}) : super(key: key);
@@ -60,33 +65,39 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
key: settingsConsoleDllInputKey,
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.download(InjectableDll.console, path, force: true);
await _dllController.download(InjectableDll.console, path, force: true);
settingsConsoleDllInputKey.currentState?.validate();
}
),
createFileSetting(
key: settingsAuthDllInputKey,
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.auth);
_dllController.backendDll.text = path;
_dllController.download(InjectableDll.auth, path, force: true);
await _dllController.download(InjectableDll.auth, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
createFileSetting(
key: settingsMemoryDllInputKey,
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _dllController.memoryLeakDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.memoryLeak);
_dllController.memoryLeakDll.text = path;
_dllController.download(InjectableDll.memoryLeak, path, force: true);
await _dllController.download(InjectableDll.memoryLeak, path, force: true);
settingsAuthDllInputKey.currentState?.validate();
}
),
_internalFilesServerType,
@@ -142,9 +153,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: _scheduleMirrorDownload
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -181,13 +192,15 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
);
}else {
return createFileSetting(
key: settingsGameServerDllInputKey,
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.customGameServerDll,
onReset: () {
onReset: () async {
final path = _dllController.getDefaultDllPath(InjectableDll.gameServer);
_dllController.customGameServerDll.text = path;
_dllController.download(InjectableDll.gameServer, path);
await _dllController.download(InjectableDll.gameServer, path);
settingsGameServerDllInputKey.currentState?.validate();
}
);
}
@@ -224,9 +237,9 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: _scheduleMirrorDownload
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: _scheduleMirrorDownload
),
),
const SizedBox(width: 8.0),
@@ -337,7 +350,7 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
)).toList()
))
);
SettingTile get _installationDirectory => SettingTile(
icon: Icon(
FluentIcons.folder_24_regular

View File

@@ -14,22 +14,22 @@ import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:windows_taskbar/windows_taskbar.dart';
class AddVersionDialog extends StatefulWidget {
class DownloadVersionDialog extends StatefulWidget {
final bool closable;
const AddVersionDialog({Key? key, required this.closable}) : super(key: key);
const DownloadVersionDialog({Key? key, required this.closable}) : super(key: key);
@override
State<AddVersionDialog> createState() => _AddVersionDialogState();
State<DownloadVersionDialog> createState() => _DownloadVersionDialogState();
}
class _AddVersionDialogState extends State<AddVersionDialog> {
class _DownloadVersionDialogState extends State<DownloadVersionDialog> {
final GameController _gameController = Get.find<GameController>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _pathController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey();
final GlobalKey<FormFieldState> _formFieldKey = GlobalKey();
final Rx<_DownloadStatus> _status = Rx(_DownloadStatus.form);
final Rx<_BuildSource> _source = Rx(_BuildSource.githubArchive);
final Rxn<FortniteBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn();
@@ -46,6 +46,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
@override
void dispose() {
_nameController.dispose();
_pathController.dispose();
_cancelDownload();
super.dispose();
@@ -63,10 +64,10 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
child: Obx(() {
switch(_status.value){
case _DownloadStatus.form:
return Obx(() => FormDialog(
content: _buildFormBody(downloadableBuilds),
return FormDialog(
content: _formBody,
buttons: _formButtons
));
);
case _DownloadStatus.downloading:
case _DownloadStatus.extracting:
return GenericDialog(
@@ -86,7 +87,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
);
case _DownloadStatus.done:
return InfoDialog(
text: translations.downloadedVersion
text: translations.downloadedVersion
);
}
})
@@ -96,7 +97,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
if(widget.closable)
DialogButton(type: ButtonType.secondary),
DialogButton(
text: _source.value == _BuildSource.local ? translations.saveLocalVersion : translations.download,
text: translations.download,
type: widget.closable ? ButtonType.primary : ButtonType.only,
color: FluentTheme.of(context).accentColor,
onTap: () => _startDownload(context),
@@ -120,13 +121,6 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return;
}
final source = _source.value;
if(source == _BuildSource.local) {
Navigator.of(context).pop();
_addFortniteVersion(build);
return;
}
_status.value = _DownloadStatus.downloading;
final communicationPort = ReceivePort();
communicationPort.listen((message) {
@@ -161,16 +155,22 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return;
}
final name = _nameController.text.trim();
final location = Directory(_pathController.text);
final files = await findFiles(location, kShippingExe);
if(files.length == 1) {
await patchHeadless(files.first);
}
_status.value = _DownloadStatus.done;
WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
_addFortniteVersion(build);
}
void _addFortniteVersion(FortniteBuild build) {
WidgetsBinding.instance.addPostFrameCallback((_) => _gameController.addVersion(FortniteVersion(
content: build.version,
location: Directory(_pathController.text)
)));
final version = FortniteVersion(
name: name,
gameVersion: build.gameVersion,
location: location
);
_gameController.addVersion(version);
}
void _onDownloadError(Object? error, StackTrace? stackTrace) {
@@ -269,25 +269,32 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return translations.downloading;
}
Widget _buildFormBody(List<FortniteBuild> builds) {
return Column(
Widget get _formBody => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSourceSelector(),
InfoLabel(
label: translations.versionName,
child: TextFormBox(
controller: _nameController,
validator: _checkVersionName,
placeholder: translations.versionNameLabel,
autovalidateMode: AutovalidateMode.onUserInteraction
),
),
const SizedBox(
height: 16.0
),
_buildBuildSelector(builds),
_buildSelector,
FileSelector(
label: translations.gameFolderTitle,
placeholder: _source.value == _BuildSource.local ? translations.gameFolderPlaceholder : translations.buildInstallationDirectoryPlaceholder,
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
placeholder: translations.buildInstallationDirectoryPlaceholder,
windowTitle: translations.buildInstallationDirectoryWindowTitle,
controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
validator: _checkDownloadDestination,
folder: true,
allowNavigator: true
),
@@ -297,20 +304,14 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
)
],
);
}
String? _checkGameFolder(text) {
String? _checkVersionName(text) {
if (text == null || text.isEmpty) {
return translations.emptyGamePath;
return translations.emptyVersionName;
}
final directory = Directory(text);
if (!directory.existsSync()) {
return translations.directoryDoesNotExist;
}
if (FortniteVersionExtension.findFile(directory, "FortniteClient-Win64-Shipping.exe") == null) {
return translations.missingShippingExe;
if(_gameController.getVersionByName(text) != null) {
return translations.versionAlreadyExists;
}
return null;
@@ -324,44 +325,46 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return null;
}
Widget _buildBuildSelector(List<FortniteBuild> builds) => InfoLabel(
Widget get _buildSelector => InfoLabel(
label: translations.build,
child: FormField<FortniteBuild?>(
key: _formFieldKey,
validator: (data) => _checkBuild(data),
builder: (formContext) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: (_source.value == _BuildSource.local ? builds : builds.where((build) => build.available)).map((element) => _buildBuildItem(element)).toList(),
value: _build.value,
onChanged: (value) {
if(value == null){
return;
}
_build.value = value;
formContext.didChange(value);
formContext.validate();
_updateFormDefaults();
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ComboBox<FortniteBuild>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: downloadableBuilds.where((build) => build.available)
.map((element) => _buildBuildItem(element))
.toList(),
value: _build.value,
onChanged: (value) {
if(value == null){
return;
}
),
if(formContext.hasError)
const SizedBox(height: 4.0),
if(formContext.hasError)
Text(
formContext.errorText ?? "",
style: TextStyle(
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
),
_build.value = value;
formContext.didChange(value);
formContext.validate();
_updateFormDefaults();
}
),
if(formContext.hasError)
const SizedBox(height: 4.0),
if(formContext.hasError)
Text(
formContext.errorText ?? "",
style: TextStyle(
color: Colors.red.defaultBrushFor(FluentTheme.of(context).brightness)
),
SizedBox(
height: formContext.hasError ? 8.0 : 16.0
),
],
)
SizedBox(
height: formContext.hasError ? 8.0 : 16.0
),
],
)
)
);
@@ -370,40 +373,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return translations.selectBuild;
}
final versions = _gameController.versions.value;
if (versions.any((element) => data.version == element.content)) {
return translations.versionAlreadyExists;
}
return null;
}
ComboBoxItem<FortniteBuild> _buildBuildItem(FortniteBuild element) => ComboBoxItem<FortniteBuild>(
value: element,
child: Text(element.version.toString())
);
Widget _buildSourceSelector() => InfoLabel(
label: translations.source,
child: ComboBox<_BuildSource>(
placeholder: Text(translations.selectBuild),
isExpanded: true,
items: _BuildSource.values.map((entry) => _buildSourceItem(entry)).toList(),
value: _source.value,
onChanged: (value) {
if(value == null){
return;
}
_source.value = value;
_updateFormDefaults();
}
)
);
ComboBoxItem<_BuildSource> _buildSourceItem(_BuildSource element) => ComboBoxItem<_BuildSource>(
value: element,
child: Text(element.translatedName)
child: Text(element.gameVersion)
);
@@ -415,21 +390,21 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
];
Future<void> _updateFormDefaults() async {
if(_source.value != _BuildSource.local && _build.value?.available != true) {
if(_build.value?.available != true) {
_build.value = null;
}
final disks = WindowsDisk.available();
if(_source.value != _BuildSource.local && disks.isNotEmpty) {
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
final build = _build.value;
if(build == null){
return;
final build = _build.value;
if(build != null) {
_nameController.text = build.gameVersion;
_nameController.selection = TextSelection.collapsed(offset: build.gameVersion.length);
final disks = WindowsDisk.available();
if(disks.isNotEmpty) {
final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
final pathText = "${bestDisk.path}FortniteBuilds\\${build.gameVersion}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
}
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
_pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length);
}
_formKey.currentState?.validate();
@@ -443,18 +418,3 @@ enum _DownloadStatus {
error,
done
}
enum _BuildSource {
local,
githubArchive;
String get translatedName {
switch(this) {
case _BuildSource.local:
return translations.localBuild;
case _BuildSource.githubArchive:
return translations.githubArchive;
}
}
}

View File

@@ -0,0 +1,234 @@
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file/file_selector.dart';
import 'package:path/path.dart' as path;
import 'package:version/version.dart';
class ImportVersionDialog extends StatefulWidget {
final FortniteVersion? version;
final bool closable;
const ImportVersionDialog({Key? key, required this.version, required this.closable}) : super(key: key);
@override
State<ImportVersionDialog> createState() => _ImportVersionDialogState();
}
class _ImportVersionDialogState extends State<ImportVersionDialog> {
final TextEditingController _nameController = TextEditingController();
final GameController _gameController = Get.find<GameController>();
final TextEditingController _pathController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey();
final Rx<_ImportState> _validator = Rx(_ImportState.inputData);
@override
void initState() {
final version = widget.version;
if(version != null) {
_nameController.text = version.name;
_nameController.selection = TextSelection.collapsed(offset: version.name.length);
_pathController.text = version.location.path;
_pathController.selection = TextSelection.collapsed(offset: version.location.path.length);
}
super.initState();
}
@override
void dispose() {
_nameController.dispose();
_pathController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
key: _formKey,
child: Obx(() {
switch(_validator.value) {
case _ImportState.inputData:
return FormDialog(
content: _importBody,
buttons: _importButtons
);
case _ImportState.validating:
return ProgressDialog(
text: translations.importingVersion
);
case _ImportState.success:
return InfoDialog(
text: translations.importedVersion
);
case _ImportState.missingShippingExeError:
return InfoDialog(
text: translations.importVersionMissingShippingExeError(kShippingExe)
);
case _ImportState.multipleShippingExesError:
return InfoDialog(
text: translations.importVersionMultipleShippingExesError(kShippingExe)
);
case _ImportState.unsupportedVersionError:
return InfoDialog(
text: translations.importVersionUnsupportedVersionError
);
}
})
);
Widget get _importBody => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InfoLabel(
label: translations.versionName,
child: TextFormBox(
controller: _nameController,
validator: _checkVersionName,
placeholder: translations.versionNameLabel,
autovalidateMode: AutovalidateMode.onUserInteraction
),
),
const SizedBox(
height: 16.0
),
FileSelector(
label: translations.gameFolderTitle,
placeholder: translations.gameFolderPlaceholder,
windowTitle: translations.gameFolderPlaceWindowTitle,
controller: _pathController,
validator: _checkGamePath,
validatorMode: AutovalidateMode.onUserInteraction,
folder: true,
allowNavigator: true,
onSelected: (selected) {
var name = path.basename(selected);
if(_gameController.getVersionByName(name) != null) {
var counter = 1;
while(_gameController.getVersionByName("$name-$counter") != null) {
counter++;
}
name = "$name-$counter";
}
_nameController.text = name;
_nameController.selection = TextSelection.collapsed(offset: name.length);
},
),
const SizedBox(
height: 16.0
)
],
);
List<DialogButton> get _importButtons => [
if(widget.closable)
DialogButton(type: ButtonType.secondary),
DialogButton(
text: translations.saveLocalVersion,
type: widget.closable ? ButtonType.primary : ButtonType.only,
color: FluentTheme.of(context).accentColor,
onTap: _importVersion,
)
];
void _importVersion() async {
final topResult = _formKey.currentState?.validate();
if(topResult != true) {
return;
}
_validator.value = _ImportState.validating;
final name = _nameController.text.trim();
final directory = Directory(_pathController.text.trim());
final files = await Future.wait([
Future.delayed(const Duration(seconds: 1)).then((_) => <File>[]),
findFiles(directory, kShippingExe).then((files) async {
if(files.length == 1) {
await patchHeadless(files.first);
}
return files;
})
]).then((values) => values.expand((entry) => entry).toList());
if (files.isEmpty) {
_validator.value = _ImportState.missingShippingExeError;
return;
}
if(files.length != 1) {
_validator.value = _ImportState.multipleShippingExesError;
return;
}
final gameVersion = await extractGameVersion(files.first.path, path.basename(directory.path));
try {
if(Version.parse(gameVersion) >= kMaxAllowedVersion) {
_validator.value = _ImportState.unsupportedVersionError;
return;
}
}catch(_) {
}
if(widget.version == null) {
final version = FortniteVersion(
name: name,
gameVersion: gameVersion,
location: files.first.parent
);
_gameController.addVersion(version);
}else {
widget.version?.name = name;
widget.version?.gameVersion = gameVersion;
widget.version?.location = files.first.parent;
}
_validator.value = _ImportState.success;
}
String? _checkVersionName(String? text) {
final version = widget.version;
if(version != null && version.name == text) {
return null;
}
if (text == null || text.isEmpty) {
return translations.emptyVersionName;
}
if(_gameController.getVersionByName(text) != null) {
return translations.versionAlreadyExists;
}
return null;
}
String? _checkGamePath(String? input) {
if(input == null || input.isEmpty) {
return translations.emptyGamePath;
}
final directory = Directory(input);
if(!directory.existsSync()) {
return translations.directoryDoesNotExist;
}
return null;
}
}
enum _ImportState {
inputData,
validating,
success,
missingShippingExeError,
multipleShippingExesError,
unsupportedVersionError
}

View File

@@ -8,15 +8,46 @@ import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/dialog.dart';
import 'package:reboot_launcher/src/messenger/info_bar.dart';
import 'package:reboot_launcher/src/widget/message/version.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/download_version.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version/import_version.dart';
import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key);
static SettingTile buildTile({
required GlobalKey<OverlayTargetState> key
}) => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription),
contentWidth: null,
content: ConstrainedBox(
constraints: BoxConstraints(
minWidth: SettingTile.kDefaultContentWidth,
),
child: OverlayTarget(
key: key,
child: const VersionSelector(),
)
)
);
static Future<void> openImportDialog(FortniteVersion? version) => showRebootDialog<bool>(
builder: (context) => ImportVersionDialog(
version: version,
closable: true,
),
dismissWithEsc: true
);
static Future<void> openDownloadDialog() => showRebootDialog<bool>(
builder: (context) => AddVersionDialog(
builder: (context) => DownloadVersionDialog(
closable: true,
),
dismissWithEsc: true
@@ -34,7 +65,7 @@ class _VersionSelectorState extends State<VersionSelector> {
@override
Widget build(BuildContext context) => Obx(() {
return _createOptionsMenu(
version: _gameController.selectedVersion,
version: _gameController.selectedVersion.value,
close: false,
child: FlyoutTarget(
controller: _flyoutController,
@@ -42,7 +73,7 @@ class _VersionSelectorState extends State<VersionSelector> {
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(
_gameController.selectedVersion?.content.toString() ?? translations.selectVersion,
_gameController.selectedVersion.value?.name ?? translations.selectVersion,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -65,7 +96,7 @@ class _VersionSelectorState extends State<VersionSelector> {
var result = await _flyoutController.showFlyout<_ContextualOption?>(
builder: (context) => MenuFlyout(
items: _ContextualOption.values
.map((entry) => _createOption(context, entry))
.map((entry) => _createOption(entry))
.toList()
)
);
@@ -76,7 +107,7 @@ class _VersionSelectorState extends State<VersionSelector> {
List<MenuFlyoutItem> _createSelectorItems(BuildContext context) {
final items = _gameController.versions.value
.map((version) => _createVersionItem(context, version))
.map((version) => _createVersionItem(version))
.toList();
items.add(MenuFlyoutItem(
trailing: Padding(
@@ -87,12 +118,23 @@ class _VersionSelectorState extends State<VersionSelector> {
),
),
text: Text(translations.addVersion),
onPressed: () => VersionSelector.openImportDialog(null)
));
items.add(MenuFlyoutItem(
trailing: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
FluentIcons.arrow_download_24_regular,
size: 14
),
),
text: Text(translations.downloadVersion),
onPressed: VersionSelector.openDownloadDialog
));
return items;
}
MenuFlyoutItem _createVersionItem(BuildContext context, FortniteVersion version) => MenuFlyoutItem(
MenuFlyoutItem _createVersionItem(FortniteVersion version) => MenuFlyoutItem(
text: Listener(
onPointerDown: (event) async {
if (event.kind != PointerDeviceKind.mouse || event.buttons != kSecondaryMouseButton) {
@@ -101,7 +143,7 @@ class _VersionSelectorState extends State<VersionSelector> {
await _openVersionOptions(version);
},
child: Text(version.content.toString())
child: Text(version.name)
),
trailing: IconButton(
onPressed: () => _openVersionOptions(version),
@@ -109,14 +151,14 @@ class _VersionSelectorState extends State<VersionSelector> {
FluentIcons.more_vertical_24_regular
)
),
onPressed: () => _gameController.selectedVersion = version
onPressed: () => _gameController.selectedVersion.value = version
);
Future<void> _openVersionOptions(FortniteVersion version) async {
final result = await _flyoutController.showFlyout<_ContextualOption?>(
builder: (context) => MenuFlyout(
items: _ContextualOption.values
.map((entry) => _createOption(context, entry))
.map((entry) => _createOption(entry))
.toList()
),
barrierDismissible: true,
@@ -139,8 +181,19 @@ class _VersionSelectorState extends State<VersionSelector> {
launchUrl(version.location.uri)
.onError((error, stackTrace) => _onExplorerError());
break;
case _ContextualOption.modify:
if(!mounted){
return;
}
if(close) {
Navigator.of(context).pop();
}
await VersionSelector.openImportDialog(version);
break;
case _ContextualOption.delete:
final result = await _openDeleteDialog(context, version) ?? false;
final result = await _openDeleteDialog(version) ?? false;
if(!mounted || !result){
return;
}
@@ -160,7 +213,7 @@ class _VersionSelectorState extends State<VersionSelector> {
}
}
MenuFlyoutItem _createOption(BuildContext context, _ContextualOption entry) {
MenuFlyoutItem _createOption(_ContextualOption entry) {
return MenuFlyoutItem(
text: Text(entry.translatedName),
onPressed: () => Navigator.of(context).pop(entry)
@@ -168,11 +221,15 @@ class _VersionSelectorState extends State<VersionSelector> {
}
bool _onExplorerError() {
showRebootInfoBar(translations.missingVersion);
showRebootInfoBar(
translations.missingVersionError,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
return false;
}
Future<bool?> _openDeleteDialog(BuildContext context, FortniteVersion version) {
Future<bool?> _openDeleteDialog(FortniteVersion version) {
return showRebootDialog<bool>(
builder: (context) => ContentDialog(
content: Column(
@@ -209,18 +266,21 @@ class _VersionSelectorState extends State<VersionSelector> {
)
);
}
@override
GameController get gameController => _gameController;
}
enum _ContextualOption {
openExplorer,
delete;
modify,
delete
}
extension _ContextualOptionExtension on _ContextualOption {
String get translatedName {
switch(this) {
case _ContextualOption.openExplorer:
return translations.openInExplorer;
case _ContextualOption.delete:
return translations.delete;
}
return this == _ContextualOption.openExplorer ? translations.openInExplorer
: this == _ContextualOption.modify ? translations.modify
: translations.delete;
}
}
}

View File

@@ -1,26 +0,0 @@
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:reboot_launcher/src/messenger/overlay.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/fluent/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version/version_selector.dart';
SettingTile buildVersionSelector({
required GlobalKey<OverlayTargetState> key
}) => SettingTile(
icon: Icon(
FluentIcons.play_24_regular
),
title: Text(translations.selectFortniteName),
subtitle: Text(translations.selectFortniteDescription),
contentWidth: null,
content: ConstrainedBox(
constraints: BoxConstraints(
minWidth: SettingTile.kDefaultContentWidth,
),
child: OverlayTarget(
key: key,
child: const VersionSelector(),
)
)
);

View File

@@ -1,6 +1,6 @@
name: reboot_launcher
description: Graphical User Interface for Project Reboot
version: "10.0.7"
version: "10.0.8"
publish_to: 'none'
@@ -33,9 +33,6 @@ dependencies:
# Window management
window_manager: ^0.4.2
# Extract zip archives (for example the reboot.zip)
archive: ^3.6.1
# Cryptographic functions
bcrypt: ^1.1.3
pointycastle: ^3.9.1
@@ -49,6 +46,8 @@ dependencies:
get: ^4.6.6
# Native utilities
archive: ^3.6.1
watcher: ^1.1.1
clipboard: ^0.1.3
app_links: ^6.3.2
windows_taskbar: ^1.1.2

View File

@@ -55,6 +55,7 @@ begin
' Allow DLL injection',
' The Reboot Launcher needs to inject DLLs into Fortnite to create the game server',
'Selecting the option below will add the Reboot Launcher to the Windows Exclusions list. ' +
'If you are using another AntiVirus, it might be necessary to add an exclusion manually. ' +
'This is necessary because DLL injection is often detected as a virus, but is necessary to modify Fortnite. ' +
'This option was designed for advanced users who want to manually manage the exclusions list on their machine. ' +
'If you do not trust the Reboot Launcher, you can audit the source code at https://github.com/Auties00/reboot_launcher and build it from source.',