This commit is contained in:
Alessandro Autiero
2024-05-20 17:24:00 +02:00
parent 7c2caed16c
commit 9f5590d41c
562 changed files with 3303 additions and 156787 deletions

View File

@@ -1,11 +1,7 @@
export 'package:reboot_common/src/constant/authenticator.dart';
export 'package:reboot_common/src/constant/game.dart';
export 'package:reboot_common/src/constant/matchmaker.dart';
export 'package:reboot_common/src/constant/os.dart';
export 'package:reboot_common/src/constant/supabase.dart';
export 'package:reboot_common/src/model/archive.dart';
export 'package:reboot_common/src/model/fortnite_build.dart';
export 'package:reboot_common/src/model/fortnite_version.dart';
export 'package:reboot_common/src/model/game_instance.dart';
@@ -13,12 +9,11 @@ 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/util/authenticator.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';
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/reboot.dart';

View File

@@ -1,2 +1,2 @@
const String kDefaultAuthenticatorHost = "127.0.0.1";
const String kDefaultAuthenticatorPort = "3551";
const int kDefaultAuthenticatorPort = 3551;

View File

@@ -1,12 +1,13 @@
const String kDefaultPlayerName = "Player";
const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777";
const String shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> corruptedBuildErrors = [
const String kConsoleLine = "Region ";
const String kShutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> kCorruptedBuildErrors = [
"when 0 bytes remain",
"Pak chunk signature verification failed!"
];
const List<String> cannotConnectErrors = [
const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused",
"Unable to login to Fortnite servers",
"HTTP 400 response from ",

View File

@@ -1,2 +1,2 @@
const String kDefaultMatchmakerHost = "127.0.0.1";
const String kDefaultMatchmakerPort = "8080";
const int kDefaultMatchmakerPort = 8080;

View File

@@ -1 +0,0 @@
const int appBarWidth = 2;

View File

@@ -1,2 +1,2 @@
const String supabaseUrl = 'https://pocjparoguvaeeyjapjb.supabase.co';
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBvY2pwYXJvZ3V2YWVleWphcGpiIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTUzMTM4NTUsImV4cCI6MjAxMDg4OTg1NX0.BffJtbQvX1NVUy-9Nj4GVzUJXPK_1GyezDE0V5MRiao';
const String supabaseUrl = 'https://drxuhdtyigthmjfhjgfl.supabase.co';
const String supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRyeHVoZHR5aWd0aG1qZmhqZ2ZsIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODUzMDU4NjYsImV4cCI6MjAwMDg4MTg2Nn0.unuO67xf9CZgHi-3aXmC5p3RAktUfW7WwqDY-ccFN1M';

View File

@@ -1,18 +0,0 @@
import 'dart:io';
import 'dart:isolate';
class ArchiveDownloadProgress {
final double progress;
final int? minutesLeft;
final bool extracting;
ArchiveDownloadProgress(this.progress, this.minutesLeft, this.extracting);
}
class ArchiveDownloadOptions {
String archiveUrl;
Directory destination;
SendPort port;
ArchiveDownloadOptions(this.archiveUrl, this.destination, this.port);
}

View File

@@ -1,6 +1,60 @@
import 'dart:io';
import 'dart:isolate';
class FortniteBuild {
final String identifier;
final String version;
final String link;
final FortniteBuildSource source;
FortniteBuild({required this.version, required this.link});
FortniteBuild({required this.identifier, required this.version, required this.link, required this.source});
}
enum FortniteBuildSource {
manifest,
archive
}
class FortniteBuildDownloadProgress {
final double progress;
final int? minutesLeft;
final bool extracting;
FortniteBuildDownloadProgress(this.progress, this.minutesLeft, this.extracting);
}
class FortniteBuildDownloadOptions {
FortniteBuild build;
Directory destination;
SendPort port;
FortniteBuildDownloadOptions(this.build, this.destination, this.port);
}
class FortniteBuildManifestChunk {
List<int> chunksIds;
String file;
int fileSize;
FortniteBuildManifestChunk._internal(this.chunksIds, this.file, this.fileSize);
factory FortniteBuildManifestChunk.fromJson(json) => FortniteBuildManifestChunk._internal(
List<int>.from(json["ChunksIds"] as List),
json["File"],
json["FileSize"]
);
}
class FortniteBuildManifestFile {
String name;
List<FortniteBuildManifestChunk> chunks;
int size;
FortniteBuildManifestFile._internal(this.name, this.chunks, this.size);
factory FortniteBuildManifestFile.fromJson(json) => FortniteBuildManifestFile._internal(
json["Name"],
List<FortniteBuildManifestChunk>.from(json["Chunks"].map((chunk) => FortniteBuildManifestChunk.fromJson(chunk))),
json["Size"]
);
}

View File

@@ -8,35 +8,18 @@ class GameInstance {
final int? eacPid;
int? observerPid;
bool hosting;
bool launched;
bool tokenError;
bool linkedHosting;
GameInstance? child;
GameInstance(this.versionName, this.gamePid, this.launcherPid, this.eacPid, this.hosting, this.linkedHosting)
: tokenError = false,
assert(!linkedHosting || !hosting, "Only a game instance can have a linked hosting server");
GameInstance._fromJson(this.versionName, this.gamePid, this.launcherPid, this.eacPid, this.observerPid,
this.hosting, this.tokenError, this.linkedHosting);
static GameInstance? fromJson(Map<String, dynamic>? json) {
if(json == null) {
return null;
}
var gamePid = json["game"];
if(gamePid == null) {
return null;
}
var version = json["versionName"];
var launcherPid = json["launcher"];
var eacPid = json["eac"];
var observerPid = json["observer"];
var hosting = json["hosting"];
var tokenError = json["tokenError"];
var linkedHosting = json["linkedHosting"];
return GameInstance._fromJson(version, gamePid, launcherPid, eacPid, observerPid, hosting, tokenError, linkedHosting);
}
GameInstance({
required this.versionName,
required this.gamePid,
required this.launcherPid,
required this.eacPid,
required this.hosting,
required this.child
}): tokenError = false, launched = false;
void kill() {
Process.killPid(gamePid, ProcessSignal.sigabrt);
@@ -49,16 +32,6 @@ class GameInstance {
if(observerPid != null) {
Process.killPid(observerPid!, ProcessSignal.sigabrt);
}
child?.kill();
}
Map<String, dynamic> toJson() => {
'versionName': versionName,
'game': gamePid,
'launcher': launcherPid,
'eac': eacPid,
'observer': observerPid,
'hosting': hosting,
'tokenError': tokenError,
'linkedHosting': linkedHosting
};
}

View File

@@ -6,30 +6,27 @@ import 'package:shelf_proxy/shelf_proxy.dart';
final authenticatorDirectory = Directory("${assetsDirectory.path}\\authenticator");
final authenticatorStartExecutable = File("${authenticatorDirectory.path}\\lawinserver.exe");
final authenticatorKillExecutable = File("${authenticatorDirectory.path}\\kill.bat");
Future<int> startEmbeddedAuthenticator(bool detached) async => startBackgroundProcess(
executable: authenticatorStartExecutable,
window: detached
);
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, int.parse(kDefaultAuthenticatorPort));
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async {
print("CALLED: $uri");
return await serve(proxyHandler(uri), kDefaultAuthenticatorHost, kDefaultAuthenticatorPort);
}
Future<bool> isAuthenticatorPortFree() async => isPortFree(int.parse(kDefaultAuthenticatorPort));
Future<bool> isAuthenticatorPortFree() async => await pingAuthenticator(kDefaultAuthenticatorHost, kDefaultAuthenticatorPort.toString()) == null;
Future<bool> freeAuthenticatorPort() async {
await Process.run(authenticatorKillExecutable.path, []);
var standardResult = await isAuthenticatorPortFree();
await killProcessByPort(kDefaultAuthenticatorPort);
final standardResult = await isAuthenticatorPortFree();
if(standardResult) {
return true;
}
var elevatedResult = await runElevatedProcess(authenticatorKillExecutable.path, "");
if(!elevatedResult) {
return false;
}
return await isAuthenticatorPortFree();
return false;
}
Future<Uri?> pingAuthenticator(String host, String port, [bool https=false]) async {
@@ -48,7 +45,7 @@ Future<Uri?> pingAuthenticator(String host, String port, [bool https=false]) asy
var response = await request.close();
return response.statusCode == 200 || response.statusCode == 404 ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await pingAuthenticator(host, port, true);
return https || declaredScheme != null || isLocalHost(host) ? null : await pingAuthenticator(host, port, true);
}
}

View File

@@ -2,24 +2,62 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:dio/dio.dart';
const String kStopBuildDownloadSignal = "kill";
final Dio _dio = Dio();
final String _manifestSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
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;
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
var response = await _dio.get<String>(
_manifestSourceUrl,
final results = await Future.wait([_fetchManifestBuilds(), _fetchArchiveBuilds()]);
final data = <FortniteBuild>[];
for(final result in results) {
data.addAll(result);
}
return data;
}
Future<List<FortniteBuild>> _fetchManifestBuilds() async {
try {
final response = await _dio.get<String>("$_manifestSourceUrl/versions.json");
final body = response.data;
return jsonDecode(body!).map((version) {
final nameParts = version.split("-");
if(nameParts.length < 2) {
return null;
}
final name = nameParts[1];
return FortniteBuild(
identifier: name,
version: "Fortnite ${name}",
link: "$_manifestSourceUrl/$name/$name.manifest",
source: FortniteBuildSource.manifest
);
}).whereType<FortniteBuild>().toList();
}catch(_) {
return [];
}
}
Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
final response = await _dio.get<String>(
_archiveSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
return [];
}
var results = <FortniteBuild>[];
@@ -40,63 +78,154 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
var version = parts.first.trim();
version = version.substring(0, version.indexOf("-"));
results.add(FortniteBuild(version: "Fortnite $version", link: link));
results.add(FortniteBuild(
identifier: version,
version: "Fortnite $version",
link: link,
source: FortniteBuildSource.archive
));
}
return results;
}
Future<void> downloadArchiveBuild(ArchiveDownloadOptions options) async {
var stopped = _setupLifecycle(options);
var outputDir = Directory("${options.destination.path}\\.build");
outputDir.createSync(recursive: true);
options.destination.createSync(recursive: true);
var fileName = options.archiveUrl.substring(options.archiveUrl.lastIndexOf("/") + 1);
var extension = path.extension(fileName);
var tempFile = File("${outputDir.path}\\$fileName");
if(tempFile.existsSync()) {
tempFile.deleteSync(recursive: true);
}
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
try {
final stopped = _setupLifecycle(options);
switch(options.build.source) {
case FortniteBuildSource.archive:
final outputDir = Directory("${options.destination.path}\\.build");
await outputDir.create(recursive: true);
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final extension = path.extension(fileName);
final tempFile = File("${outputDir.path}\\$fileName");
if(await tempFile.exists()) {
await tempFile.delete(recursive: true);
}
var startTime = DateTime.now().millisecondsSinceEpoch;
var response = _downloadFile(options, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
var awaitedResponse = await response;
if (!awaitedResponse.statusCode.toString().startsWith("20")) {
throw Exception("Erroneous status code: ${awaitedResponse.statusCode}");
final startTime = DateTime.now().millisecondsSinceEpoch;
final response = _downloadArchive(options, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
var awaitedResponse = await response;
if (!awaitedResponse.statusCode.toString().startsWith("20")) {
options.port.send("Erroneous status code: ${awaitedResponse.statusCode}");
return;
}
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir);
break;
case FortniteBuildSource.manifest:
final response = await _dio.get<String>(options.build.link);
final manifest = FortniteBuildManifestFile.fromJson(jsonDecode(response.data!));
final totalBytes = manifest.size;
final outputDir = options.destination;
await outputDir.create(recursive: true);
final startTime = DateTime.now().millisecondsSinceEpoch;
final codec = GZipCodec();
var completedBytes = 0;
var lastPercentage = 0.0;
final writers = manifest.chunks.map((chunkedFile) async {
final outputFile = File('${outputDir.path}/${chunkedFile.file}');
if(outputFile.existsSync()) {
if(outputFile.lengthSync() != chunkedFile.fileSize) {
await outputFile.delete();
} else {
completedBytes += chunkedFile.fileSize;
final percentage = completedBytes * 100 / totalBytes;
if(percentage - lastPercentage > 0.1) {
_onProgress(
startTime,
DateTime.now().millisecondsSinceEpoch,
percentage,
false,
options
);
}
return;
}
}
await outputFile.parent.create(recursive: true);
for(final chunkId in chunkedFile.chunksIds) {
final response = await _dio.get<Uint8List>(
"$_manifestSourceUrl/${options.build.identifier}/$chunkId.chunk",
options: Options(
responseType: ResponseType.bytes,
headers: {
"Accept-Encoding": "gzip"
}
),
);
var responseBody = response.data;
if(responseBody == null) {
continue;
}
final decodedBody = codec.decode(responseBody);
await outputFile.writeAsBytes(
decodedBody,
mode: FileMode.append,
flush: true
);
completedBytes += decodedBody.length;
final percentage = completedBytes * 100 / totalBytes;
if(percentage - lastPercentage > 0.1) {
_onProgress(
startTime,
DateTime.now().millisecondsSinceEpoch,
percentage,
false,
options
);
}
}
});
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");
}
}
Future<Response> _downloadArchive(FortniteBuildDownloadOptions options, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
var received = byteStart ?? 0;
try {
return 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);
},
deleteOnError: false,
options: Options(
headers: byteStart == null ? null : {
"Range": "bytes=${byteStart}-"
}
)
);
}catch(error) {
if(errorsCount >= _maxDownloadErrors) {
throw error;
}
await _extract(stopped, extension, tempFile, options);
return await _downloadArchive(options, tempFile, startTime, received, errorsCount + 1);
}
delete(outputDir);
}
Future<Response> _downloadFile(ArchiveDownloadOptions options, File tempFile, int startTime, [int? byteStart = null]) {
var received = byteStart ?? 0;
return _dio.download(
options.archiveUrl,
tempFile.path,
onReceiveProgress: (data, length) {
received = data;
var now = DateTime.now();
var progress = (received / length) * 100;
var msLeft = startTime + (now.millisecondsSinceEpoch - startTime) * length / received - now.millisecondsSinceEpoch;
var minutesLeft = (msLeft / 1000 / 60).round();
options.port.send(ArchiveDownloadProgress(progress, minutesLeft, false));
},
deleteOnError: false,
options: Options(
headers: byteStart == null ? null : {
"Range": "bytes=${byteStart}-"
}
)
).catchError((error) => _downloadFile(options, tempFile, startTime, received));
}
Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFile, ArchiveDownloadOptions options) async {
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
var startTime = DateTime.now().millisecondsSinceEpoch;
Process? process;
switch (extension.toLowerCase()) {
@@ -106,25 +235,20 @@ Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFil
["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path]
);
process.stdout.listen((bytes) {
var now = DateTime.now().millisecondsSinceEpoch;
var data = utf8.decode(bytes);
final now = DateTime.now().millisecondsSinceEpoch;
final data = utf8.decode(bytes);
if(data == "Everything is Ok") {
options.port.send(ArchiveDownloadProgress(100, 0, true));
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
}
var element = data.trim().split(" ")[0];
final element = data.trim().split(" ")[0];
if(!element.endsWith("%")) {
return;
}
var percentage = int.parse(element.substring(0, element.length - 1));
if(percentage == 0) {
options.port.send(ArchiveDownloadProgress(percentage.toDouble(), null, true));
return;
}
_onProgress(startTime, now, percentage, options);
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
break;
case ".rar":
@@ -133,34 +257,29 @@ Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFil
["x", "-o+", tempFile.path, "*.*", options.destination.path]
);
process.stdout.listen((event) {
var now = DateTime.now().millisecondsSinceEpoch;
var data = utf8.decode(event);
final now = DateTime.now().millisecondsSinceEpoch;
final data = utf8.decode(event);
data.replaceAll("\r", "")
.replaceAll("\b", "")
.trim()
.split("\n")
.forEach((entry) {
if(entry == "All OK") {
options.port.send(ArchiveDownloadProgress(100, 0, true));
return;
}
if(entry == "All OK") {
options.port.send(FortniteBuildDownloadProgress(100, 0, true));
return;
}
var element = _rarProgressRegex.firstMatch(entry)?.group(1);
if(element == null) {
return;
}
final element = _rarProgressRegex.firstMatch(entry)?.group(1);
if(element == null) {
return;
}
var percentage = int.parse(element);
if(percentage == 0) {
options.port.send(ArchiveDownloadProgress(percentage.toDouble(), null, true));
return;
}
_onProgress(startTime, now, percentage, options);
});
final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options);
});
});
process.stderr.listen((event) {
var data = utf8.decode(event);
final data = utf8.decode(event);
options.port.send(data);
});
break;
@@ -171,17 +290,22 @@ Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFil
await Future.any([stopped.future, process.exitCode]);
}
void _onProgress(int startTime, int now, int percentage, ArchiveDownloadOptions options) {
var msLeft = startTime + (now - startTime) * 100 / percentage - now;
var minutesLeft = (msLeft / 1000 / 60).round();
options.port.send(ArchiveDownloadProgress(percentage.toDouble(), minutesLeft, true));
void _onProgress(int startTime, int now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
if(percentage == 0) {
options.port.send(FortniteBuildDownloadProgress(percentage, null, extracting));
return;
}
final msLeft = startTime + (now - startTime) * 100 / percentage - now;
final minutesLeft = (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(percentage, minutesLeft, extracting));
}
Completer<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == "kill") {
if(message == kStopBuildDownloadSignal && !stopped.isCompleted) {
stopped.complete();
}
});

View File

@@ -0,0 +1,64 @@
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';
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";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootDllFile.exists();
final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
}
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 output = File(outputPath);
await output.parent.create(recursive: true);
await output.writeAsBytes(response.bodyBytes);
}
Future<int> downloadRebootDll(String url) async {
Directory? outputDir;
final now = DateTime.now();
try {
final response = await http.get(Uri.parse(url));
outputDir = await installationDirectory.createTemp("reboot_out");
final tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
return now.millisecondsSinceEpoch;
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}
Stream<String> watchDlls() async* {
if(_watcher) {
return;
}
_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;
}
}
}

View File

@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
@@ -75,21 +74,16 @@ Future<void> writeMatchmakingIp(String text) async {
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
}
Future<bool> isMatchmakerPortFree() async => isPortFree(int.parse(kDefaultMatchmakerPort));
Future<bool> isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort.toString()) == null;
Future<bool> freeMatchmakerPort() async {
await Process.run(matchmakerKillExecutable.path, []);
var standardResult = await isMatchmakerPortFree();
await killProcessByPort(kDefaultMatchmakerPort);
final standardResult = await isMatchmakerPortFree();
if(standardResult) {
return true;
}
var elevatedResult = await runElevatedProcess(matchmakerKillExecutable.path, "");
if(!elevatedResult) {
return false;
}
return await isMatchmakerPortFree();
return false;
}
Future<Uri?> pingMatchmaker(String host, String port, [bool wss=false]) async {
@@ -124,7 +118,7 @@ Future<Uri?> pingMatchmaker(String host, String port, [bool wss=false]) async {
await socket.close();
return result ? uri : null;
}catch(_){
return wss || declaredScheme != null ? null : await pingMatchmaker(host, port, true);
return wss || declaredScheme != null || isLocalHost(host) ? null : await pingMatchmaker(host, port, true);
}
}

View File

@@ -1,22 +1,95 @@
import 'dart:io';
import 'dart:ffi';
import 'package:reboot_common/common.dart';
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');
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;
}
class _MIB_TCPTABLE_OWNER_PID extends Struct {
@Uint32()
external int dwNumEntries;
@Array(1)
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";
Future<bool> isPortFree(int port) async {
try {
final server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
await server.close();
return true;
} catch (e) {
return false;
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) {
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;
}
Future<void> resetWinNat() async {
var binary = File("${assetsDirectory.path}\\misc\\winnat.bat");
await runElevatedProcess(binary.path, "");
}
int _htons(int port) => ((port & 0xFF) << 8) | ((port >> 8) & 0xFF);

View File

@@ -1,8 +1,8 @@
import 'dart:io';
import 'dart:isolate';
import 'package:reboot_common/common.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent;

View File

@@ -4,10 +4,13 @@ import 'dart:async';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:ffi/ffi.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<
@@ -84,23 +87,23 @@ Future<void> injectDll(int pid, String dll) async {
}
}
Future<bool> runElevatedProcess(String executable, String args) async {
var shellInput = calloc<SHELLEXECUTEINFO>();
bool runElevatedProcess(String executable, String args) {
final shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput);
return shellResult == 1;
final result = ShellExecuteEx(shellInput) == 1;
free(shellInput);
return result;
}
void _startBackgroundProcess(_BackgroundProcessParameters params) {
var args = params.args;
var concatenatedArgs = args == null ? "" : " ${args.map((entry) => '"$entry"').join(" ")}";
var executablePath = TEXT("${params.executable.path}$concatenatedArgs");
var executablePath = TEXT('cmd.exe /k "${params.executable.path}"$concatenatedArgs');
var startupInfo = calloc<STARTUPINFO>();
var processInfo = calloc<PROCESS_INFORMATION>();
var windowFlag = params.window ? CREATE_NEW_CONSOLE : CREATE_NO_WINDOW;
@@ -203,4 +206,57 @@ Future<bool> watchProcess(int pid) async {
var result = await completer.future;
isolate.kill(priority: Isolate.immediate);
return result;
}
List<String> createRebootArgs(String username, String password, bool host, bool headless, String additionalArgs) {
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
var args = [
"-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(host && headless){
args.addAll([
"-nullrhi",
"-nosplash",
"-nosound",
]);
}
if(additionalArgs.isNotEmpty){
args.addAll(additionalArgs.split(" "));
}
return args;
}
String _parseUsername(String username, bool host) {
if(host) {
return "Player${Random().nextInt(1000)}";
}
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}

View File

@@ -1,100 +0,0 @@
import 'dart:io';
import 'dart:math';
import 'package:archive/archive_io.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
const String rebootDownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/main/Release.zip";
final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
List<String> createRebootArgs(String username, String password, bool host, String additionalArgs) {
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
var args = [
"-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(host){
args.addAll([
"-nullrhi",
"-nosplash",
"-nosound",
]);
}
if(additionalArgs.isNotEmpty){
args.addAll(additionalArgs.split(" "));
}
return args;
}
String _parseUsername(String username, bool host) {
if(host) {
return "Player${Random().nextInt(1000)}";
}
if (username.isEmpty) {
return kDefaultPlayerName;
}
username = username.replaceAll(RegExp("[^A-Za-z0-9]"), "").trim();
if(username.isEmpty){
return kDefaultPlayerName;
}
return username;
}
Future<int> downloadRebootDll(String url, int? lastUpdateMs, {int hours = 24, bool force = false}) async {
Directory? outputDir;
var now = DateTime.now();
try {
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await rebootDllFile.exists();
if (!force && lastUpdate != null && now.difference(lastUpdate).inHours <= hours && exists) {
return lastUpdateMs!;
}
var response = await http.get(Uri.parse(rebootDownloadUrl));
outputDir = await installationDirectory.createTemp("reboot_out");
var tempZip = File("${outputDir.path}\\reboot.zip");
await tempZip.writeAsBytes(response.bodyBytes);
await extractFileToDisk(tempZip.path, outputDir.path);
var rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
if (!exists || sha1.convert(await rebootDllFile.readAsBytes()) != sha1.convert(await rebootDll.readAsBytes())) {
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes());
}
return now.millisecondsSinceEpoch;
} finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}