mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
9.0.4
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export 'package:reboot_common/src/constant/authenticator.dart';
|
||||
export 'package:reboot_common/src/constant/backend.dart';
|
||||
export 'package:reboot_common/src/constant/game.dart';
|
||||
export 'package:reboot_common/src/constant/matchmaker.dart';
|
||||
export 'package:reboot_common/src/constant/supabase.dart';
|
||||
@@ -9,8 +9,7 @@ export 'package:reboot_common/src/model/server_result.dart';
|
||||
export 'package:reboot_common/src/model/server_type.dart';
|
||||
export 'package:reboot_common/src/model/update_status.dart';
|
||||
export 'package:reboot_common/src/model/update_timer.dart';
|
||||
export 'package:reboot_common/src/model/process.dart';
|
||||
export 'package:reboot_common/src/util/authenticator.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/matchmaker.dart';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
const String kDefaultAuthenticatorHost = "127.0.0.1";
|
||||
const int kDefaultAuthenticatorPort = 3551;
|
||||
2
common/lib/src/constant/backend.dart
Normal file
2
common/lib/src/constant/backend.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
const String kDefaultBackendHost = "127.0.0.1";
|
||||
const int kDefaultBackendPort = 3551;
|
||||
@@ -1,11 +1,17 @@
|
||||
const String kDefaultPlayerName = "Player";
|
||||
const String kDefaultGameServerHost = "127.0.0.1";
|
||||
const String kDefaultGameServerPort = "7777";
|
||||
const String kConsoleLine = "Region ";
|
||||
const String kInitializedLine = "Game Engine Initialized";
|
||||
const List<String> kLoggedInLines = [
|
||||
"[UOnlineAccountCommon::ContinueLoggingIn]",
|
||||
"(Completed)"
|
||||
];
|
||||
const String kShutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
|
||||
const List<String> kCorruptedBuildErrors = [
|
||||
"Critical error",
|
||||
"when 0 bytes remain",
|
||||
"Pak chunk signature verification failed!"
|
||||
"Pak chunk signature verification failed!",
|
||||
"Couldn't find pak signature file"
|
||||
];
|
||||
const List<String> kCannotConnectErrors = [
|
||||
"port 3551 failed: Connection refused",
|
||||
|
||||
@@ -20,7 +20,11 @@ class FortniteBuildDownloadProgress {
|
||||
final int? minutesLeft;
|
||||
final bool extracting;
|
||||
|
||||
FortniteBuildDownloadProgress(this.progress, this.minutesLeft, this.extracting);
|
||||
FortniteBuildDownloadProgress({
|
||||
required this.progress,
|
||||
required this.extracting,
|
||||
this.minutesLeft,
|
||||
});
|
||||
}
|
||||
|
||||
class FortniteBuildDownloadOptions {
|
||||
|
||||
@@ -6,7 +6,6 @@ class GameInstance {
|
||||
final int gamePid;
|
||||
final int? launcherPid;
|
||||
final int? eacPid;
|
||||
int? observerPid;
|
||||
bool hosting;
|
||||
bool launched;
|
||||
bool tokenError;
|
||||
@@ -29,9 +28,18 @@ class GameInstance {
|
||||
if(eacPid != null) {
|
||||
Process.killPid(eacPid!, ProcessSignal.sigabrt);
|
||||
}
|
||||
if(observerPid != null) {
|
||||
Process.killPid(observerPid!, ProcessSignal.sigabrt);
|
||||
}
|
||||
|
||||
bool get nestedHosting {
|
||||
GameInstance? child = this;
|
||||
while(child != null) {
|
||||
if(child.hosting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
child = child.child;
|
||||
}
|
||||
child?.kill();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
class Win32Process {
|
||||
final int pid;
|
||||
final Stream<String> stdOutput;
|
||||
final Stream<String> errorOutput;
|
||||
|
||||
Win32Process({
|
||||
required this.pid,
|
||||
required this.stdOutput,
|
||||
required this.errorOutput
|
||||
});
|
||||
}
|
||||
|
||||
class PrimitiveWin32Process {
|
||||
final int pid;
|
||||
final int stdOutputHandle;
|
||||
final int errorOutputHandle;
|
||||
|
||||
PrimitiveWin32Process({
|
||||
required this.pid,
|
||||
required this.stdOutputHandle,
|
||||
required this.errorOutputHandle
|
||||
});
|
||||
}
|
||||
@@ -24,4 +24,6 @@ enum ServerResultType {
|
||||
pingError;
|
||||
|
||||
bool get isError => name.contains("Error");
|
||||
|
||||
bool get isSuccess => this == ServerResultType.startSuccess || this == ServerResultType.stopSuccess;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
|
||||
final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator");
|
||||
final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe");
|
||||
|
||||
Future<Win32Process> startEmbeddedAuthenticator(bool detached) async => startProcess(
|
||||
executable: authenticatorStartExecutable,
|
||||
window: detached,
|
||||
|
||||
);
|
||||
|
||||
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort);
|
||||
|
||||
Future<bool> isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort) == null;
|
||||
|
||||
Future<bool> freeAuthenticatorPort() async {
|
||||
await killProcessByPort(kDefaultAuthenticatorPort);
|
||||
final standardResult = await isAuthenticatorPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<Uri?> pingAuthenticator(String host, int port, [bool https=false]) async {
|
||||
var hostName = _getHostName(host);
|
||||
var declaredScheme = _getScheme(host);
|
||||
try{
|
||||
var uri = Uri(
|
||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||
host: hostName,
|
||||
port: port,
|
||||
path: "unknown"
|
||||
);
|
||||
var client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 5);
|
||||
var request = await client.getUrl(uri);
|
||||
var response = await request.close();
|
||||
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
|
||||
}catch(_){
|
||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingAuthenticator(host, port, true);
|
||||
}
|
||||
}
|
||||
|
||||
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
|
||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
|
||||
@@ -1,24 +1,61 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
|
||||
final matchmakerDirectory = Directory("${assetsDirectory.path}\\matchmaker");
|
||||
final matchmakerStartExecutable = File("${matchmakerDirectory.path}\\fortmatchmaker.exe");
|
||||
final matchmakerKillExecutable = File("${authenticatorDirectory.path}\\kill.bat");
|
||||
final matchmakerConfigFile = File("${authenticatorDirectory.path}\\Config\\config.ini");
|
||||
final Directory backendDirectory = Directory("${assetsDirectory.path}\\backend");
|
||||
final File backendStartExecutable = File("${backendDirectory.path}\\lawinserver.exe");
|
||||
final File matchmakerConfigFile = File("${backendDirectory.path}\\Config\\config.ini");
|
||||
final Semaphore _semaphore = Semaphore();
|
||||
String? _lastIp;
|
||||
String? _lastPort;
|
||||
Semaphore _semaphore = Semaphore();
|
||||
|
||||
Future<Win32Process> startEmbeddedMatchmaker(bool detached) async => startProcess(
|
||||
executable: matchmakerStartExecutable,
|
||||
Future<Process> startEmbeddedBackend(bool detached) async => startProcess(
|
||||
executable: backendStartExecutable,
|
||||
window: detached,
|
||||
|
||||
);
|
||||
|
||||
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);
|
||||
|
||||
Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost, kDefaultBackendPort) == null;
|
||||
|
||||
Future<bool> freeBackendPort() async {
|
||||
await killProcessByPort(kDefaultBackendPort);
|
||||
final standardResult = await isBackendPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||
var hostName = _getHostName(host);
|
||||
var declaredScheme = _getScheme(host);
|
||||
try{
|
||||
var uri = Uri(
|
||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||
host: hostName,
|
||||
port: port,
|
||||
path: "unknown"
|
||||
);
|
||||
var client = HttpClient()
|
||||
..connectionTimeout = const Duration(seconds: 5);
|
||||
var request = await client.getUrl(uri);
|
||||
var response = await request.close();
|
||||
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
|
||||
}catch(_){
|
||||
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
|
||||
}
|
||||
}
|
||||
|
||||
String? _getHostName(String host) => host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
|
||||
String? _getScheme(String host) => host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
|
||||
Stream<String?> watchMatchmakingIp() async* {
|
||||
if(!matchmakerConfigFile.existsSync()){
|
||||
return;
|
||||
@@ -99,7 +136,7 @@ Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
|
||||
var completer = Completer<bool>();
|
||||
var socket = await WebSocket.connect(uri.toString());
|
||||
socket.listen(
|
||||
(data) {
|
||||
(data) {
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(true);
|
||||
}
|
||||
@@ -125,20 +162,4 @@ Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
|
||||
|
||||
String? _getHostName(String host) => host.replaceFirst("ws://", "").replaceFirst("wss://", "");
|
||||
|
||||
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
|
||||
|
||||
extension StringExtension on String {
|
||||
bool get isBlank {
|
||||
if(isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for(var char in this.split("")) {
|
||||
if(char != " ") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
|
||||
@@ -25,8 +25,11 @@ Dio _buildDioInstance() {
|
||||
|
||||
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
|
||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||
const String _manifestSourceUrl = "https://manifest.fnbuilds.services";
|
||||
const int _maxDownloadErrors = 30;
|
||||
const String _manifestSourceUrl = "http://manifest.simplyblk.xyz";
|
||||
const String _deniedConnectionError = "The connection was denied: your firewall might be blocking the download";
|
||||
const String _unavailableError = "The build downloader is not available right now";
|
||||
const String _genericError = "The build downloader is not working correctly";
|
||||
const int _maxErrors = 100;
|
||||
|
||||
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () =>
|
||||
@@ -35,7 +38,7 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
|
||||
final data = <FortniteBuild>[];
|
||||
final data = <FortniteBuild>[];
|
||||
for(final result in results) {
|
||||
data.addAll(result);
|
||||
}
|
||||
@@ -45,7 +48,15 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
|
||||
|
||||
Future<List<FortniteBuild>> _fetchManifestBuilds() async {
|
||||
try {
|
||||
final response = await _dio.get<String>("$_manifestSourceUrl/versions.json");
|
||||
final response = await _dio.get<String>(
|
||||
"$_manifestSourceUrl/versions.json",
|
||||
options: Options(
|
||||
headers: {
|
||||
"Accept-Encoding": "*",
|
||||
"Cookie": "_c_t_c=1"
|
||||
}
|
||||
)
|
||||
);
|
||||
final body = response.data;
|
||||
return jsonDecode(body!).map((version) {
|
||||
final nameParts = version.split("-");
|
||||
@@ -131,7 +142,14 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||
delete(outputDir);
|
||||
break;
|
||||
case FortniteBuildSource.manifest:
|
||||
final response = await _dio.get<String>(options.build.link);
|
||||
final response = await _dio.get<String>(
|
||||
options.build.link,
|
||||
options: Options(
|
||||
headers: {
|
||||
"Cookie": "_c_t_c=1"
|
||||
}
|
||||
)
|
||||
);
|
||||
final manifest = FortniteBuildManifestFile.fromJson(jsonDecode(response.data!));
|
||||
|
||||
final totalBytes = manifest.size;
|
||||
@@ -171,7 +189,8 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||
options: Options(
|
||||
responseType: ResponseType.bytes,
|
||||
headers: {
|
||||
"Accept-Encoding": "gzip"
|
||||
"Accept-Encoding": "gzip",
|
||||
"Cookie": "_c_t_c=1"
|
||||
}
|
||||
),
|
||||
);
|
||||
@@ -201,24 +220,23 @@ Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
|
||||
}
|
||||
});
|
||||
await Future.any([stopped.future, Future.wait(writers)]);
|
||||
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
|
||||
break;
|
||||
}
|
||||
}catch(error, stackTrace) {
|
||||
options.port.send("$error\n$stackTrace");
|
||||
}catch(error) {
|
||||
_onError(error, options);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
|
||||
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
|
||||
var received = byteStart ?? 0;
|
||||
try {
|
||||
return await _dio.download(
|
||||
await _dio.download(
|
||||
options.build.link,
|
||||
tempFile.path,
|
||||
onReceiveProgress: (data, length) {
|
||||
received = data;
|
||||
final percentage = (received / length) * 100;
|
||||
_onProgress(startTime, DateTime.now().millisecondsSinceEpoch, percentage, false, options);
|
||||
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options);
|
||||
},
|
||||
deleteOnError: false,
|
||||
options: Options(
|
||||
@@ -228,10 +246,14 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
|
||||
}
|
||||
|
||||
if(statusCode == 403 || statusCode == 503) {
|
||||
throw Exception("The connection was denied: your firewall might be blocking the download");
|
||||
throw _deniedConnectionError;
|
||||
}
|
||||
|
||||
throw Exception("The build downloader is not available right now");
|
||||
if(statusCode == 404) {
|
||||
throw _unavailableError;
|
||||
}
|
||||
|
||||
throw _genericError;
|
||||
},
|
||||
headers: byteStart == null || byteStart <= 0 ? {
|
||||
"Cookie": "_c_t_c=1"
|
||||
@@ -242,17 +264,18 @@ Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tem
|
||||
)
|
||||
);
|
||||
}catch(error) {
|
||||
if(errorsCount >= _maxDownloadErrors) {
|
||||
throw error;
|
||||
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) {
|
||||
_onError(error, options);
|
||||
return;
|
||||
}
|
||||
|
||||
return await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
|
||||
await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
Win32Process? process;
|
||||
Process? process;
|
||||
switch (extension.toLowerCase()) {
|
||||
case ".zip":
|
||||
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
|
||||
@@ -271,10 +294,10 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
],
|
||||
);
|
||||
process.stdOutput.listen((data) {
|
||||
print(data);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
if(data == "Everything is Ok") {
|
||||
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
|
||||
if(data.toLowerCase().contains("everything is ok")) {
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -286,6 +309,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
|
||||
_onProgress(startTime, now, percentage, true, options);
|
||||
});
|
||||
process.stdError.listen((data) {
|
||||
if(!data.isBlank) {
|
||||
_onError(data, options);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ".rar":
|
||||
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
|
||||
@@ -298,17 +326,17 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
args: [
|
||||
"x",
|
||||
"-o+",
|
||||
tempFile.path,
|
||||
'"${tempFile.path}"',
|
||||
"*.*",
|
||||
options.destination.path
|
||||
'"${options.destination.path}"'
|
||||
]
|
||||
);
|
||||
process.stdOutput.listen((data) {
|
||||
print(data);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||
data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
|
||||
if(data == "All OK") {
|
||||
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
|
||||
_onProgress(startTime, now, 100, true, options);
|
||||
process?.kill(ProcessSignal.sigabrt);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +348,11 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
final percentage = int.parse(element).toDouble();
|
||||
_onProgress(startTime, now, percentage, true, options);
|
||||
});
|
||||
process.errorOutput.listen((data) => options.port.send(data));
|
||||
process.stdError.listen((data) {
|
||||
if(!data.isBlank) {
|
||||
_onError(data, options);
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw ArgumentError("Unexpected file extension: $extension}");
|
||||
@@ -329,17 +361,31 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
await Future.any([stopped.future, watchProcess(process.pid)]);
|
||||
}
|
||||
|
||||
void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
||||
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
||||
if(percentage == 0) {
|
||||
options.port.send(FortniteBuildDownloadProgress(percentage, null, extracting));
|
||||
options.port.send(FortniteBuildDownloadProgress(
|
||||
progress: percentage,
|
||||
extracting: extracting
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final msLeft = startTime + (now - startTime) * 100 / percentage - now;
|
||||
final minutesLeft = (msLeft / 1000 / 60).round();
|
||||
options.port.send(FortniteBuildDownloadProgress(percentage, minutesLeft, extracting));
|
||||
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now;
|
||||
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
|
||||
options.port.send(FortniteBuildDownloadProgress(
|
||||
progress: percentage,
|
||||
extracting: extracting,
|
||||
minutesLeft: minutesLeft
|
||||
));
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:reboot_common/common.dart';
|
||||
bool _watcher = false;
|
||||
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
|
||||
const String kRebootDownloadUrl =
|
||||
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
||||
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip";
|
||||
|
||||
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
|
||||
final lastUpdate = await _getLastUpdate(lastUpdateMs);
|
||||
@@ -18,7 +18,7 @@ Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force =
|
||||
}
|
||||
|
||||
Future<void> downloadCriticalDll(String name, String outputPath) async {
|
||||
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/tree/master/gui/assets/dlls/$name"));
|
||||
final response = await http.get(Uri.parse("https://github.com/Auties00/reboot_launcher/raw/master/gui/assets/dlls/$name"));
|
||||
if(response.statusCode != 200) {
|
||||
throw Exception("Cannot download $name: status code ${response.statusCode}");
|
||||
}
|
||||
@@ -64,7 +64,6 @@ Stream<String> watchDlls() async* {
|
||||
|
||||
_watcher = true;
|
||||
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
|
||||
print(event);
|
||||
if (event.path.endsWith(".dll")) {
|
||||
yield event.path;
|
||||
}
|
||||
|
||||
@@ -31,27 +31,36 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
|
||||
var read = await file.readAsBytes();
|
||||
var length = await file.length();
|
||||
var offset = 0;
|
||||
var counter = 0;
|
||||
while(offset < length){
|
||||
if(read[offset] == original[counter]){
|
||||
counter++;
|
||||
}else {
|
||||
counter = 0;
|
||||
}
|
||||
|
||||
offset++;
|
||||
if(counter == original.length){
|
||||
for(var index = 0; index < patched.length; index++){
|
||||
read[offset - counter + index] = patched[index];
|
||||
var readOffset = 0;
|
||||
var patchOffset = -1;
|
||||
var patchCount = 0;
|
||||
while(readOffset < length){
|
||||
if(read[readOffset] == original[patchCount]){
|
||||
if(patchOffset == -1) {
|
||||
patchOffset = readOffset;
|
||||
}
|
||||
|
||||
await file.writeAsBytes(read, mode: FileMode.write);
|
||||
return true;
|
||||
if(++patchCount == original.length) {
|
||||
break;
|
||||
}
|
||||
}else {
|
||||
patchOffset = -1;
|
||||
}
|
||||
|
||||
readOffset++;
|
||||
}
|
||||
|
||||
return false;
|
||||
print("Offset: $patchOffset");
|
||||
if(patchOffset == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var i = 0; i < patched.length; i++) {
|
||||
read[patchOffset + i] = patched[i];
|
||||
}
|
||||
|
||||
await file.writeAsBytes(read, flush: true);
|
||||
return true;
|
||||
}catch(_){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -52,8 +52,10 @@ extension FortniteVersionExtension on FortniteVersion {
|
||||
}
|
||||
}
|
||||
|
||||
Future<File?> get executable async {
|
||||
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.exe");
|
||||
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
|
||||
Future<File?> get headlessGameExecutable async {
|
||||
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
@@ -63,12 +65,9 @@ extension FortniteVersionExtension on FortniteVersion {
|
||||
return null;
|
||||
}
|
||||
|
||||
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Reboot.exe");
|
||||
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
||||
await original.copy(output.path);
|
||||
await Future.wait([
|
||||
Isolate.run(() => patchMatchmaking(output)),
|
||||
Isolate.run(() => patchHeadless(output)),
|
||||
]);
|
||||
await Isolate.run(() => patchHeadless(output));
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:reboot_common/src/model/process.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
import '../constant/game.dart';
|
||||
|
||||
final _ntdll = DynamicLibrary.open('ntdll.dll');
|
||||
final _kernel32 = DynamicLibrary.open('kernel32.dll');
|
||||
final _CreateRemoteThread = _kernel32.lookupFunction<
|
||||
@@ -90,139 +90,53 @@ Future<void> injectDll(int pid, String dll) async {
|
||||
}
|
||||
}
|
||||
|
||||
void _startProcess(_ProcessParameters params) {
|
||||
final args = params.args;
|
||||
final port = params.port;
|
||||
final concatenatedArgs = args == null ? "" : " ${args.join(" ")}";
|
||||
final command = params.window ? 'cmd.exe /k ""${params.executable.path}"$concatenatedArgs"' : '"${params.executable.path}"$concatenatedArgs';
|
||||
print(command);
|
||||
final processInfo = calloc<PROCESS_INFORMATION>();
|
||||
final lpStartupInfo = calloc<STARTUPINFO>();
|
||||
lpStartupInfo.ref.cb = sizeOf<STARTUPINFO>();
|
||||
lpStartupInfo.ref.dwFlags |= STARTF_USESTDHANDLES;
|
||||
final securityAttributes = calloc<SECURITY_ATTRIBUTES>();
|
||||
securityAttributes.ref.nLength = sizeOf<SECURITY_ATTRIBUTES>();
|
||||
securityAttributes.ref.bInheritHandle = TRUE;
|
||||
final hStdOutRead = calloc<HANDLE>();
|
||||
final hStdOutWrite = calloc<HANDLE>();
|
||||
final hStdErrRead = calloc<HANDLE>();
|
||||
final hStdErrWrite = calloc<HANDLE>();
|
||||
if (CreatePipe(hStdOutRead, hStdOutWrite, securityAttributes, 0) == 0 || CreatePipe(hStdErrRead, hStdErrWrite, securityAttributes, 0) == 0) {
|
||||
final error = GetLastError();
|
||||
port.send("Cannot create process pipe: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
if(SetHandleInformation(hStdOutRead.value, HANDLE_FLAG_INHERIT, 0) == 0 || SetHandleInformation(hStdErrRead.value, HANDLE_FLAG_INHERIT, 0) == 0) {
|
||||
final error = GetLastError();
|
||||
port.send("Cannot set process pipe information: $error");
|
||||
return;
|
||||
Future<Process> startProcess({required File executable, List<String>? args, bool wrapProcess = true, bool window = false, String? name}) async {
|
||||
final argsOrEmpty = args ?? [];
|
||||
if(wrapProcess) {
|
||||
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: executable.parent.path,
|
||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||
runInShell: window
|
||||
);
|
||||
return _withLogger(name, executable, process, window);
|
||||
}
|
||||
|
||||
lpStartupInfo.ref.hStdOutput = hStdOutWrite.value;
|
||||
lpStartupInfo.ref.hStdError = hStdErrWrite.value;
|
||||
final success = CreateProcess(
|
||||
nullptr,
|
||||
TEXT(command),
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
NORMAL_PRIORITY_CLASS | (params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW) | CREATE_NEW_PROCESS_GROUP,
|
||||
nullptr,
|
||||
TEXT(params.executable.parent.path),
|
||||
lpStartupInfo,
|
||||
processInfo
|
||||
final process = await Process.start(
|
||||
executable.path,
|
||||
args ?? [],
|
||||
workingDirectory: executable.parent.path,
|
||||
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
|
||||
runInShell: window
|
||||
);
|
||||
if (success == 0) {
|
||||
final error = GetLastError();
|
||||
port.send("Cannot start process: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
CloseHandle(processInfo.ref.hProcess);
|
||||
CloseHandle(processInfo.ref.hThread);
|
||||
CloseHandle(hStdOutWrite.value);
|
||||
CloseHandle(hStdErrWrite.value);
|
||||
final pid = processInfo.ref.dwProcessId;
|
||||
free(lpStartupInfo);
|
||||
free(processInfo);
|
||||
port.send(PrimitiveWin32Process(
|
||||
pid: pid,
|
||||
stdOutputHandle: hStdOutRead.value,
|
||||
errorOutputHandle: hStdErrRead.value
|
||||
));
|
||||
return _withLogger(name, executable, process, window);
|
||||
}
|
||||
|
||||
class _PipeReaderParams {
|
||||
final int handle;
|
||||
final SendPort port;
|
||||
|
||||
_PipeReaderParams(this.handle, this.port);
|
||||
}
|
||||
|
||||
void _pipeToStreamChannelled(_PipeReaderParams params) {
|
||||
final buf = calloc<Uint8>(chunkSize);
|
||||
while(true) {
|
||||
final bytesReadPtr = calloc<Uint32>();
|
||||
final success = ReadFile(params.handle, buf, chunkSize, bytesReadPtr, nullptr);
|
||||
if (success == FALSE) {
|
||||
break;
|
||||
}
|
||||
|
||||
final bytesRead = bytesReadPtr.value;
|
||||
if (bytesRead == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
final lines = utf8.decode(buf.asTypedList(bytesRead)).split('\n');
|
||||
for(final line in lines) {
|
||||
params.port.send(line);
|
||||
}
|
||||
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) {
|
||||
final extendedProcess = _ExtendedProcess(process, true);
|
||||
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log");
|
||||
loggingFile.parent.createSync(recursive: true);
|
||||
if(loggingFile.existsSync()) {
|
||||
loggingFile.deleteSync();
|
||||
}
|
||||
|
||||
CloseHandle(params.handle);
|
||||
free(buf);
|
||||
Isolate.current.kill();
|
||||
}
|
||||
|
||||
Stream<String> _pipeToStream(int pipeHandle) {
|
||||
final port = ReceivePort();
|
||||
Isolate.spawn(
|
||||
_pipeToStreamChannelled,
|
||||
_PipeReaderParams(pipeHandle, port.sendPort)
|
||||
);
|
||||
return port.map((event) => event as String);
|
||||
}
|
||||
|
||||
class _ProcessParameters {
|
||||
File executable;
|
||||
List<String>? args;
|
||||
bool window;
|
||||
SendPort port;
|
||||
|
||||
_ProcessParameters(this.executable, this.args, this.window, this.port);
|
||||
}
|
||||
|
||||
Future<Win32Process> startProcess({required File executable, List<String>? args, bool output = true, bool window = false}) async {
|
||||
final completer = Completer<Win32Process>();
|
||||
final port = ReceivePort();
|
||||
port.listen((message) {
|
||||
if(message is PrimitiveWin32Process) {
|
||||
completer.complete(Win32Process(
|
||||
pid: message.pid,
|
||||
stdOutput: _pipeToStream(message.stdOutputHandle),
|
||||
errorOutput: _pipeToStream(message.errorOutputHandle)
|
||||
));
|
||||
} else {
|
||||
completer.completeError(message);
|
||||
}
|
||||
});
|
||||
Isolate.spawn(
|
||||
_startProcess,
|
||||
_ProcessParameters(executable, args, window, port.sendPort),
|
||||
errorsAreFatal: true
|
||||
);
|
||||
return await completer.future;
|
||||
final semaphore = Semaphore(1);
|
||||
void logEvent(String event) async {
|
||||
await semaphore.acquire();
|
||||
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
|
||||
semaphore.release();
|
||||
}
|
||||
extendedProcess.stdOutput.listen(logEvent);
|
||||
extendedProcess.stdError.listen(logEvent);
|
||||
if(!window) {
|
||||
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
|
||||
}
|
||||
return extendedProcess;
|
||||
}
|
||||
|
||||
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
|
||||
@@ -247,8 +161,11 @@ bool resume(int pid) {
|
||||
|
||||
void _watchProcess(int pid) {
|
||||
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
|
||||
WaitForSingleObject(processHandle, INFINITE);
|
||||
CloseHandle(processHandle);
|
||||
try {
|
||||
WaitForSingleObject(processHandle, INFINITE);
|
||||
}finally {
|
||||
CloseHandle(processHandle);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> watchProcess(int pid) async {
|
||||
@@ -261,16 +178,14 @@ Future<bool> watchProcess(int pid) async {
|
||||
});
|
||||
var errorPort = ReceivePort();
|
||||
errorPort.listen((_) => completer.complete(false));
|
||||
var isolate = await Isolate.spawn(
|
||||
await Isolate.spawn(
|
||||
_watchProcess,
|
||||
pid,
|
||||
onExit: exitPort.sendPort,
|
||||
onError: errorPort.sendPort,
|
||||
errorsAreFatal: true
|
||||
);
|
||||
var result = await completer.future;
|
||||
isolate.kill(priority: Isolate.immediate);
|
||||
return result;
|
||||
return await completer.future;
|
||||
}
|
||||
|
||||
List<String> createRebootArgs(String username, String password, bool host, bool headless, String additionalArgs) {
|
||||
@@ -324,4 +239,53 @@ String _parseUsername(String username, bool host) {
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
class _ExtendedProcess extends 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;
|
||||
}
|
||||
}
|
||||
|
||||
extension ProcessExtension on Process {
|
||||
Stream<String> get stdOutput => this.stdout.expand((event) => utf8.decode(event).split("\n"));
|
||||
|
||||
Stream<String> get stdError => this.stderr.expand((event) => utf8.decode(event).split("\n"));
|
||||
}
|
||||
Reference in New Issue
Block a user