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,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;
}
}