Final version

This commit is contained in:
Alessandro Autiero
2023-09-21 16:48:31 +02:00
parent 4bba21c038
commit 73c1cc8526
90 changed files with 3204 additions and 2608 deletions

View File

@@ -1,22 +1,29 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
import 'package:dio/dio.dart';
final Uri _manifestSourceUrl = Uri.parse(
"https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md");
final Dio _dio = Dio();
final String _manifestSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
var response = await http.get(_manifestSourceUrl);
var response = await _dio.get<String>(
_manifestSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var results = <FortniteBuild>[];
for (var line in response.body.split("\n")) {
for (var line in response.data?.split("\n") ?? []) {
if(!line.startsWith("|")) {
continue;
}
@@ -44,77 +51,118 @@ Future<void> downloadArchiveBuild(ArchiveDownloadOptions options) async {
var stopped = _setupLifecycle(options);
var outputDir = Directory("${options.destination.path}\\.build");
outputDir.createSync(recursive: true);
try {
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);
}
await _download(options, tempFile, stopped);
await _extract(stopped, extension, tempFile, options);
delete(outputDir);
} catch(message) {
throw Exception("Cannot download build: $message");
}
}
Future<void> _download(ArchiveDownloadOptions options, File tempFile, Completer<dynamic> stopped) async {
var client = http.Client();
var request = http.Request("GET", Uri.parse(options.archiveUrl));
request.headers['Connection'] = 'Keep-Alive';
var response = await client.send(request);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
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);
}
var startTime = DateTime.now().millisecondsSinceEpoch;
var length = response.contentLength!;
var received = 0;
var sink = tempFile.openWrite();
var subscription = response.stream.listen((data) async {
received += data.length;
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));
sink.add(data);
});
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}");
}
await Future.any([stopped.future, subscription.asFuture()]);
if(stopped.isCompleted) {
await subscription.cancel();
}else {
await sink.flush();
await sink.close();
await sink.done;
await _extract(stopped, extension, tempFile, options);
}
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 {
if(stopped.isCompleted) {
return;
}
options.port.send(ArchiveDownloadProgress(0, -1, true));
var startTime = DateTime.now().millisecondsSinceEpoch;
Process? process;
switch (extension.toLowerCase()) {
case '.zip':
case ".zip":
process = await Process.start(
'tar',
['-xf', tempFile.path, '-C', options.destination.path],
mode: ProcessStartMode.inheritStdio
"${assetsDirectory.path}\\build\\7zip.exe",
["a", "-bsp1", '-o"${options.destination.path}"', tempFile.path]
);
process.stdout.listen((bytes) {
var now = DateTime.now().millisecondsSinceEpoch;
var data = utf8.decode(bytes);
if(data == "Everything is Ok") {
options.port.send(ArchiveDownloadProgress(100, 0, true));
return;
}
var 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);
});
break;
case '.rar':
case ".rar":
process = await Process.start(
'${assetsDirectory.path}\\build\\winrar.exe',
['x', tempFile.path, '*.*', options.destination.path],
mode: ProcessStartMode.inheritStdio
"${assetsDirectory.path}\\build\\winrar.exe",
["x", "-o+", tempFile.path, "*.*", options.destination.path]
);
process.stdout.listen((event) {
var now = DateTime.now().millisecondsSinceEpoch;
var 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;
}
var 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);
});
});
process.stderr.listen((event) {
var data = utf8.decode(event);
options.port.send(data);
});
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
@@ -123,6 +171,12 @@ 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));
}
Completer<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
@@ -133,20 +187,4 @@ Completer<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
});
options.port.send(lifecyclePort.sendPort);
return stopped;
}
class ArchiveDownloadOptions {
String archiveUrl;
Directory destination;
SendPort port;
ArchiveDownloadOptions(this.archiveUrl, this.destination, this.port);
}
class ArchiveDownloadProgress {
final double progress;
final int minutesLeft;
final bool extracting;
ArchiveDownloadProgress(this.progress, this.minutesLeft, this.extracting);
}

View File

@@ -13,10 +13,9 @@ final File rebootDllFile = File("${assetsDirectory.path}\\dlls\\reboot.dll");
List<String> createRebootArgs(String username, String password, bool host, String additionalArgs) {
if(password.isEmpty) {
username = username.isEmpty ? kDefaultPlayerName : username;
username = host ? "$username${Random().nextInt(1000)}" : username;
username = '$username@projectreboot.dev';
username = '${_parseUsername(username, host)}@projectreboot.dev';
}
password = password.isNotEmpty ? password : "Rebooted";
var args = [
"-epicapp=Fortnite",
@@ -48,6 +47,23 @@ List<String> createRebootArgs(String username, String password, bool host, Strin
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;