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

@@ -1,60 +1,95 @@
import 'package:interact/interact.dart';
import 'package:reboot_cli/cli.dart';
import 'package:tint/tint.dart';
import 'dart:io';
import 'dart:isolate';
import 'package:interact_cli/interact_cli.dart';
import 'package:reboot_cli/src/controller/config.dart';
import 'package:reboot_cli/src/util/console.dart';
import 'package:reboot_cli/src/util/extensions.dart';
import 'package:reboot_common/common.dart';
import 'package:tint/tint.dart';
import 'package:version/version.dart';
const Command _buildList = Command(name: 'list', parameters: [], subCommands: []);
const Command _buildImport = Command(name: 'import', parameters: ['version', 'path'], subCommands: []);
const Command _buildDownload = Command(name: 'download', parameters: ['version', 'path'], subCommands: []);
const Command _build = Command(name: 'build', parameters: [], subCommands: [_buildImport, _buildDownload]);
const Command _build = Command(name: 'versions', parameters: [], subCommands: [_buildList, _buildImport, _buildDownload]);
const Command _play = Command(name: 'play', parameters: [], subCommands: []);
const Command _host = Command(name: 'host', parameters: [], subCommands: []);
const Command _backend = Command(name: 'backend', parameters: [], subCommands: []);
final List<String> _versions = downloadableBuilds.map((build) => build.version.toString()).toList(growable: false);
void main(List<String> args) {
_welcome();
void main(List<String> args) async {
enableLoggingToConsole = false;
useDefaultPath = true;
final parser = Parser(commands: [_build, _play, _host, _backend]);
print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());
final parser = ConsoleParser(
commands: [
_build,
_play,
_host,
_backend
]
);
final command = parser.parse(args);
print(command);
_handleRootCommand(command);
await _handleRootCommand(command);
}
void _handleRootCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
case 'build':
_handleBuildCommand(call?.subCall);
Future<void> _handleRootCommand(CommandCall? command) async {
if(command == null) {
await _askRootCommand();
return;
}
switch (command.name) {
case 'versions':
await _handleBuildCommand(command.subCall);
break;
case 'play':
_handleBuildCommand(call?.subCall);
_handlePlayCommand(command.subCall);
break;
case 'host':
_handleBuildCommand(call?.subCall);
_handleHostCommand(command.subCall);
break;
case 'backend':
_handleBuildCommand(call?.subCall);
_handleBackendCommand(command.subCall);
break;
default:
_askRootCommand();
await _askRootCommand();
break;
}
}
void _askRootCommand() {
Future<void> _askRootCommand() async {
final commands = [_build.name, _play.name, _host.name, _backend.name];
final commandSelector = Select.withTheme(
prompt: ' Select a command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
_handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
await _handleRootCommand(CommandCall(name: commands[commandSelector.interact()]));
}
void _handleBuildCommand(CommandCall? call) {
switch(call == null ? null : call.name) {
Future<void> _handleBuildCommand(CommandCall? call) async {
if(call == null) {
_askBuildCommand();
return;
}
switch(call.name) {
case 'import':
_handleBuildImportCommand(call!);
await _handleBuildImportCommand(call);
break;
case 'download':
_handleBuildDownloadCommand(call!);
_handleBuildDownloadCommand(call);
break;
case 'list':
_handleBuildListCommand(call);
break;
default:
_askBuildCommand();
@@ -62,23 +97,283 @@ void _handleBuildCommand(CommandCall? call) {
}
}
void _handleBuildImportCommand(CommandCall call) {
final version = call.parameters['path'];
final path = call.parameters['path'];
print(version);
print(path);
void _handleBuildListCommand(CommandCall commandCall) {
final versions = readVersions();
final versionSelector = Select.withTheme(
prompt: ' Select a version:',
options: versions.map((version) => version.content.toString()).toList(growable: false),
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
final version = versions[versionSelector.interact()];
final actionSelector = Select.withTheme(
prompt: ' Select an action:',
options: ['Play', 'Host', 'Delete', 'Open in Explorer'],
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
actionSelector.interact();
}
void _handleBuildDownloadCommand(CommandCall call) {
Future<void> _handleBuildImportCommand(CommandCall call) async {
final version = _getOrPromptVersion(call);
if(version == null) {
return;
}
final path = await _getOrPromptPath(call, true);
if(path == null) {
return;
}
final fortniteVersion = FortniteVersion(
name: "dummy",
content: Version.parse(version),
location: Directory(path)
);
writeVersion(fortniteVersion);
print('');
print('✅ Imported build: ${version.green()}');
}
String? _getOrPromptVersion(CommandCall call) {
final version = call.parameters['version'];
if(version != null) {
final result = version.trim();
if (_versions.contains(result)) {
return result;
}
print('');
print("❌ Unknown version: $result");
return null;
}
stdout.write('❓ Type a version: ');
final result = runAutoComplete(_autocompleteVersion).trim();
if(_versions.contains(result)) {
print('✅ Type a version: ${result.green()}');
return result;
}
print('');
print("❌ Unknown version: $version");
return null;
}
Future<String?> _getOrPromptPath(CommandCall call, bool existing) async {
var path = call.parameters['path'];
if(path != null) {
final result = path.trim();
final check = await _checkBuildPath(result, existing);
if(!check) {
return null;
}
return result;
}
stdout.write('❓ Type a path: ');
final result = runAutoComplete(_autocompletePath).trim();
final check = await _checkBuildPath(result, existing);
if(!check) {
return null;
}
print('✅ Type a path: ${result.green()}');
return result;
}
Future<bool> _checkBuildPath(String path, bool existing) async {
final directory = Directory(path);
if(!directory.existsSync()) {
if(existing) {
print('');
print("❌ Unknown path: $path");
return false;
}else {
directory.createSync(recursive: true);
}
}
if (existing) {
final checker = Spinner.withTheme(
icon: '',
rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished looking for FortniteClient-Win64-Shipping.exe' : 'Looking for FortniteClient-Win64-Shipping.exe...',
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
).interact();
final result = await Future.wait([
Future.delayed(const Duration(seconds: 1)).then((_) => true),
Isolate.run(() => FortniteVersionExtension.findFiles(directory, "FortniteClient-Win64-Shipping.exe") != null)
]).then((values) => values.reduce((first, second) => first && second));
checker.done();
if(!result) {
print("❌ Cannot find FortniteClient-Win64-Shipping.exe: $path");
return false;
}
}
return true;
}
String? _autocompleteVersion(String input) => input.isEmpty ? null : _versions.firstWhereOrNull((version) => version.toLowerCase().startsWith(input.toLowerCase()));
String? _autocompletePath(String path) {
try {
if (path.isEmpty) {
return null;
}
final String separator = Platform.isWindows ? '\\' : '/';
path = path.replaceAll('\\', separator);
if (FileSystemEntity.isFileSync(path)) {
return null;
}
if(FileSystemEntity.isDirectorySync(path)) {
return path.endsWith(separator) ? null : path + separator;
}
final lastSeparatorIndex = path.lastIndexOf(separator);
String directoryPath;
String partialName;
String prefixPath;
if (lastSeparatorIndex == -1) {
directoryPath = '';
partialName = path;
prefixPath = '';
} else {
directoryPath = path.substring(0, lastSeparatorIndex);
partialName = path.substring(lastSeparatorIndex + 1);
prefixPath = path.substring(0, lastSeparatorIndex + 1);
if (directoryPath.isEmpty && lastSeparatorIndex == 0) {
directoryPath = separator;
} else if (directoryPath.isEmpty) {
directoryPath = '.';
}
}
final dir = Directory(directoryPath);
if (!dir.existsSync()) {
return null;
}
final entries = dir.listSync();
final matches = <FileSystemEntity>[];
for (var entry in entries) {
final name = entry.path.split(separator).last;
if (name.startsWith(partialName)) {
matches.add(entry);
}
}
if (matches.isEmpty) {
return null;
}
matches.sort((a, b) {
final aIsDir = a is Directory;
final bIsDir = b is Directory;
if (aIsDir != bIsDir) {
return aIsDir ? -1 : 1;
}
final aName = a.path.split(separator).last;
final bName = b.path.split(separator).last;
if (aName.length != bName.length) {
return aName.length - bName.length;
}
return aName.compareTo(bName);
});
final bestMatch = matches.first;
final bestMatchName = bestMatch.path.split(separator).last;
var result = prefixPath + bestMatchName;
if (bestMatch is Directory) {
result = result.endsWith(separator) ? result : result + separator;
}
return result;
} catch (_) {
return null;
}
}
Future<void> _handleBuildDownloadCommand(CommandCall call) async {
final version = _getOrPromptVersion(call);
if(version == null) {
return;
}
final parsedVersion = Version.parse(version);
final build = downloadableBuilds.firstWhereOrNull((build) => build.version == parsedVersion);
if(build == null) {
print('');
print("❌ Cannot find mirror for version: $parsedVersion");
return;
}
final path = await _getOrPromptPath(call, false);
if(path == null) {
return;
}
double progress = 0;
bool extracting = false;
final downloader = Spinner.withTheme(
icon: '',
rightPrompt: (status) => status != SpinnerStateType.inProgress ? 'Finished ${extracting ? 'extracting' : 'downloading'} ${parsedVersion.toString()}' : '${extracting ? 'Extracting' : 'Downloading'} ${parsedVersion.toString()} (${progress.round()}%)...',
theme: Theme.colorfulTheme.copyWith(successSuffix: '', errorPrefix: '', spinners: '🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛'.split(' '))
).interact();
final parsedDirectory = Directory(path);
final receivePort = ReceivePort();
SendPort? sendPort;
receivePort.listen((message) {
if(message is FortniteBuildDownloadProgress) {
if(message.progress >= 100) {
sendPort?.send(kStopBuildDownloadSignal);
stopDownloadServer();
downloader.done();
receivePort.close();
final fortniteVersion = FortniteVersion(
name: "dummy",
content: parsedVersion,
location: parsedDirectory
);
writeVersion(fortniteVersion);
print('');
print('✅ Downloaded build: ${version.green()}');
}else {
progress = message.progress;
extracting = message.extracting;
}
}else if(message is SendPort) {
sendPort = message;
}else {
sendPort?.send(kStopBuildDownloadSignal);
stopDownloadServer();
downloader.done();
receivePort.close();
print("❌ Cannot download build: $message");
}
});
final options = FortniteBuildDownloadOptions(
build,
parsedDirectory,
receivePort.sendPort
);
await downloadArchiveBuild(options);
}
void _askBuildCommand() {
final commands = [_buildImport.name, _buildDownload.name];
final commands = [_buildList.name, _buildImport.name, _buildDownload.name];
final commandSelector = Select.withTheme(
prompt: ' Select a build command:',
prompt: ' Select a version command:',
options: commands,
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '')
theme: Theme.colorfulTheme.copyWith(inputPrefix: '', inputSuffix: '', successSuffix: '', errorPrefix: '')
);
_handleBuildCommand(CommandCall(name: commands[commandSelector.interact()]));
}
@@ -93,10 +388,4 @@ void _handleHostCommand(CommandCall? call) {
void _handleBackendCommand(CommandCall? call) {
}
void _welcome() => print("""
🎮 Reboot Launcher
🔥 Launch, manage, and play Fortnite using Project Reboot!
🚀 Developed by Auties00 - Version 10.0.7
""".green());
}

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,24 @@
import 'dart:convert';
import 'dart:io';
import 'package:reboot_common/common.dart';
List<FortniteVersion> readVersions() {
final file = _versionsFile;
if(!file.existsSync()) {
return [];
}
Iterable decodedVersionsJson = jsonDecode(file.readAsStringSync());
return decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry))
.toList();
}
void writeVersion(FortniteVersion version) {
final versions = readVersions();
versions.add(version);
_versionsFile.writeAsString(jsonEncode(versions.map((version) => version.toJson()).toList()));
}
File get _versionsFile => File('${installationDirectory.path}/versions.json');

View File

@@ -1,9 +1,12 @@
import 'dart:collection';
import 'dart:io';
import 'package:dart_console/dart_console.dart';
class Parser {
typedef AutoComplete = String? Function(String);
class ConsoleParser {
final List<Command> commands;
Parser({required this.commands});
ConsoleParser({required this.commands});
CommandCall? parse(List<String> args) {
var position = 0;
@@ -86,4 +89,72 @@ class CommandCall {
@override
String toString() => 'CommandCall{name: $name, parameters: $parameters, subCall: $subCall}';
}
}
String runAutoComplete(AutoComplete completion) {
final console = Console();
console.rawMode = true;
final position = console.cursorPosition!;
var currentInput = '';
var running = true;
var result = '';
while (running) {
final key = console.readKey();
switch (key.controlChar) {
case ControlCharacter.ctrlC:
running = false;
break;
case ControlCharacter.enter:
_eraseUntil(console, position);
console.write(currentInput);
console.writeLine();
result = currentInput;
running = false;
break;
case ControlCharacter.tab:
final suggestion = completion(currentInput);
if (suggestion != null) {
_eraseUntil(console, position);
currentInput = suggestion;
console.write(currentInput);
}
break;
case ControlCharacter.backspace:
if (currentInput.isNotEmpty) {
currentInput = currentInput.substring(0, currentInput.length - 1);
_eraseUntil(console, position);
console.write(currentInput);
_showSuggestion(console, position, currentInput, completion);
}
break;
default:
currentInput += key.char;
console.write(key.char);
_showSuggestion(console, position, currentInput, completion);
}
}
return result;
}
void _eraseUntil(Console console, Coordinate position) {
console.cursorPosition = position;
stdout.write('\x1b[K');
}
void _showSuggestion(Console console, Coordinate position, String input, AutoComplete completion) {
final suggestion = completion(input);
if(suggestion == null) {
_eraseUntil(console, position);
console.write(input);
}else if(suggestion.length > input.length) {
final remaining = suggestion.substring(input.length);
final cursorPosition = console.cursorPosition;
console.setForegroundColor(ConsoleColor.brightBlack);
console.write(remaining);
console.resetColorAttributes();
console.cursorPosition = cursorPosition;
}
}

View File

@@ -0,0 +1,10 @@
extension IterableExtension<E> on Iterable<E> {
E? firstWhereOrNull(bool test(E element)) {
for (final element in this) {
if (test(element)) {
return element;
}
}
return null;
}
}

View File

@@ -5,14 +5,15 @@ version: "10.0.7"
publish_to: 'none'
environment:
sdk: ">=2.19.0 <=3.5.3"
sdk: ">=3.0.0 <=3.5.3"
dependencies:
reboot_common:
path: ./../common
tint: ^2.0.1
interact: ^2.2.0
interact_cli: ^2.4.0
args: ^2.6.0
version: ^3.0.2
dependency_overrides:
xml: ^6.3.0

View File

@@ -1,7 +1,6 @@
export 'package:reboot_common/src/constant/backend.dart';
export 'package:reboot_common/src/constant/game.dart';
export 'package:reboot_common/src/constant/supabase.dart';
export 'package:reboot_common/src/extension/path.dart';
export 'package:reboot_common/src/extension/process.dart';
export 'package:reboot_common/src/model/fortnite_build.dart';
export 'package:reboot_common/src/model/fortnite_version.dart';
@@ -13,10 +12,7 @@ export 'package:reboot_common/src/model/update_timer.dart';
export 'package:reboot_common/src/model/fortnite_server.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';
export 'package:reboot_common/src/util/network.dart';
export 'package:reboot_common/src/util/patcher.dart';
export 'package:reboot_common/src/util/path.dart';
export 'package:reboot_common/src/util/process.dart';
export 'package:reboot_common/src/util/log.dart';
export 'package:reboot_common/src/util/downloader.dart';
export 'package:reboot_common/src/util/os.dart';
export 'package:reboot_common/src/util/log.dart';
export 'package:reboot_common/src/util/game.dart';

View File

@@ -1,3 +1,5 @@
import 'package:version/version.dart';
const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1";
@@ -21,6 +23,10 @@ const List<String> kCannotConnectErrors = [
"Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout"
];
const String kGameFinishedLine = "PlayersLeft: 1";
const String kGameFinishedLine = "TeamsLeft: 1";
const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";
const String kDisplayInitializedLine = "Initialized";
const String kShippingExe = "FortniteClient-Win64-Shipping.exe";
const String kLauncherExe = "FortniteLauncher.exe";
const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe";
final Version kMaxAllowedVersion = Version.parse("30.10");

View File

@@ -1,54 +0,0 @@
import 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
extension FortniteVersionExtension on FortniteVersion {
static String _marker = "FortniteClient.mod";
static File? findFile(Directory directory, String name) {
try{
for(final child in directory.listSync()) {
if(child is Directory) {
if(!path.basename(child.path).startsWith("\.")) {
final result = findFile(child, name);
if(result != null) {
return result;
}
}
}else if(child is File) {
if(path.basename(child.path) == name) {
return child;
}
}
}
return null;
}catch(_){
return null;
}
}
Future<File?> get shippingExecutable async {
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
if(result == null) {
return null;
}
final marker = findFile(location, _marker);
if(marker != null) {
return result;
}
await Isolate.run(() => patchHeadless(result));
await File("${location.path}\\$_marker").create();
return result;
}
File? get launcherExecutable => findFile(location, "FortniteLauncher.exe");
File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe");
File? get splashBitmap => findFile(location, "Splash.bmp");
}

View File

@@ -1,15 +1,13 @@
import 'dart:io';
import 'dart:isolate';
import 'package:version/version.dart';
class FortniteBuild {
final Version version;
final String gameVersion;
final String link;
final bool available;
FortniteBuild({
required this.version,
required this.gameVersion,
required this.link,
required this.available
});

View File

@@ -1,22 +1,20 @@
import 'dart:io';
import 'package:version/version.dart';
class FortniteVersion {
Version content;
String name;
String gameVersion;
Directory location;
FortniteVersion.fromJson(json)
: content = Version.parse(json["content"]),
: name = json["name"],
gameVersion = json["gameVersion"],
location = Directory(json["location"]);
FortniteVersion({required this.content, required this.location});
FortniteVersion({required this.name, required this.gameVersion, required this.location});
Map<String, dynamic> toJson() => {
'content': content.toString(),
'name': name,
'gameVersion': gameVersion,
'location': location.path
};
@override
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
}

View File

@@ -5,14 +5,13 @@ import 'package:version/version.dart';
class GameInstance {
final Version version;
final String version;
final int gamePid;
final int? launcherPid;
final int? eacPid;
final List<InjectableDll> injectedDlls;
final GameServerType? serverType;
final bool headless;
bool launched;
bool movedToVirtualDesktop;
bool tokenError;
bool killed;
GameInstance? child;
@@ -22,9 +21,9 @@ class GameInstance {
required this.gamePid,
required this.launcherPid,
required this.eacPid,
required this.serverType,
required this.headless,
required this.child
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
}): tokenError = false, killed = false, launched = false, injectedDlls = [];
void kill() {
GameInstance? child = this;
@@ -35,20 +34,16 @@ class GameInstance {
}
void _kill() {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
if(launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
}
if(eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
if(!killed) {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
if (launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
}
if (eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
}
}
}
}
enum GameServerType {
headless,
virtualWindow,
window
}

View File

@@ -137,9 +137,14 @@ Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onEr
window: detached,
);
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
var killed = false;
process.stdError.listen((error) {
log("[BACKEND] Error: $error");
onError?.call(error);
if(!killed) {
log("[BACKEND] Error: $error");
killed = true;
process.kill(ProcessSignal.sigterm);
onError?.call(error);
}
});
if(!detached) {
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));

View File

@@ -1,464 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart';
import 'package:uuid/uuid.dart';
import 'package:version/version.dart';
import 'package:http/http.dart' as http;
const String kStopBuildDownloadSignal = "kill";
final int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final String _ariaSecret = "RebootLauncher";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(version: Version.parse("1.7.2"), link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(version: Version.parse("1.8"), link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(version: Version.parse("1.8.1"), link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(version: Version.parse("1.8.2"), link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(version: Version.parse("1.9"), link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(version: Version.parse("1.9.1"), link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(version: Version.parse("1.10"), link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(version: Version.parse("1.11"), link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(version: Version.parse("2.1.0"), link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(version: Version.parse("2.2.0"), link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(version: Version.parse("2.3"), link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(version: Version.parse("2.4.0"), link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(version: Version.parse("2.4.2"), link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(version: Version.parse("2.5.0"), link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(version: Version.parse("3.0"), link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(version: Version.parse("3.1"), link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(version: Version.parse("3.1.1"), link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(version: Version.parse("3.2"), link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(version: Version.parse("3.3"), link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(version: Version.parse("3.5"), link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(version: Version.parse("3.6"), link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(version: Version.parse("4.0"), link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(version: Version.parse("4.1"), link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(version: Version.parse("4.2"), link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(version: Version.parse("4.4"), link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(version: Version.parse("4.5"), link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(version: Version.parse("5.00"), link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(version: Version.parse("5.0.1"), link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(version: Version.parse("5.10"), link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(version: Version.parse("5.21"), link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(version: Version.parse("5.30"), link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(version: Version.parse("5.40"), link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(version: Version.parse("6.00"), link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(version: Version.parse("6.01"), link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(version: Version.parse("6.1.1"), link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(version: Version.parse("6.02"), link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(version: Version.parse("6.2.1"), link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10"), link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(version: Version.parse("6.10.1"), link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(version: Version.parse("6.10.2"), link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(version: Version.parse("6.21"), link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(version: Version.parse("6.22"), link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(version: Version.parse("6.30"), link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(version: Version.parse("6.31"), link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(version: Version.parse("7.00"), link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(version: Version.parse("7.10"), link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(version: Version.parse("7.20"), link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(version: Version.parse("7.30"), link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(version: Version.parse("7.40"), link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(version: Version.parse("8.00"), link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(version: Version.parse("8.20"), link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(version: Version.parse("8.30"), link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(version: Version.parse("8.40"), link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(version: Version.parse("8.50"), link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(version: Version.parse("8.51"), link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(version: Version.parse("9.00"), link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(version: Version.parse("9.01"), link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(version: Version.parse("9.10"), link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(version: Version.parse("9.21"), link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(version: Version.parse("9.30"), link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(version: Version.parse("9.40"), link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(version: Version.parse("9.41"), link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(version: Version.parse("10.00"), link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(version: Version.parse("10.10"), link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(version: Version.parse("10.20"), link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(version: Version.parse("10.31"), link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(version: Version.parse("10.40"), link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(version: Version.parse("11.00"), link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(version: Version.parse("11.31"), link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(version: Version.parse("12.00"), link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(version: Version.parse("12.21"), link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(version: Version.parse("12.50"), link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(version: Version.parse("12.61"), link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(version: Version.parse("13.00"), link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(version: Version.parse("13.40"), link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(version: Version.parse("14.00"), link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(version: Version.parse("14.40"), link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(version: Version.parse("14.60"), link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(version: Version.parse("15.30"), link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(version: Version.parse("16.40"), link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(version: Version.parse("17.30"), link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(version: Version.parse("17.50"), link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(version: Version.parse("18.40"), link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(version: Version.parse("19.10"), link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(version: Version.parse("20.40"), link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--check-certificate=false"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
"token:${_ariaSecret}"
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
stopped.complete();
}
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}

View File

@@ -1,82 +0,0 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
const String _kRebootBelowS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
const String _kRebootAboveS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<void> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return;
}
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}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
}
Future<void> downloadRebootDll(File file, String url, bool aboveS20) async {
Directory? outputDir;
try {
var response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
if(response.statusCode != 200) {
throw Exception("status code ${response.statusCode}");
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

@@ -0,0 +1,546 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:reboot_common/src/extension/types.dart';
import 'package:uuid/uuid.dart';
final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
const String _kRebootBelowS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootFallback.zip";
const String _kRebootAboveS20FallbackDownloadUrl =
"https://github.com/Auties00/reboot_launcher/raw/master/gui/dependencies/dlls/RebootS20Fallback.zip";
const String kStopBuildDownloadSignal = "kill";
final int _ariaPort = 6800;
final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
final List<FortniteBuild> downloadableBuilds = [
FortniteBuild(gameVersion: "1.7.2", link: "https://public.simplyblk.xyz/1.7.2.zip", available: true),
FortniteBuild(gameVersion: "1.8", link: "https://public.simplyblk.xyz/1.8.rar", available: true),
FortniteBuild(gameVersion: "1.8.1", link: "https://public.simplyblk.xyz/1.8.1.rar", available: true),
FortniteBuild(gameVersion: "1.8.2", link: "https://public.simplyblk.xyz/1.8.2.rar", available: true),
FortniteBuild(gameVersion: "1.9", link: "https://public.simplyblk.xyz/1.9.rar", available: true),
FortniteBuild(gameVersion: "1.9.1", link: "https://public.simplyblk.xyz/1.9.1.rar", available: true),
FortniteBuild(gameVersion: "1.10", link: "https://public.simplyblk.xyz/1.10.rar", available: true),
FortniteBuild(gameVersion: "1.11", link: "https://public.simplyblk.xyz/1.11.zip", available: true),
FortniteBuild(gameVersion: "2.1.0", link: "https://public.simplyblk.xyz/2.1.0.zip", available: true),
FortniteBuild(gameVersion: "2.2.0", link: "https://public.simplyblk.xyz/2.2.0.rar", available: true),
FortniteBuild(gameVersion: "2.3", link: "https://public.simplyblk.xyz/2.3.rar", available: true),
FortniteBuild(gameVersion: "2.4.0", link: "https://public.simplyblk.xyz/2.4.0.zip", available: true),
FortniteBuild(gameVersion: "2.4.2", link: "https://public.simplyblk.xyz/2.4.2.zip", available: true),
FortniteBuild(gameVersion: "2.5.0", link: "https://public.simplyblk.xyz/2.5.0.rar", available: true),
FortniteBuild(gameVersion: "3.0", link: "https://public.simplyblk.xyz/3.0.zip", available: true),
FortniteBuild(gameVersion: "3.1", link: "https://public.simplyblk.xyz/3.1.rar", available: true),
FortniteBuild(gameVersion: "3.1.1", link: "https://public.simplyblk.xyz/3.1.1.zip", available: true),
FortniteBuild(gameVersion: "3.2", link: "https://public.simplyblk.xyz/3.2.zip", available: true),
FortniteBuild(gameVersion: "3.3", link: "https://public.simplyblk.xyz/3.3.rar", available: true),
FortniteBuild(gameVersion: "3.5", link: "https://public.simplyblk.xyz/3.5.rar", available: true),
FortniteBuild(gameVersion: "3.6", link: "https://public.simplyblk.xyz/3.6.zip", available: true),
FortniteBuild(gameVersion: "4.0", link: "https://public.simplyblk.xyz/4.0.zip", available: true),
FortniteBuild(gameVersion: "4.1", link: "https://public.simplyblk.xyz/4.1.zip", available: true),
FortniteBuild(gameVersion: "4.2", link: "https://public.simplyblk.xyz/4.2.zip", available: true),
FortniteBuild(gameVersion: "4.4", link: "https://public.simplyblk.xyz/4.4.rar", available: true),
FortniteBuild(gameVersion: "4.5", link: "https://public.simplyblk.xyz/4.5.rar", available: true),
FortniteBuild(gameVersion: "5.00", link: "https://public.simplyblk.xyz/5.00.rar", available: true),
FortniteBuild(gameVersion: "5.0.1", link: "https://public.simplyblk.xyz/5.0.1.rar", available: true),
FortniteBuild(gameVersion: "5.10", link: "https://public.simplyblk.xyz/5.10.rar", available: true),
FortniteBuild(gameVersion: "5.21", link: "https://public.simplyblk.xyz/5.21.rar", available: true),
FortniteBuild(gameVersion: "5.30", link: "https://public.simplyblk.xyz/5.30.rar", available: true),
FortniteBuild(gameVersion: "5.40", link: "https://public.simplyblk.xyz/5.40.rar", available: true),
FortniteBuild(gameVersion: "6.00", link: "https://public.simplyblk.xyz/6.00.rar", available: true),
FortniteBuild(gameVersion: "6.01", link: "https://public.simplyblk.xyz/6.01.rar", available: true),
FortniteBuild(gameVersion: "6.1.1", link: "https://public.simplyblk.xyz/6.1.1.rar", available: true),
FortniteBuild(gameVersion: "6.02", link: "https://public.simplyblk.xyz/6.02.rar", available: true),
FortniteBuild(gameVersion: "6.2.1", link: "https://public.simplyblk.xyz/6.2.1.rar", available: true),
FortniteBuild(gameVersion: "6.10", link: "https://public.simplyblk.xyz/6.10.rar", available: true),
FortniteBuild(gameVersion: "6.10.1", link: "https://public.simplyblk.xyz/6.10.1.rar", available: true),
FortniteBuild(gameVersion: "6.10.2", link: "https://public.simplyblk.xyz/6.10.2.rar", available: true),
FortniteBuild(gameVersion: "6.21", link: "https://public.simplyblk.xyz/6.21.rar", available: true),
FortniteBuild(gameVersion: "6.22", link: "https://public.simplyblk.xyz/6.22.rar", available: true),
FortniteBuild(gameVersion: "6.30", link: "https://public.simplyblk.xyz/6.30.rar", available: true),
FortniteBuild(gameVersion: "6.31", link: "https://public.simplyblk.xyz/6.31.rar", available: true),
FortniteBuild(gameVersion: "7.00", link: "https://public.simplyblk.xyz/7.00.rar", available: true),
FortniteBuild(gameVersion: "7.10", link: "https://public.simplyblk.xyz/7.10.rar", available: true),
FortniteBuild(gameVersion: "7.20", link: "https://public.simplyblk.xyz/7.20.rar", available: true),
FortniteBuild(gameVersion: "7.30", link: "https://public.simplyblk.xyz/7.30.zip", available: true),
FortniteBuild(gameVersion: "7.40", link: "https://public.simplyblk.xyz/7.40.rar", available: true),
FortniteBuild(gameVersion: "8.00", link: "https://public.simplyblk.xyz/8.00.zip", available: true),
FortniteBuild(gameVersion: "8.20", link: "https://public.simplyblk.xyz/8.20.rar", available: true),
FortniteBuild(gameVersion: "8.30", link: "https://public.simplyblk.xyz/8.30.rar", available: true),
FortniteBuild(gameVersion: "8.40", link: "https://public.simplyblk.xyz/8.40.zip", available: true),
FortniteBuild(gameVersion: "8.50", link: "https://public.simplyblk.xyz/8.50.zip", available: true),
FortniteBuild(gameVersion: "8.51", link: "https://public.simplyblk.xyz/8.51.rar", available: true),
FortniteBuild(gameVersion: "9.00", link: "https://public.simplyblk.xyz/9.00.zip", available: true),
FortniteBuild(gameVersion: "9.01", link: "https://public.simplyblk.xyz/9.01.zip", available: true),
FortniteBuild(gameVersion: "9.10", link: "https://public.simplyblk.xyz/9.10.rar", available: true),
FortniteBuild(gameVersion: "9.21", link: "https://public.simplyblk.xyz/9.21.zip", available: true),
FortniteBuild(gameVersion: "9.30", link: "https://public.simplyblk.xyz/9.30.zip", available: true),
FortniteBuild(gameVersion: "9.40", link: "https://public.simplyblk.xyz/9.40.zip", available: true),
FortniteBuild(gameVersion: "9.41", link: "https://public.simplyblk.xyz/9.41.rar", available: true),
FortniteBuild(gameVersion: "10.00", link: "https://public.simplyblk.xyz/10.00.zip", available: true),
FortniteBuild(gameVersion: "10.10", link: "https://public.simplyblk.xyz/10.10.zip", available: true),
FortniteBuild(gameVersion: "10.20", link: "https://public.simplyblk.xyz/10.20.zip", available: true),
FortniteBuild(gameVersion: "10.31", link: "https://public.simplyblk.xyz/10.31.zip", available: true),
FortniteBuild(gameVersion: "10.40", link: "https://public.simplyblk.xyz/10.40.rar", available: true),
FortniteBuild(gameVersion: "11.00", link: "https://public.simplyblk.xyz/11.00.zip", available: true),
FortniteBuild(gameVersion: "11.31", link: "https://public.simplyblk.xyz/11.31.rar", available: true),
FortniteBuild(gameVersion: "12.00", link: "https://public.simplyblk.xyz/12.00.rar", available: true),
FortniteBuild(gameVersion: "12.21", link: "https://public.simplyblk.xyz/12.21.zip", available: true),
FortniteBuild(gameVersion: "12.50", link: "https://public.simplyblk.xyz/12.50.zip", available: true),
FortniteBuild(gameVersion: "12.61", link: "https://public.simplyblk.xyz/12.61.zip", available: true),
FortniteBuild(gameVersion: "13.00", link: "https://public.simplyblk.xyz/13.00.rar", available: true),
FortniteBuild(gameVersion: "13.40", link: "https://public.simplyblk.xyz/13.40.zip", available: true),
FortniteBuild(gameVersion: "14.00", link: "https://public.simplyblk.xyz/14.00.rar", available: true),
FortniteBuild(gameVersion: "14.40", link: "https://public.simplyblk.xyz/14.40.rar", available: true),
FortniteBuild(gameVersion: "14.60", link: "https://public.simplyblk.xyz/14.60.rar", available: true),
FortniteBuild(gameVersion: "15.30", link: "https://public.simplyblk.xyz/15.30.rar", available: true),
FortniteBuild(gameVersion: "16.40", link: "https://public.simplyblk.xyz/16.40.rar", available: true),
FortniteBuild(gameVersion: "17.30", link: "https://public.simplyblk.xyz/17.30.zip", available: true),
FortniteBuild(gameVersion: "17.50", link: "https://public.simplyblk.xyz/17.50.zip", available: true),
FortniteBuild(gameVersion: "18.40", link: "https://public.simplyblk.xyz/18.40.zip", available: true),
FortniteBuild(gameVersion: "19.10", link: "https://public.simplyblk.xyz/19.10.rar", available: true),
FortniteBuild(gameVersion: "20.40", link: "https://public.simplyblk.xyz/20.40.zip", available: true),
];
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
Timer? timer;
try {
final stopped = _setupLifecycle(options);
await outputFile.parent.create(recursive: true);
final downloadItemCompleter = Completer<File>();
await _startAriaServer();
final downloadId = await _startAriaDownload(options, outputFile);
timer = Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.tellStatus",
"params": [
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
}
final result = statusResponseJson["result"];
final files = result["files"] as List?;
if(files == null || files.isEmpty) {
downloadItemCompleter.completeError("Download aborted");
timer.cancel();
return;
}
final error = result["errorCode"];
if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
}
timer.cancel();
return;
}
final speed = int.parse(result["downloadSpeed"] ?? "0");
final completedLength = int.parse(files[0]["completedLength"] ?? "0");
final totalLength = int.parse(files[0]["length"] ?? "0");
final percentage = completedLength * 100 / totalLength;
final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
_onProgress(
options.port,
percentage,
speed,
minutesLeft,
false
);
}catch(error) {
throw "Invalid download status (${error})";
}
});
await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options);
}finally {
delete(outputFile);
timer?.cancel();
}
}
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe at ${aria2c.path}";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none",
"--check-certificate=false"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
Process? process;
switch (extension.toLowerCase()) {
case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) {
throw "Missing 7zip.exe";
}
process = await startProcess(
executable: sevenZip,
args: [
"x",
"-bsp1",
'-o"${options.destination.path}"',
"-y",
'"${tempFile.path}"'
],
);
var completed = false;
process.stdOutput.listen((data) {
if(data.toLowerCase().contains("everything is ok")) {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted zip archive", options);
}
});
break;
case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) {
throw "Missing winrar.exe";
}
process = await startProcess(
executable: winrar,
args: [
"x",
"-o+",
'"${tempFile.path}"',
"*.*",
'"${options.destination.path}"'
]
);
var completed = false;
process.stdOutput.listen((data) {
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") {
completed = true;
_onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt);
return;
}
final element = _rarProgressRegex.firstMatch(data)?.group(1);
if(element == null) {
return;
}
final percentage = int.parse(element).toDouble();
_onProgress(
options.port,
percentage,
0,
-1,
true
);
});
process.stdError.listen((data) {
if(!data.isBlank) {
_onError(data, options);
}
});
process.exitCode.then((_) {
if(!completed) {
_onError("Corrupted rar archive", options);
}
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
process.kill(ProcessSignal.sigabrt);
}
void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) {
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(FortniteBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, FortniteBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
lifecyclePort.close();
stopped.complete();
}
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
Future<bool> downloadDependency(InjectableDll dll, String outputPath) async {
String? name;
switch(dll) {
case InjectableDll.console:
name = "console.dll";
case InjectableDll.auth:
name = "cobalt.dll";
case InjectableDll.memoryLeak:
name = "memory.dll";
case InjectableDll.gameServer:
name = null;
}
if(name == null) {
return false;
}
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}");
}
final output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes, flush: true);
try {
await output.readAsBytes();
return true;
}catch(_) {
return false;
}
}
Future<void> downloadRebootDll(File file, String url, bool aboveS20) async {
Directory? outputDir;
try {
var response = await http.get(Uri.parse(url));
if(response.statusCode != 200) {
response = await http.get(Uri.parse(aboveS20 ? _kRebootAboveS20FallbackDownloadUrl : _kRebootBelowS20FallbackDownloadUrl));
if(response.statusCode != 200) {
throw Exception("status code ${response.statusCode}");
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

@@ -0,0 +1,276 @@
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
final DynamicLibrary _shell32 = DynamicLibrary.open('shell32.dll');
final SHGetPropertyStoreFromParsingName =
_shell32.lookupFunction<
Int32 Function(Pointer<Utf16> pszPath, Pointer<Void> pbc, Uint32 flags,
Pointer<GUID> riid, Pointer<Pointer<COMObject>> ppv),
int Function(Pointer<Utf16> pszPath, Pointer<Void> pbc, int flags,
Pointer<GUID> riid, Pointer<Pointer<COMObject>> ppv)>('SHGetPropertyStoreFromParsingName');
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
// Not used right now
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
Future<bool> patchHeadless(File file) async =>
await _patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async => Isolate.run(() async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
final source = await file.readAsBytes();
var readOffset = 0;
var patchOffset = -1;
var patchCount = 0;
while(readOffset < source.length){
if(source[readOffset] == original[patchCount]){
if(patchOffset == -1) {
patchOffset = readOffset;
}
if(readOffset - patchOffset + 1 == original.length) {
break;
}
patchCount++;
}else {
patchOffset = -1;
patchCount = 0;
}
readOffset++;
}
if(patchOffset == -1) {
return false;
}
for(var i = 0; i < patched.length; i++) {
source[patchOffset + i] = patched[i];
}
await file.writeAsBytes(source, flush: true);
return true;
}catch(_){
return false;
}
});
List<String> createRebootArgs(String username, String password, bool host, bool headless, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args");
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = LinkedHashMap<String, String>(
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
hashCode: (a) => a.toUpperCase().hashCode
);
args.addAll({
"-epicapp": "Fortnite",
"-epicenv": "Prod",
"-epiclocale": "en-us",
"-epicportal": "",
"-skippatchcheck": "",
"-nobe": "",
"-fromfl": "eac",
"-fltoken": "3db3ba5dcbd2e16703f3978d",
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(logging) {
args["-log"] = "";
}
if(host) {
args["-nosplash"] = "";
args["-nosound"] = "";
if(headless){
args["-nullrhi"] = "";
}
}
log("[PROCESS] Default args: $args");
log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
}
log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
}
void handleGameOutput({
required String line,
required bool host,
required void Function() onLoggedIn,
required void Function() onMatchEnd,
required void Function() onShutdown,
required void Function() onTokenError,
required void Function() onBuildCorrupted,
}) {
if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd();
}
}
String _parseUsername(String username, bool host) {
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
Future<String> extractGameVersion(String filePath, String defaultGameVersion) => Isolate.run(() {
final filePathPtr = filePath.toNativeUtf16();
final pPropertyStore = calloc<COMObject>();
final iidPropertyStore = GUIDFromString(IID_IPropertyStore);
final ret = SHGetPropertyStoreFromParsingName(
filePathPtr,
nullptr,
GETPROPERTYSTOREFLAGS.GPS_DEFAULT,
iidPropertyStore,
pPropertyStore.cast()
);
calloc.free(filePathPtr);
calloc.free(iidPropertyStore);
if (FAILED(ret)) {
calloc.free(pPropertyStore);
throw WindowsException(ret);
}
final propertyStore = IPropertyStore(pPropertyStore);
final countPtr = calloc<Uint32>();
final hrCount = propertyStore.getCount(countPtr);
final count = countPtr.value;
calloc.free(countPtr);
if (FAILED(hrCount)) {
throw WindowsException(hrCount);
}
for (var i = 0; i < count; i++) {
final pKey = calloc<PROPERTYKEY>();
final hrKey = propertyStore.getAt(i, pKey);
if (FAILED(hrKey)) {
calloc.free(pKey);
continue;
}
final pv = calloc<PROPVARIANT>();
final hrValue = propertyStore.getValue(pKey, pv);
if (!FAILED(hrValue)) {
if (pv.ref.vt == VARENUM.VT_LPWSTR) {
final valueStr = pv.ref.pwszVal.toDartString();
if (valueStr.contains("+++Fortnite")) {
var gameVersion = valueStr.substring(valueStr.lastIndexOf("-") + 1);
if(gameVersion == "Cert") {
final engineVersion = valueStr.substring(0, valueStr.indexOf("+"));
final engineVersionParts = engineVersion.split("-");
final engineVersionBuild = int.parse(engineVersionParts[1]);
switch (engineVersionBuild) {
case 2870186:
gameVersion = "OT6.5";
break;
case 3700114:
gameVersion = "1.7.2";
break;
case 3724489:
gameVersion = "1.8.0";
break;
case 3729133:
gameVersion = "1.8.1";
break;
case 3741772:
gameVersion = "1.8.2";
break;
case 3757339:
gameVersion = "1.9";
break;
case 3775276:
gameVersion = "1.9.1";
break;
case 3790078:
gameVersion = "1.10";
break;
case 3807424:
gameVersion = "1.11";
break;
case 3825894:
gameVersion = "2.1";
break;
default:
gameVersion = defaultGameVersion;
break;
}
}
return gameVersion;
}
}
}
calloc.free(pKey);
calloc.free(pv);
}
return defaultGameVersion;
});

View File

@@ -5,6 +5,7 @@ import 'package:sync/semaphore.dart';
final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1);
bool enableLoggingToConsole = true;
File _createLoggingFile() {
final file = File("${installationDirectory.path}\\launcher.log");
@@ -19,7 +20,9 @@ File _createLoggingFile() {
void log(String message) async {
try {
await _semaphore.acquire();
print(message);
if(enableLoggingToConsole) {
print(message);
}
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
}catch(error) {
print("[LOGGER_ERROR] An error occurred while logging: $error");

View File

@@ -1,96 +0,0 @@
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
const _AF_INET = 2;
const _TCP_TABLE_OWNER_PID_LISTENER = 3;
final _getExtendedTcpTable = DynamicLibrary.open('iphlpapi.dll').lookupFunction<
Int32 Function(Pointer, Pointer<Uint32>, Int32, Int32, Int32, Int32),
int Function(Pointer, Pointer<Uint32>, int, int, int, int)>('GetExtendedTcpTable');
final class _MIB_TCPROW_OWNER_PID extends Struct {
@Uint32()
external int dwState;
@Uint32()
external int dwLocalAddr;
@Uint32()
external int dwLocalPort;
@Uint32()
external int dwRemoteAddr;
@Uint32()
external int dwRemotePort;
@Uint32()
external int dwOwningPid;
}
final class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32()
external int dwNumEntries;
@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";
bool killProcessByPort(int port) {
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
final dwSize = calloc<DWORD>();
dwSize.value = 0;
int result = _getExtendedTcpTable(
nullptr,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
if (result == ERROR_INSUFFICIENT_BUFFER) {
free(pTcpTable);
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
result = _getExtendedTcpTable(
pTcpTable,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
}
if (result == NO_ERROR) {
final table = pTcpTable.ref;
for (int i = 0; i < table.dwNumEntries; i++) {
final row = table.table[i];
final localPort = _htons(row.dwLocalPort);
if (localPort == port) {
final pid = row.dwOwningPid;
calloc.free(pTcpTable);
calloc.free(dwSize);
final hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL) {
final result = TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
return result != 0;
}
return false;
}
}
}
calloc.free(pTcpTable);
calloc.free(dwSize);
return false;
}
int _htons(int port) => ((port & 0xFF) << 8) | ((port >> 8) & 0xFF);

468
common/lib/src/util/os.dart Normal file
View File

@@ -0,0 +1,468 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
import 'package:path/path.dart' as path;
bool useDefaultPath = false;
Directory get installationDirectory {
if(useDefaultPath) {
final dir = Directory('$_home/Reboot Launcher');
dir.createSync(recursive: true);
return dir;
}else {
return File(Platform.resolvedExecutable).parent;
}
}
String get _home {
if (Platform.isMacOS) {
return Platform.environment['HOME'] ?? '.';
} else if (Platform.isLinux) {
return Platform.environment['HOME'] ?? '.';
} else if (Platform.isWindows) {
return Platform.environment['UserProfile'] ?? '.';
}else {
return '.';
}
}
String? get antiVirusName {
final pLoc = calloc<COMObject>();
final rclsid = GUIDFromString(CLSID_WbemLocator);
final riid = GUIDFromString(IID_IWbemLocator);
final hr = CoCreateInstance(
rclsid,
nullptr,
CLSCTX.CLSCTX_INPROC_SERVER,
riid,
pLoc.cast(),
);
calloc.free(rclsid);
calloc.free(riid);
if (FAILED(hr)) {
return null;
}
final locator = IWbemLocator(pLoc);
final pSvc = calloc<COMObject>();
final scope = 'ROOT\\SecurityCenter2'.toNativeUtf16();
final hr2 = locator.connectServer(
scope,
nullptr,
nullptr,
nullptr,
0,
nullptr,
nullptr,
pSvc.cast()
);
calloc.free(scope);
if (FAILED(hr2)) {
return null;
}
final service = IWbemServices(pSvc);
final pEnumerator = calloc<COMObject>();
final wql = 'WQL'.toNativeUtf16();
final query = 'SELECT * FROM AntiVirusProduct'.toNativeUtf16();
final hr3 = service.execQuery(
wql,
query,
WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_FORWARD_ONLY | WBEM_GENERIC_FLAG_TYPE.WBEM_FLAG_RETURN_IMMEDIATELY,
nullptr,
pEnumerator.cast(),
);
calloc.free(wql);
calloc.free(query);
if (FAILED(hr3)) {
return null;
}
final enumerator = IEnumWbemClassObject(pEnumerator);
final uReturn = calloc<Uint32>();
final pClsObj = calloc<COMObject>();
final hr4 = enumerator.next(
WBEM_INFINITE,
1,
pClsObj.cast(),
uReturn,
);
String? result;
if (SUCCEEDED(hr4) && uReturn.value > 0) {
final clsObj = IWbemClassObject(pClsObj);
final vtProp = calloc<VARIANT>();
final propName = 'displayName'.toNativeUtf16();
final hr5 = clsObj.get(
propName,
0,
vtProp,
nullptr,
nullptr,
);
calloc.free(propName);
if (SUCCEEDED(hr5) && vtProp.ref.vt == VARENUM.VT_BSTR) {
final bstr = vtProp.ref.bstrVal;
result = bstr.toDartString();
}
calloc.free(vtProp);
}
calloc.free(uReturn);
return result;
}
String get defaultAntiVirusName => "Windows Defender";
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory {
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}
return installationDirectory;
}
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");
Directory get tempDirectory =>
Directory(Platform.environment["Temp"]!);
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
}
const _AF_INET = 2;
const _TCP_TABLE_OWNER_PID_LISTENER = 3;
final _getExtendedTcpTable = DynamicLibrary.open('iphlpapi.dll').lookupFunction<
Int32 Function(Pointer, Pointer<Uint32>, Int32, Int32, Int32, Int32),
int Function(Pointer, Pointer<Uint32>, int, int, int, int)>('GetExtendedTcpTable');
final class _MIB_TCPROW_OWNER_PID extends Struct {
@Uint32()
external int dwState;
@Uint32()
external int dwLocalAddr;
@Uint32()
external int dwLocalPort;
@Uint32()
external int dwRemoteAddr;
@Uint32()
external int dwRemotePort;
@Uint32()
external int dwOwningPid;
}
final class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32()
external int dwNumEntries;
@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";
bool killProcessByPort(int port) {
var pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>();
final dwSize = calloc<DWORD>();
dwSize.value = 0;
int result = _getExtendedTcpTable(
nullptr,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
if (result == WIN32_ERROR.ERROR_INSUFFICIENT_BUFFER) {
calloc.free(pTcpTable);
pTcpTable = calloc<_MIB_TCPTABLE_OWNER_PID>(dwSize.value);
result = _getExtendedTcpTable(
pTcpTable,
dwSize,
FALSE,
_AF_INET,
_TCP_TABLE_OWNER_PID_LISTENER,
0
);
}
if (result == NO_ERROR) {
final table = pTcpTable.ref;
for (int i = 0; i < table.dwNumEntries; i++) {
final row = table.table[i];
final localPort = _htons(row.dwLocalPort);
if (localPort == port) {
final pid = row.dwOwningPid;
calloc.free(pTcpTable);
calloc.free(dwSize);
final hProcess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL) {
final result = TerminateProcess(hProcess, 0);
CloseHandle(hProcess);
return result != 0;
}
return false;
}
}
}
calloc.free(pTcpTable);
calloc.free(dwSize);
return false;
}
int _htons(int port) => ((port & 0xFF) << 8) | ((port >> 8) & 0xFF);
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
Future<void> injectDll(int pid, File file) async {
try {
await file.readAsBytes();
}catch(_) {
throw "${path.basename(file.path)} is not accessible";
}
final process = OpenProcess(0x43A, FALSE, pid);
final processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw "Cannot get process address for pid $pid";
}
final dllAddress = VirtualAllocEx(
process,
nullptr,
file.path.length + 1,
0x3000,
0x4
);
if(dllAddress == 0) {
throw "Cannot allocate memory for dll";
}
final writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
file.path.toNativeUtf8(),
file.path.length,
nullptr
);
if (writeMemoryResult != 1) {
throw "Memory write failed";
}
final createThreadResult = _CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw "Thread creation failed";
}
CloseHandle(process);
}
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 ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
return ShellExecuteEx(shellInput) == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start(
tempScriptFile.path,
[],
workingDirectory: workingDirectory,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
String? _getWorkingDirectory(File executable) {
try {
log("[PROCESS] Calculating working directory for $executable");
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
log("[PROCESS] Using working directory: $workingDirectory");
return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
}
}
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
final class _ExtendedProcess implements Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _delegate.exitCode;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
@override
int get pid => _delegate.pid;
@override
IOSink get stdin => _delegate.stdin;
@override
Stream<List<int>> get stdout {
final out = _stdout;
if(out == null) {
throw StateError("Output is not attached");
}
return out;
}
@override
Stream<List<int>> get stderr {
final err = _stderr;
if(err == null) {
throw StateError("Output is not attached");
}
return err;
}
}
Future<List<File>> findFiles(Directory directory, String name) => Isolate.run(() => directory.list(recursive: true, followLinks: true)
.handleError((_) {})
.where((event) => event is File && path.basename(event.path) == name)
.map((event) => event as File)
.toList());

View File

@@ -1,69 +0,0 @@
import 'dart:io';
import 'dart:typed_data';
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
// Not used right now
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
Future<bool> patchHeadless(File file) async =>
await _patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
final source = await file.readAsBytes();
var readOffset = 0;
var patchOffset = -1;
var patchCount = 0;
while(readOffset < source.length){
if(source[readOffset] == original[patchCount]){
if(patchOffset == -1) {
patchOffset = readOffset;
}
if(readOffset - patchOffset + 1 == original.length) {
break;
}
patchCount++;
}else {
patchOffset = -1;
patchCount = 0;
}
readOffset++;
}
if(patchOffset == -1) {
return false;
}
for(var i = 0; i < patched.length; i++) {
source[patchOffset + i] = patched[i];
}
await file.writeAsBytes(source, flush: true);
return true;
}catch(_){
return false;
}
}

View File

@@ -1,37 +0,0 @@
import 'dart:io';
Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent;
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory {
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}
return installationDirectory;
}
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");
Directory get tempDirectory =>
Directory(Platform.environment["Temp"]!);
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
}

View File

@@ -1,312 +0,0 @@
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:collection';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.dart';
import 'package:reboot_common/common.dart';
import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
const chunkSize = 1024;
Future<void> injectDll(int pid, File dll) async {
// Get the path to the file
final dllPath = dll.path;
final process = OpenProcess(
0x43A,
0,
pid
);
final processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw Exception("Cannot get process address for pid $pid");
}
final dllAddress = VirtualAllocEx(
process,
nullptr,
dllPath.length + 1,
0x3000,
0x4
);
final writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
dllPath.toNativeUtf8(),
dllPath.length,
nullptr
);
if (writeMemoryResult != 1) {
throw Exception("Memory write failed");
}
final createThreadResult = _CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw Exception("Thread creation failed");
}
final closeResult = CloseHandle(process);
if(closeResult != 1){
throw Exception("Cannot close handle");
}
}
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 ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
return ShellExecuteEx(shellInput) == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start(
tempScriptFile.path,
[],
workingDirectory: workingDirectory,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
final process = await Process.start(
executable.path,
args ?? [],
workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
return _ExtendedProcess(process, true);
}
String? _getWorkingDirectory(File executable) {
try {
log("[PROCESS] Calculating working directory for $executable");
final workingDirectory = executable.parent.resolveSymbolicLinksSync();
log("[PROCESS] Using working directory: $workingDirectory");
return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
}
}
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle);
}
}
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
log("[PROCESS] Generating reboot args");
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
final args = LinkedHashMap<String, String>(
equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
hashCode: (a) => a.toUpperCase().hashCode
);
args.addAll({
"-epicapp": "Fortnite",
"-epicenv": "Prod",
"-epiclocale": "en-us",
"-epicportal": "",
"-skippatchcheck": "",
"-nobe": "",
"-fromfl": "eac",
"-fltoken": "3db3ba5dcbd2e16703f3978d",
"-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(logging) {
args["-log"] = "";
}
if(host) {
args["-nosplash"] = "";
args["-nosound"] = "";
if(hostType == GameServerType.headless){
args["-nullrhi"] = "";
}
}
log("[PROCESS] Default args: $args");
log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
}
log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
}
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)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd();
}else if(line.contains(kDisplayLine) && line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached();
}
}
String _parseUsername(String username, bool host) {
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
final class _ExtendedProcess implements Process {
final Process _delegate;
final Stream<List<int>>? _stdout;
final Stream<List<int>>? _stderr;
_ExtendedProcess(Process delegate, bool attached) :
_delegate = delegate,
_stdout = attached ? delegate.stdout.asBroadcastStream() : null,
_stderr = attached ? delegate.stderr.asBroadcastStream() : null;
@override
Future<int> get exitCode => _delegate.exitCode;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);
@override
int get pid => _delegate.pid;
@override
IOSink get stdin => _delegate.stdin;
@override
Stream<List<int>> get stdout {
final out = _stdout;
if(out == null) {
throw StateError("Output is not attached");
}
return out;
}
@override
Stream<List<int>> get stderr {
final err = _stderr;
if(err == null) {
throw StateError("Output is not attached");
}
return err;
}
}

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.',