mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
10.0.8
This commit is contained in:
@@ -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';
|
||||
@@ -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");
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
546
common/lib/src/util/downloader.dart
Normal file
546
common/lib/src/util/downloader.dart
Normal 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;
|
||||
}
|
||||
276
common/lib/src/util/game.dart
Normal file
276
common/lib/src/util/game.dart
Normal 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;
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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
468
common/lib/src/util/os.dart
Normal 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());
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user