mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-14 03:32:23 +01:00
Refactored GUI
This commit is contained in:
64
common/lib/src/game/game_build.dart
Normal file
64
common/lib/src/game/game_build.dart
Normal 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"]
|
||||
);
|
||||
}
|
||||
34
common/lib/src/game/game_constants.dart
Normal file
34
common/lib/src/game/game_constants.dart
Normal 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");
|
||||
10
common/lib/src/game/game_dll.dart
Normal file
10
common/lib/src/game/game_dll.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
enum GameDll {
|
||||
console,
|
||||
auth,
|
||||
gameServer,
|
||||
memoryLeak
|
||||
}
|
||||
|
||||
extension InjectableDllVersionAware on GameDll {
|
||||
bool get isVersionDependent => this == GameDll.gameServer;
|
||||
}
|
||||
559
common/lib/src/game/game_downloader.dart
Normal file
559
common/lib/src/game/game_downloader.dart
Normal 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;
|
||||
}
|
||||
50
common/lib/src/game/game_instance.dart
Normal file
50
common/lib/src/game/game_instance.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
383
common/lib/src/game/game_metadata.dart
Normal file
383
common/lib/src/game/game_metadata.dart
Normal 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;
|
||||
});
|
||||
20
common/lib/src/game/game_version.dart
Normal file
20
common/lib/src/game/game_version.dart
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user