mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 19:22:22 +01:00
9.0.8
This commit is contained in:
@@ -20,3 +20,4 @@ const List<String> kCannotConnectErrors = [
|
||||
"Network failure when attempting to check platform restrictions",
|
||||
"UOnlineAccountCommon::ForceLogout"
|
||||
];
|
||||
const String kGameFinishedLine = "PlayersLeft: 1";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
const String kDefaultMatchmakerHost = "127.0.0.1";
|
||||
const int kDefaultMatchmakerPort = 8080;
|
||||
42
common/lib/src/extension/path.dart
Normal file
42
common/lib/src/extension/path.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:reboot_common/common.dart';
|
||||
|
||||
extension FortniteVersionExtension on FortniteVersion {
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
final result = directory.listSync(recursive: true)
|
||||
.firstWhere((element) => path.basename(element.path) == name);
|
||||
return File(result.path);
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File? get gameExecutable => findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
|
||||
Future<File?> get headlessGameExecutable async {
|
||||
final result = findExecutable(location, "FortniteClient-Win64-Shipping-Headless.exe");
|
||||
if(result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(original == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
||||
await original.copy(output.path);
|
||||
await Isolate.run(() => patchHeadless(output));
|
||||
return output;
|
||||
}
|
||||
|
||||
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
||||
|
||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||
}
|
||||
8
common/lib/src/extension/process.dart
Normal file
8
common/lib/src/extension/process.dart
Normal file
@@ -0,0 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
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"));
|
||||
}
|
||||
15
common/lib/src/extension/types.dart
Normal file
15
common/lib/src/extension/types.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
extension StringExtension on String {
|
||||
bool get isBlank {
|
||||
if(isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for(var char in this.split("")) {
|
||||
if(char != " ") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,12 @@ class FortniteBuild {
|
||||
final String identifier;
|
||||
final String version;
|
||||
final String link;
|
||||
final FortniteBuildSource source;
|
||||
|
||||
FortniteBuild({required this.identifier, required this.version, required this.link, required this.source});
|
||||
}
|
||||
|
||||
enum FortniteBuildSource {
|
||||
manifest,
|
||||
archive
|
||||
FortniteBuild({
|
||||
required this.identifier,
|
||||
required this.version,
|
||||
required this.link
|
||||
});
|
||||
}
|
||||
|
||||
class FortniteBuildDownloadProgress {
|
||||
|
||||
@@ -8,6 +8,7 @@ class GameInstance {
|
||||
final int? eacPid;
|
||||
bool hosting;
|
||||
bool launched;
|
||||
bool movedToVirtualDesktop;
|
||||
bool tokenError;
|
||||
GameInstance? child;
|
||||
|
||||
@@ -18,7 +19,7 @@ class GameInstance {
|
||||
required this.eacPid,
|
||||
required this.hosting,
|
||||
required this.child
|
||||
}): tokenError = false, launched = false;
|
||||
}): tokenError = false, launched = false, movedToVirtualDesktop = false;
|
||||
|
||||
void kill() {
|
||||
Process.killPid(gamePid, ProcessSignal.sigabrt);
|
||||
@@ -42,4 +43,4 @@ class GameInstance {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ini/ini.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_common/src/extension/types.dart';
|
||||
import 'package:shelf/shelf_io.dart';
|
||||
import 'package:shelf_proxy/shelf_proxy.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
@@ -33,8 +35,8 @@ Future<bool> freeBackendPort() async {
|
||||
}
|
||||
|
||||
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||
var hostName = _getHostName(host);
|
||||
var declaredScheme = _getScheme(host);
|
||||
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
|
||||
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
|
||||
try{
|
||||
var uri = Uri(
|
||||
scheme: declaredScheme ?? (https ? "https" : "http"),
|
||||
@@ -52,10 +54,6 @@ Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -110,56 +108,4 @@ Future<void> writeMatchmakingIp(String text) async {
|
||||
config.set("GameServer", "ip", ip);
|
||||
config.set("GameServer", "port", port);
|
||||
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);
|
||||
}
|
||||
|
||||
Future<bool> isMatchmakerPortFree() async => await pingMatchmaker(kDefaultMatchmakerHost, kDefaultMatchmakerPort) == null;
|
||||
|
||||
Future<bool> freeMatchmakerPort() async {
|
||||
await killProcessByPort(kDefaultMatchmakerPort);
|
||||
final standardResult = await isMatchmakerPortFree();
|
||||
if(standardResult) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<Uri?> pingMatchmaker(String host, int port, [bool wss=false]) async {
|
||||
var hostName = _getHostName(host);
|
||||
var declaredScheme = _getScheme(host);
|
||||
try{
|
||||
var uri = Uri(
|
||||
scheme: declaredScheme ?? (wss ? "wss" : "ws"),
|
||||
host: hostName,
|
||||
port: port
|
||||
);
|
||||
var completer = Completer<bool>();
|
||||
var socket = await WebSocket.connect(uri.toString());
|
||||
socket.listen(
|
||||
(data) {
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(true);
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if(!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
var result = await completer.future;
|
||||
await socket.close();
|
||||
return result ? uri : null;
|
||||
}catch(_){
|
||||
return wss || declaredScheme != null || isLocalHost(host) ? null : await pingMatchmaker(host, port, true);
|
||||
}
|
||||
}
|
||||
|
||||
String? _getHostName(String host) => host.replaceFirst("ws://", "").replaceFirst("wss://", "");
|
||||
|
||||
String? _getScheme(String host) => host.startsWith("ws://") ? "ws" : host.startsWith("wss://") ? "wss" : null;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:dio/dio.dart';
|
||||
import 'package:dio/io.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_common/src/extension/types.dart';
|
||||
|
||||
const String kStopBuildDownloadSignal = "kill";
|
||||
|
||||
@@ -23,61 +24,14 @@ Dio _buildDioInstance() {
|
||||
return dio;
|
||||
}
|
||||
|
||||
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
|
||||
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
|
||||
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
|
||||
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 = () =>
|
||||
HttpClient()
|
||||
..badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true;
|
||||
|
||||
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",
|
||||
options: Options(
|
||||
headers: {
|
||||
"Accept-Encoding": "*",
|
||||
"Cookie": "_c_t_c=1"
|
||||
}
|
||||
)
|
||||
);
|
||||
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(
|
||||
@@ -88,32 +42,15 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
|
||||
return [];
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.data ?? "{}");
|
||||
var results = <FortniteBuild>[];
|
||||
for (var line in response.data?.split("\n") ?? []) {
|
||||
if(!line.startsWith("|")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var parts = line.substring(1, line.length - 1).split("|");
|
||||
if(parts.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var link = parts.last.trim();
|
||||
if(!link.endsWith(".zip") && !link.endsWith(".rar")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var version = parts.first.trim();
|
||||
version = version.substring(0, version.indexOf("-"));
|
||||
for(final entry in data.entries) {
|
||||
results.add(FortniteBuild(
|
||||
identifier: version,
|
||||
version: "Fortnite $version",
|
||||
link: link,
|
||||
source: FortniteBuildSource.archive
|
||||
identifier: entry.key,
|
||||
version: "${entry.value["title"]} (${entry.key})",
|
||||
link: entry.value["url"]
|
||||
));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -121,107 +58,23 @@ Future<List<FortniteBuild>> _fetchArchiveBuilds() async {
|
||||
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);
|
||||
}
|
||||
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final response = _downloadArchive(options, tempFile, startTime);
|
||||
await Future.any([stopped.future, response]);
|
||||
if(!stopped.isCompleted) {
|
||||
await _extractArchive(stopped, extension, tempFile, options);
|
||||
}
|
||||
|
||||
delete(outputDir);
|
||||
break;
|
||||
case FortniteBuildSource.manifest:
|
||||
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;
|
||||
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",
|
||||
"Cookie": "_c_t_c=1"
|
||||
}
|
||||
),
|
||||
);
|
||||
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)]);
|
||||
break;
|
||||
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);
|
||||
}
|
||||
|
||||
final startTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final response = _downloadArchive(options, tempFile, startTime);
|
||||
await Future.any([stopped.future, response]);
|
||||
if(!stopped.isCompleted) {
|
||||
await _extractArchive(stopped, extension, tempFile, options);
|
||||
}
|
||||
|
||||
delete(outputDir);
|
||||
}catch(error) {
|
||||
_onError(error, options);
|
||||
}
|
||||
@@ -358,7 +211,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
|
||||
throw ArgumentError("Unexpected file extension: $extension}");
|
||||
}
|
||||
|
||||
await Future.any([stopped.future, watchProcess(process.pid)]);
|
||||
await Future.any([stopped.future, process.exitCode]);
|
||||
}
|
||||
|
||||
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) {
|
||||
@@ -385,7 +238,6 @@ void _onError(Object? error, FortniteBuildDownloadOptions options) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
|
||||
var stopped = Completer();
|
||||
var lifecyclePort = ReceivePort();
|
||||
|
||||
@@ -29,8 +29,8 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
throw Exception("Cannot mutate length of binary file");
|
||||
}
|
||||
|
||||
var read = await file.readAsBytes();
|
||||
var length = await file.length();
|
||||
final read = await file.readAsBytes();
|
||||
final length = await file.length();
|
||||
var readOffset = 0;
|
||||
var patchOffset = -1;
|
||||
var patchCount = 0;
|
||||
@@ -50,7 +50,6 @@ Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
|
||||
readOffset++;
|
||||
}
|
||||
|
||||
print("Offset: $patchOffset");
|
||||
if(patchOffset == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -39,41 +39,4 @@ Future<bool> delete(FileSystemEntity file) async {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension FortniteVersionExtension on FortniteVersion {
|
||||
static File? findExecutable(Directory directory, String name) {
|
||||
try{
|
||||
var result = directory.listSync(recursive: true)
|
||||
.firstWhere((element) => path.basename(element.path) == name);
|
||||
return File(result.path);
|
||||
}catch(_){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var original = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
|
||||
if(original == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var output = File("${original.parent.path}\\FortniteClient-Win64-Shipping-Headless.exe");
|
||||
await original.copy(output.path);
|
||||
await Isolate.run(() => patchHeadless(output));
|
||||
return output;
|
||||
}
|
||||
|
||||
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
|
||||
|
||||
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
|
||||
|
||||
File? get splashBitmap => findExecutable(location, "Splash.bmp");
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import 'package:path/path.dart' as path;
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:reboot_common/common.dart';
|
||||
import 'package:reboot_common/src/extension/process.dart';
|
||||
import 'package:sync/semaphore.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
@@ -282,10 +283,4 @@ class _ExtendedProcess extends Process {
|
||||
|
||||
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