Refactored GUI

This commit is contained in:
Alessandro Autiero
2025-08-10 19:43:57 +01:00
parent 52abf5eb95
commit 4ea73d17c7
75 changed files with 2020 additions and 2011 deletions

View File

@@ -0,0 +1,64 @@
import 'dart:io';
import 'dart:isolate';
class GameBuild {
final String gameVersion;
final String link;
final bool available;
GameBuild({
required this.gameVersion,
required this.link,
required this.available
});
}
class GameBuildDownloadProgress {
final double progress;
final int? timeLeft;
final bool extracting;
final int speed;
GameBuildDownloadProgress({
required this.progress,
required this.extracting,
required this.timeLeft,
required this.speed
});
}
class GameBuildDownloadOptions {
GameBuild build;
Directory destination;
SendPort port;
GameBuildDownloadOptions(this.build, this.destination, this.port);
}
class GameBuildManifestChunk {
List<int> chunksIds;
String file;
int fileSize;
GameBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize);
factory GameBuildManifestChunk.fromJson(json) => GameBuildManifestChunk._internal(
List<int>.from(json["ChunksIds"] as List),
json["File"],
json["FileSize"]
);
}
class GameBuildManifestFile {
String name;
List<GameBuildManifestChunk> chunks;
int size;
GameBuildManifestFile._internal(this.name, this.chunks, this.size);
factory GameBuildManifestFile.fromJson(json) => GameBuildManifestFile._internal(
json["Name"],
List<GameBuildManifestChunk>.from(json["Chunks"].map((chunk) => GameBuildManifestChunk.fromJson(chunk))),
json["Size"]
);
}

View File

@@ -0,0 +1,34 @@
import 'package:version/version.dart';
const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String kInitializedLine = "Game Engine Initialized";
const List<String> kLoggedInLines = [
"[UOnlineAccountCommon::ContinueLoggingIn]",
"(Completed)"
];
const String kShutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> kCorruptedBuildErrors = [
"Critical error",
"when 0 bytes remain",
"Pak chunk signature verification failed!",
"LogWindows:Error: Fatal error!"
];
const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused",
"Unable to login to Fortnite servers",
"HTTP 400 response from ",
"Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout"
];
const String kGameFinishedLine = "TeamsLeft: 1";
const String kDisplayLine = "Display";
const String kDisplayInitializedLine = "Initialized";
const String kShippingExe = "FortniteClient-Win64-Shipping.exe";
const String kLauncherExe = "FortniteLauncher.exe";
const String kEacExe = "FortniteClient-Win64-Shipping_EAC.exe";
const String kCrashReportExe = "CrashReportClient.exe";
const String kGFSDKAftermathLibDll = "GFSDK_Aftermath_Lib.dll";
final Version kMaxAllowedVersion = Version.parse("30.10");

View File

@@ -0,0 +1,10 @@
enum GameDll {
console,
auth,
gameServer,
memoryLeak
}
extension InjectableDllVersionAware on GameDll {
bool get isVersionDependent => this == GameDll.gameServer;
}

View File

@@ -0,0 +1,559 @@
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: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<GameBuild> downloadableBuilds = [
GameBuild(gameVersion: "1.7.2", link: "https://builds.rebootfn.org/1.7.2.zip", available: true),
GameBuild(gameVersion: "1.8", link: "https://builds.rebootfn.org/1.8.rar", available: true),
GameBuild(gameVersion: "1.8.1", link: "https://builds.rebootfn.org/1.8.1.rar", available: true),
GameBuild(gameVersion: "1.8.2", link: "https://builds.rebootfn.org/1.8.2.rar", available: true),
GameBuild(gameVersion: "1.9", link: "https://builds.rebootfn.org/1.9.rar", available: true),
GameBuild(gameVersion: "1.9.1", link: "https://builds.rebootfn.org/1.9.1.rar", available: true),
GameBuild(gameVersion: "1.10", link: "https://builds.rebootfn.org/1.10.rar", available: true),
GameBuild(gameVersion: "1.11", link: "https://builds.rebootfn.org/1.11.zip", available: true),
GameBuild(gameVersion: "2.1.0", link: "https://builds.rebootfn.org/2.1.0.zip", available: true),
GameBuild(gameVersion: "2.2.0", link: "https://builds.rebootfn.org/2.2.0.rar", available: true),
GameBuild(gameVersion: "2.3", link: "https://builds.rebootfn.org/2.3.rar", available: true),
GameBuild(gameVersion: "2.4.0", link: "https://builds.rebootfn.org/2.4.0.zip", available: true),
GameBuild(gameVersion: "2.4.2", link: "https://builds.rebootfn.org/2.4.2.zip", available: true),
GameBuild(gameVersion: "2.5.0", link: "https://builds.rebootfn.org/2.5.0.rar", available: true),
GameBuild(gameVersion: "3.0", link: "https://builds.rebootfn.org/3.0.zip", available: true),
GameBuild(gameVersion: "3.1", link: "https://builds.rebootfn.org/3.1.rar", available: true),
GameBuild(gameVersion: "3.1.1", link: "https://builds.rebootfn.org/3.1.1.zip", available: true),
GameBuild(gameVersion: "3.2", link: "https://builds.rebootfn.org/3.2.zip", available: true),
GameBuild(gameVersion: "3.3", link: "https://builds.rebootfn.org/3.3.rar", available: true),
GameBuild(gameVersion: "3.5", link: "https://builds.rebootfn.org/3.5.rar", available: true),
GameBuild(gameVersion: "3.6", link: "https://builds.rebootfn.org/3.6.zip", available: true),
GameBuild(gameVersion: "4.0", link: "https://builds.rebootfn.org/4.0.zip", available: true),
GameBuild(gameVersion: "4.1", link: "https://builds.rebootfn.org/4.1.zip", available: true),
GameBuild(gameVersion: "4.2", link: "https://builds.rebootfn.org/4.2.zip", available: true),
GameBuild(gameVersion: "4.4", link: "https://builds.rebootfn.org/4.4.rar", available: true),
GameBuild(gameVersion: "4.5", link: "https://builds.rebootfn.org/4.5.rar", available: true),
GameBuild(gameVersion: "5.00", link: "https://builds.rebootfn.org/5.00.rar", available: true),
GameBuild(gameVersion: "5.0.1", link: "https://builds.rebootfn.org/5.0.1.rar", available: true),
GameBuild(gameVersion: "5.10", link: "https://builds.rebootfn.org/5.10.rar", available: true),
GameBuild(gameVersion: "5.21", link: "https://builds.rebootfn.org/5.21.rar", available: true),
GameBuild(gameVersion: "5.30", link: "https://builds.rebootfn.org/5.30.rar", available: true),
GameBuild(gameVersion: "5.40", link: "https://builds.rebootfn.org/5.40.rar", available: true),
GameBuild(gameVersion: "6.00", link: "https://builds.rebootfn.org/6.00.rar", available: true),
GameBuild(gameVersion: "6.01", link: "https://builds.rebootfn.org/6.01.rar", available: true),
GameBuild(gameVersion: "6.1.1", link: "https://builds.rebootfn.org/6.1.1.rar", available: true),
GameBuild(gameVersion: "6.02", link: "https://builds.rebootfn.org/6.02.rar", available: true),
GameBuild(gameVersion: "6.2.1", link: "https://builds.rebootfn.org/6.2.1.rar", available: true),
GameBuild(gameVersion: "6.10", link: "https://builds.rebootfn.org/6.10.rar", available: true),
GameBuild(gameVersion: "6.10.1", link: "https://builds.rebootfn.org/6.10.1.rar", available: true),
GameBuild(gameVersion: "6.10.2", link: "https://builds.rebootfn.org/6.10.2.rar", available: true),
GameBuild(gameVersion: "6.21", link: "https://builds.rebootfn.org/6.21.rar", available: true),
GameBuild(gameVersion: "6.22", link: "https://builds.rebootfn.org/6.22.rar", available: true),
GameBuild(gameVersion: "6.30", link: "https://builds.rebootfn.org/6.30.rar", available: true),
GameBuild(gameVersion: "6.31", link: "https://builds.rebootfn.org/6.31.rar", available: true),
GameBuild(gameVersion: "7.00", link: "https://builds.rebootfn.org/7.00.rar", available: true),
GameBuild(gameVersion: "7.10", link: "https://builds.rebootfn.org/7.10.rar", available: true),
GameBuild(gameVersion: "7.20", link: "https://builds.rebootfn.org/7.20.rar", available: true),
GameBuild(gameVersion: "7.30", link: "https://builds.rebootfn.org/7.30.zip", available: true),
GameBuild(gameVersion: "7.40", link: "https://builds.rebootfn.org/7.40.rar", available: true),
GameBuild(gameVersion: "8.00", link: "https://builds.rebootfn.org/8.00.zip", available: true),
GameBuild(gameVersion: "8.20", link: "https://builds.rebootfn.org/8.20.rar", available: true),
GameBuild(gameVersion: "8.30", link: "https://builds.rebootfn.org/8.30.rar", available: true),
GameBuild(gameVersion: "8.40", link: "https://builds.rebootfn.org/8.40.zip", available: true),
GameBuild(gameVersion: "8.50", link: "https://builds.rebootfn.org/8.50.zip", available: true),
GameBuild(gameVersion: "8.51", link: "https://builds.rebootfn.org/8.51.rar", available: true),
GameBuild(gameVersion: "9.00", link: "https://builds.rebootfn.org/9.00.zip", available: true),
GameBuild(gameVersion: "9.01", link: "https://builds.rebootfn.org/9.01.zip", available: true),
GameBuild(gameVersion: "9.10", link: "https://builds.rebootfn.org/9.10.rar", available: true),
GameBuild(gameVersion: "9.21", link: "https://builds.rebootfn.org/9.21.zip", available: true),
GameBuild(gameVersion: "9.30", link: "https://builds.rebootfn.org/9.30.zip", available: true),
GameBuild(gameVersion: "9.40", link: "https://builds.rebootfn.org/9.40.zip", available: true),
GameBuild(gameVersion: "9.41", link: "https://builds.rebootfn.org/9.41.rar", available: true),
GameBuild(gameVersion: "10.00", link: "https://builds.rebootfn.org/10.00.zip", available: true),
GameBuild(gameVersion: "10.10", link: "https://builds.rebootfn.org/10.10.zip", available: true),
GameBuild(gameVersion: "10.20", link: "https://builds.rebootfn.org/10.20.zip", available: true),
GameBuild(gameVersion: "10.31", link: "https://builds.rebootfn.org/10.31.zip", available: true),
GameBuild(gameVersion: "10.40", link: "https://builds.rebootfn.org/10.40.rar", available: false),
GameBuild(gameVersion: "11.00", link: "https://builds.rebootfn.org/11.00.zip", available: false),
GameBuild(gameVersion: "11.31", link: "https://builds.rebootfn.org/11.31.rar", available: false),
GameBuild(gameVersion: "12.00", link: "https://builds.rebootfn.org/12.00.rar", available: false),
GameBuild(gameVersion: "12.21", link: "https://builds.rebootfn.org/12.21.zip", available: false),
GameBuild(gameVersion: "Fortnite 12.41", link: "https://builds.rebootfn.org/Fortnite%2012.41.zip", available: false),
GameBuild(gameVersion: "12.50", link: "https://builds.rebootfn.org/12.50.zip", available: false),
GameBuild(gameVersion: "12.61", link: "https://builds.rebootfn.org/12.61.zip", available: false),
GameBuild(gameVersion: "13.00", link: "https://builds.rebootfn.org/13.00.rar", available: false),
GameBuild(gameVersion: "13.40", link: "https://builds.rebootfn.org/13.40.zip", available: false),
GameBuild(gameVersion: "14.00", link: "https://builds.rebootfn.org/14.00.rar", available: false),
GameBuild(gameVersion: "14.40", link: "https://builds.rebootfn.org/14.40.rar", available: false),
GameBuild(gameVersion: "14.60", link: "https://builds.rebootfn.org/14.60.rar", available: false),
GameBuild(gameVersion: "15.30", link: "https://builds.rebootfn.org/15.30.rar", available: false),
GameBuild(gameVersion: "16.40", link: "https://builds.rebootfn.org/16.40.rar", available: false),
GameBuild(gameVersion: "17.30", link: "https://builds.rebootfn.org/17.30.zip", available: false),
GameBuild(gameVersion: "17.50", link: "https://builds.rebootfn.org/17.50.zip", available: false),
GameBuild(gameVersion: "18.40", link: "https://builds.rebootfn.org/18.40.zip", available: false),
GameBuild(gameVersion: "19.10", link: "https://builds.rebootfn.org/19.10.rar", available: false),
GameBuild(gameVersion: "20.40", link: "https://builds.rebootfn.org/20.40.zip", available: false)
];
Future<void> downloadArchiveBuild(GameBuildDownloadOptions 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(GameBuildDownloadOptions 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, GameBuildDownloadOptions 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.isBlankOrEmpty) {
_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.isBlankOrEmpty) {
_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(GameBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: null,
speed: speed
));
return;
}
port.send(GameBuildDownloadProgress(
progress: percentage,
extracting: extracting,
timeLeft: minutesLeft,
speed: speed
));
}
void _onError(Object? error, GameBuildDownloadOptions options) {
if(error != null) {
options.port.send(error.toString());
}
}
Completer<dynamic> _setupLifecycle(GameBuildDownloadOptions 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(GameDll dll, String outputPath) async {
String? name;
switch(dll) {
case GameDll.console:
name = "console.dll";
case GameDll.auth:
name = "cobalt.dll";
case GameDll.memoryLeak:
name = "memory.dll";
case GameDll.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<bool> 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 "status code ${response.statusCode}";
}
}
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
try {
await tempZip.writeAsBytes(response.bodyBytes, flush: true); // Write reboot.zip to disk
await tempZip.readAsBytes(); // Check implicitly if antivirus doesn't like reboot
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = outputDir.listSync()
.firstWhere((element) => path.extension(element.path) == ".dll") as File;
final rebootDllSource = await rebootDll.readAsBytes();
await file.writeAsBytes(rebootDllSource, flush: true);
await file.readAsBytes(); // Check implicitly if antivirus doesn't like reboot
return true;
} catch(_) {
return false; // Anti virus probably flagged reboot
}
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

View File

@@ -0,0 +1,50 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
class GameInstance {
final String version;
final bool host;
final int gamePid;
final int? launcherPid;
final int? eacPid;
final List<GameDll> injectedDlls;
final bool headless;
bool launched;
bool tokenError;
bool killed;
GameInstance? child;
GameInstance({
required this.version,
required this.host,
required this.gamePid,
required this.launcherPid,
required this.eacPid,
required this.headless,
required this.child
}): tokenError = false, killed = false, launched = false, injectedDlls = [];
void kill() {
GameInstance? child = this;
while(child != null) {
child._kill();
child = child.child;
}
}
void _kill() {
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);
}
}
}
}

View File

@@ -0,0 +1,383 @@
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';
import 'package:path/path.dart' as path;
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
]);
// https://github.com/polynite/fn-releases
const Map<int, String> _buildToGameVersion = {
2870186: "1.0.0",
3700114: "1.7.2",
3724489: "1.8.0",
3729133: "1.8.1",
3741772: "1.8.2",
3757339: "1.9",
3775276: "1.9.1",
3790078: "1.10",
3807424: "1.11",
3825894: "2.1",
3841827: "2.2",
3847564: "2.3",
3858292: "2.4",
3870737: "2.4.2",
3889387: "2.5",
3901517: "3.0.0",
3915963: "3.1",
3917250: "3.1.1",
3935073: "3.2",
3942182: "3.3",
4008490: "3.5",
4019403: "3.6",
4039451: "4.0",
4053532: "4.1",
4072250: "4.2",
4117433: "4.4",
4127312: "4.4.1",
4159770: "4.5",
4204761: "5.0",
4214610: "5.01",
4240749: "5.10",
4288479: "5.21",
4305896: "5.30",
4352937: "5.40",
4363240: "5.41",
4395664: "6.0",
4424678: "6.01",
4461277: "6.0.2",
4464155: "6.10",
4476098: "6.10.1",
4480234: "6.10.2",
4526925: "6.21",
4543176: "6.22",
4573279: "6.31",
4629139: "7.0",
4667333: "7.10",
4727874: "7.20",
4834550: "7.30",
5046157: "7.40",
5203069: "8.00",
5625478: "8.20",
5793395: "8.30",
6005771: "8.40",
6058028: "8.50",
6165369: "8.51",
6337466: "9.00",
6428087: "9.01",
6639283: "9.10",
6922310: "9.21",
7095426: "9.30",
7315705: "9.40",
7609292: "9.41",
7704164: "10.00",
7955722: "10.10",
8456527: "10.20",
8723043: "10.31",
9380822: "10.40",
9603448: "11.00",
9901083: "11.10",
10708866: "11.30",
10800459: "11.31",
11265652: "11.50",
11556442: "12.00",
11883027: "12.10",
12353830: "12.21",
12905909: "12.41",
13137020: "12.50",
13498980: "12.61",
14113327: "13.40",
14211474: "14.00",
14456520: "14.30",
14550713: "14.40",
14786821: "14.60",
14835335: "15.00",
15014719: "15.10",
15341163: "15.30",
15526472: "15.50",
15913292: "16.10",
16163563: "16.30",
16218553: "16.40",
16469788: "16.50",
16745144: "17.10",
17004569: "17.30",
17269705: "17.40",
17388565: "17.50",
17468642: "18.00",
17661844: "18.10",
17745267: "18.20",
17811397: "18.21",
17882303: "18.30",
18163738: "18.40",
18489740: "19.01",
18675304: "19.10",
19458861: "20.00",
19598943: "20.10",
19751212: "20.20",
19950687: "20.30",
20244966: "20.40",
20463113: "21.00",
20696680: "21.10",
21035704: "21.20",
21657658: "21.50",
};
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;
}
// Parsing the version is not that easy
// Also on some versions the shipping exe has it as well, but not on all: that's why i'm using the crash report client
// ++Fortnite+Release-34.10-CL-40567068
// 4.16.0-3700114+++Fortnite+Release-Cert
// 4.19.0-3870737+++Fortnite+Release-Next
// 4.20.0-4008490+++Fortnite+Release-3.5
Future<String> extractGameVersion(Directory directory) => Isolate.run(() async {
log("[VERSION] Looking for $kCrashReportExe in ${directory.path}");
final defaultGameVersion = path.basename(directory.path);
final crashReportClients = await findFiles(directory, kCrashReportExe);
if (crashReportClients.isEmpty) {
log("[VERSION] Didn't find a unique match: $crashReportClients");
return defaultGameVersion;
}
log("[VERSION] Extracting game version from ${crashReportClients.last.path}(default: $defaultGameVersion)");
final filePathPtr = crashReportClients.last.path.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)) {
log("[VERSION] Using default value");
calloc.free(pPropertyStore);
return defaultGameVersion;
}
final propertyStore = IPropertyStore(pPropertyStore);
final countPtr = calloc<Uint32>();
final hrCount = propertyStore.getCount(countPtr);
final count = countPtr.value;
calloc.free(countPtr);
if (FAILED(hrCount)) {
log("[VERSION] Using default value");
return defaultGameVersion;
}
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();
final headerIndex = valueStr.indexOf("++Fortnite");
if (headerIndex != -1) {
log("[VERSION] Found value string: $valueStr");
var gameVersion = valueStr.substring(valueStr.indexOf("-", headerIndex) + 1);
log("[VERSION] Game version: $gameVersion");
if(gameVersion == "Cert" || gameVersion == "Next") {
final engineVersion = valueStr.substring(0, valueStr.indexOf("+"));
log("[VERSION] Engine version: $engineVersion");
final engineVersionParts = engineVersion.split("-");
final engineVersionBuild = int.parse(engineVersionParts[1]);
log("[VERSION] Engine build: $engineVersionBuild");
gameVersion = _buildToGameVersion[engineVersionBuild] ?? defaultGameVersion;
}
log("[VERSION] Parsed game version: $gameVersion");
return gameVersion;
}
}
}
calloc.free(pKey);
calloc.free(pv);
}
log("[VERSION] Using default value");
return defaultGameVersion;
});

View File

@@ -0,0 +1,20 @@
import 'dart:io';
class GameVersion {
String name;
String gameVersion;
Directory location;
GameVersion.fromJson(json)
: name = json["name"],
gameVersion = json["gameVersion"],
location = Directory(json["location"]);
GameVersion({required this.name, required this.gameVersion, required this.location});
Map<String, dynamic> toJson() => {
'name': name,
'gameVersion': gameVersion,
'location': location.path
};
}