<feat: New project structure>

<feat: New release>
This commit is contained in:
Alessandro Autiero
2023-09-02 15:34:15 +02:00
parent 64b33102f4
commit b41e22adeb
953 changed files with 1373072 additions and 0 deletions

23
common/lib/common.dart Normal file
View File

@@ -0,0 +1,23 @@
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/fortnite_build.dart';
export 'package:reboot_common/src/model/fortnite_version.dart';
export 'package:reboot_common/src/model/game_instance.dart';
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/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

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

View File

@@ -0,0 +1,13 @@
const String kDefaultPlayerName = "Player";
const String shutdownLine = "FOnlineSubsystemGoogleCommon::Shutdown()";
const List<String> corruptedBuildErrors = [
"when 0 bytes remain",
"Pak chunk signature verification failed!"
];
const List<String> cannotConnectErrors = [
"port 3551 failed: Connection refused",
"Unable to login to Fortnite servers",
"HTTP 400 response from ",
"Network failure when attempting to check platform restrictions",
"UOnlineAccountCommon::ForceLogout"
];

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
class FortniteBuild {
final String version;
final String link;
FortniteBuild({required this.version, required this.link});
}

View File

@@ -0,0 +1,17 @@
import 'dart:io';
class FortniteVersion {
String name;
Directory location;
FortniteVersion.fromJson(json)
: name = json["name"],
location = Directory(json["location"]);
FortniteVersion({required this.name, required this.location});
Map<String, dynamic> toJson() => {
'name': name,
'location': location.path
};
}

View File

@@ -0,0 +1,48 @@
import 'dart:io';
class GameInstance {
final int gamePid;
final int? launcherPid;
final int? eacPid;
int? watchPid;
bool hosting;
bool tokenError;
bool linkedHosting;
GameInstance(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(Map<String, dynamic>? json) :
gamePid = json?["game"] ?? -1,
launcherPid = json?["launcher"],
eacPid = json?["eac"],
watchPid = json?["watchPid"],
hosting = json?["hosting"] ?? false,
tokenError = json?["tokenError"] ?? false,
linkedHosting = json?["linkedHosting"] ?? false;
void kill() {
Process.killPid(gamePid, ProcessSignal.sigabrt);
if(launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
}
if(eacPid != null) {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
}
if(watchPid != null) {
Process.killPid(watchPid!, ProcessSignal.sigabrt);
}
}
Map<String, dynamic> toJson() => {
'game': gamePid,
'launcher': launcherPid,
'eac': eacPid,
'watch': watchPid,
'hosting': hosting,
'tokenError': tokenError,
'linkedHosting': linkedHosting
};
}

View File

@@ -0,0 +1,23 @@
class ServerResult {
final ServerResultType type;
final Object? error;
final StackTrace? stackTrace;
ServerResult(this.type, {this.error, this.stackTrace});
}
enum ServerResultType {
missingHostError,
missingPortError,
illegalPortError,
freeingPort,
freePortSuccess,
freePortError,
pingingRemote,
pingingLocal,
pingError,
startSuccess,
startError;
bool get isError => name.contains("Error");
}

View File

@@ -0,0 +1,5 @@
enum ServerType {
embedded,
remote,
local
}

View File

@@ -0,0 +1,6 @@
enum UpdateStatus {
waiting,
started,
success,
error
}

View File

@@ -0,0 +1,6 @@
enum UpdateTimer {
never,
hour,
day,
week
}

View File

@@ -0,0 +1,79 @@
import 'dart:convert';
import 'dart:io';
import 'package:process_run/process_run.dart';
import 'package:reboot_common/common.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
final authenticatorLogFile = File("${logsDirectory.path}\\authenticator.log");
final authenticatorDirectory = Directory("${assetsDirectory.path}\\lawin");
final authenticatorExecutable = File("${authenticatorDirectory.path}\\run.bat");
Future<Process> startEmbeddedAuthenticator(bool detached) async {
if(!authenticatorExecutable.existsSync()) {
throw StateError("${authenticatorExecutable.path} doesn't exist");
}
var process = await Process.start(
authenticatorExecutable.path,
[],
workingDirectory: authenticatorDirectory.path,
mode: detached ? ProcessStartMode.detached : ProcessStartMode.normal
);
if(!detached) {
authenticatorLogFile.createSync(recursive: true);
process.outLines.forEach((element) => authenticatorLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
process.errLines.forEach((element) => authenticatorLogFile.writeAsStringSync("$element\n", mode: FileMode.append));
}
return process;
}
Future<HttpServer> startRemoteAuthenticatorProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultAuthenticatorHost, int.parse(kDefaultAuthenticatorPort));
Future<bool> isAuthenticatorPortFree() async => isPortFree(int.parse(kDefaultAuthenticatorPort));
Future<bool> freeAuthenticatorPort() async {
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_lawin.bat");
await Process.run(releaseBat.path, []);
var standardResult = await isAuthenticatorPortFree();
if(standardResult) {
return true;
}
var elevatedResult = await runElevatedProcess(releaseBat.path, "");
if(!elevatedResult) {
return false;
}
return await isAuthenticatorPortFree();
}
Future<Uri?> pingSelf(String port) async => ping(kDefaultAuthenticatorHost, port);
Future<Uri?> ping(String host, String 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: int.parse(port),
path: "unknown"
);
var client = HttpClient()
..connectionTimeout = const Duration(seconds: 5);
var request = await client.getUrl(uri);
var response = await request.close();
var body = utf8.decode(await response.single);
return body.contains("epicgames") || body.contains("lawinserver") ? uri : null;
}catch(_){
return https || declaredScheme != null ? null : await ping(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;

View File

@@ -0,0 +1,152 @@
import 'dart:async';
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';
final Uri _manifestSourceUrl = Uri.parse(
"https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md");
Future<List<FortniteBuild>> fetchBuilds(ignored) async {
var response = await http.get(_manifestSourceUrl);
if (response.statusCode != 200) {
throw Exception("Erroneous status code: ${response.statusCode}");
}
var results = <FortniteBuild>[];
for (var line in response.body.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("-"));
results.add(FortniteBuild(version: "Fortnite $version", link: link));
}
return results;
}
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}");
}
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);
});
await Future.any([stopped.future, subscription.asFuture()]);
if(stopped.isCompleted) {
await subscription.cancel();
}else {
await sink.flush();
await sink.close();
await sink.done;
}
}
Future<void> _extract(Completer<dynamic> stopped, String extension, File tempFile, ArchiveDownloadOptions options) async {
if(stopped.isCompleted) {
return;
}
options.port.send(ArchiveDownloadProgress(0, -1, true));
Process? process;
switch (extension.toLowerCase()) {
case '.zip':
process = await Process.start(
'tar',
['-xf', tempFile.path, '-C', options.destination.path],
mode: ProcessStartMode.inheritStdio
);
break;
case '.rar':
process = await Process.start(
'${assetsDirectory.path}\\misc\\winrar.exe',
['x', tempFile.path, '*.*', options.destination.path],
mode: ProcessStartMode.inheritStdio
);
break;
default:
throw ArgumentError("Unexpected file extension: $extension}");
}
await Future.any([stopped.future, process.exitCode]);
}
Completer<dynamic> _setupLifecycle(ArchiveDownloadOptions options) {
var stopped = Completer();
var lifecyclePort = ReceivePort();
lifecyclePort.listen((message) {
if(message == "kill") {
stopped.complete();
}
});
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

@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:ini/ini.dart';
import 'package:reboot_common/common.dart';
Future<void> writeMatchmakingIp(String text) async {
var file = File("${assetsDirectory.path}\\lawin\\Config\\config.ini");
if(!file.existsSync()){
return;
}
var splitIndex = text.indexOf(":");
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : "7777";
var config = Config.fromString(file.readAsStringSync());
config.set("GameServer", "ip", ip);
config.set("GameServer", "port", port);
file.writeAsStringSync(config.toString());
}
Future<bool> isMatchmakerPortFree() async => isPortFree(int.parse(kDefaultMatchmakerPort));
Future<bool> freeMatchmakerPort() async {
var releaseBat = File("${assetsDirectory.path}\\lawin\\kill_matchmaker.bat");
await Process.run(releaseBat.path, []);
var standardResult = await isMatchmakerPortFree();
if(standardResult) {
return true;
}
var elevatedResult = await runElevatedProcess(releaseBat.path, "");
if(!elevatedResult) {
return false;
}
return await isMatchmakerPortFree();
}

View File

@@ -0,0 +1,22 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
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;
}
}
Future<void> resetWinNat() async {
var binary = File("${authenticatorDirectory.path}\\winnat.bat");
await runElevatedProcess(binary.path, "");
}

View File

@@ -0,0 +1,58 @@
import 'dart:io';
import 'dart:typed_data';
final Uint8List _originalHeadless = Uint8List.fromList([
45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 115, 0, 101, 0, 115, 0, 115, 0, 105, 0, 111, 0, 110, 0, 32, 0, 45, 0, 105, 0, 110, 0, 118, 0, 105, 0, 116, 0, 101, 0, 102, 0, 114, 0, 111, 0, 109, 0, 32, 0, 45, 0, 112, 0, 97, 0, 114, 0, 116, 0, 121, 0, 95, 0, 106, 0, 111, 0, 105, 0, 110, 0, 105, 0, 110, 0, 102, 0, 111, 0, 95, 0, 116, 0, 111, 0, 107, 0, 101, 0, 110, 0, 32, 0, 45, 0, 114, 0, 101, 0, 112, 0, 108, 0, 97, 0, 121, 0
]);
final Uint8List _patchedHeadless = Uint8List.fromList([
45, 0, 108, 0, 111, 0, 103, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 112, 0, 108, 0, 97, 0, 115, 0, 104, 0, 32, 0, 45, 0, 110, 0, 111, 0, 115, 0, 111, 0, 117, 0, 110, 0, 100, 0, 32, 0, 45, 0, 110, 0, 117, 0, 108, 0, 108, 0, 114, 0, 104, 0, 105, 0, 32, 0, 45, 0, 117, 0, 115, 0, 101, 0, 111, 0, 108, 0, 100, 0, 105, 0, 116, 0, 101, 0, 109, 0, 99, 0, 97, 0, 114, 0, 100, 0, 115, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0
]);
final Uint8List _originalMatchmaking = Uint8List.fromList([
63, 0, 69, 0, 110, 0, 99, 0, 114, 0, 121, 0, 112, 0, 116, 0, 105, 0, 111, 0, 110, 0, 84, 0, 111, 0, 107, 0, 101, 0, 110, 0, 61
]);
final Uint8List _patchedMatchmaking = Uint8List.fromList([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]);
Future<bool> patchHeadless(File file) async =>
_patch(file, _originalHeadless, _patchedHeadless);
Future<bool> patchMatchmaking(File file) async =>
await _patch(file, _originalMatchmaking, _patchedMatchmaking);
Future<bool> _patch(File file, Uint8List original, Uint8List patched) async {
try {
if(original.length != patched.length){
throw Exception("Cannot mutate length of binary file");
}
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];
}
await file.writeAsBytes(read, mode: FileMode.write);
return true;
}
}
return false;
}catch(_){
return false;
}
}

View File

@@ -0,0 +1,80 @@
import 'dart:io';
import 'dart:isolate';
import 'package:reboot_common/common.dart';
import 'package:path/path.dart' as path;
Directory get installationDirectory =>
File(Platform.resolvedExecutable).parent;
Directory get assetsDirectory {
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}
return installationDirectory;
}
Directory get logsDirectory =>
Directory("${installationDirectory.path}\\logs");
Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings");
Directory get tempDirectory =>
Directory(Platform.environment["Temp"]!);
Future<bool> delete(FileSystemEntity file) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return Future.delayed(const Duration(seconds: 5)).then((value) async {
try {
await file.delete(recursive: true);
return true;
}catch(_){
return false;
}
});
}
}
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;
}
}
Future<File?> get executable async {
var result = findExecutable(location, "FortniteClient-Win64-Shipping-Reboot.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-Reboot.exe");
await original.copy(output.path);
await Future.wait([
Isolate.run(() => patchMatchmaking(output)),
Isolate.run(() => patchHeadless(output)),
]);
return output;
}
File? get launcher => findExecutable(location, "FortniteLauncher.exe");
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
File? get splashBitmap => findExecutable(location, "Splash.bmp");
}

View File

@@ -0,0 +1,176 @@
// ignore_for_file: non_constant_identifier_names
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll');
final _kernel32 = DynamicLibrary.open('kernel32.dll');
final _CreateRemoteThread = _kernel32.lookupFunction<
IntPtr Function(
IntPtr hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
IntPtr dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
Uint32 dwCreationFlags,
Pointer<Uint32> lpThreadId),
int Function(
int hProcess,
Pointer<SECURITY_ATTRIBUTES> lpThreadAttributes,
int dwStackSize,
Pointer loadLibraryAddress,
Pointer lpParameter,
int dwCreationFlags,
Pointer<Uint32> lpThreadId)>('CreateRemoteThread');
Future<void> injectDll(int pid, String dll) async {
var process = OpenProcess(
0x43A,
0,
pid
);
var processAddress = GetProcAddress(
GetModuleHandle("KERNEL32".toNativeUtf16()),
"LoadLibraryA".toNativeUtf8()
);
if (processAddress == nullptr) {
throw Exception("Cannot get process address for pid $pid");
}
var dllAddress = VirtualAllocEx(
process,
nullptr,
dll.length + 1,
0x3000,
0x4
);
var writeMemoryResult = WriteProcessMemory(
process,
dllAddress,
dll.toNativeUtf8(),
dll.length,
nullptr
);
if (writeMemoryResult != 1) {
throw Exception("Memory write failed");
}
var createThreadResult = _CreateRemoteThread(
process,
nullptr,
0,
processAddress,
dllAddress,
0,
nullptr
);
if (createThreadResult == -1) {
throw Exception("Thread creation failed");
}
var closeResult = CloseHandle(process);
if(closeResult != 1){
throw Exception("Cannot close handle");
}
}
Future<bool> runElevatedProcess(String executable, String args) async {
var 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;
}
int startBackgroundProcess(String executable, List<String> args) {
var executablePath = TEXT('$executable ${args.map((entry) => '"$entry"').join(" ")}');
var startupInfo = calloc<STARTUPINFO>();
var processInfo = calloc<PROCESS_INFORMATION>();
var success = CreateProcess(
nullptr,
executablePath,
nullptr,
nullptr,
FALSE,
CREATE_NO_WINDOW,
nullptr,
nullptr,
startupInfo,
processInfo
);
if (success == 0) {
var error = GetLastError();
throw Exception("Cannot start process: $error");
}
var pid = processInfo.ref.dwProcessId;
free(startupInfo);
free(processInfo);
return pid;
}
int _NtResumeProcess(int hWnd) {
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtResumeProcess');
return function(hWnd);
}
int _NtSuspendProcess(int hWnd) {
final function = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess');
return function(hWnd);
}
bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtSuspendProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtResumeProcess(processHandle);
CloseHandle(processHandle);
return result == 0;
}
void _watchProcess(int pid) {
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid);
WaitForSingleObject(processHandle, INFINITE);
CloseHandle(processHandle);
}
Future<bool> watchProcess(int pid) async {
var completer = Completer<bool>();
var exitPort = ReceivePort();
exitPort.listen((_) {
if(!completer.isCompleted) {
completer.complete(true);
}
});
var errorPort = ReceivePort();
errorPort.listen((_) => completer.complete(false));
await Isolate.spawn(
_watchProcess,
pid,
onExit: exitPort.sendPort,
onError: errorPort.sendPort,
errorsAreFatal: true
);
return completer.future;
}

View File

@@ -0,0 +1,92 @@
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 = username.isEmpty ? kDefaultPlayerName : username;
username = host ? "$username${Random().nextInt(1000)}" : username;
username = '$username@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;
}
Future<int> downloadRebootDll(String url, int? lastUpdateMs) async {
Directory? outputDir;
var now = DateTime.now();
try {
var lastUpdate = await _getLastUpdate(lastUpdateMs);
var exists = await rebootDllFile.exists();
if (lastUpdate != null && now.difference(lastUpdate).inHours <= 24 && 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;
}catch(message) {
if(url == rebootDownloadUrl){
var asset = File('${assetsDirectory.path}\\dlls\\reboot.dll');
await rebootDllFile.writeAsBytes(asset.readAsBytesSync());
return now.millisecondsSinceEpoch;
}
throw Exception("Cannot download reboot.zip, invalid zip: $message");
}finally{
if(outputDir != null) {
delete(outputDir);
}
}
}
Future<DateTime?> _getLastUpdate(int? lastUpdateMs) async {
return lastUpdateMs != null
? DateTime.fromMillisecondsSinceEpoch(lastUpdateMs)
: null;
}

21
common/pubspec.yaml Normal file
View File

@@ -0,0 +1,21 @@
name: reboot_common
version: "1.0.0"
publish_to: 'none'
environment:
sdk: ">=2.19.0 <=3.3.3"
dependencies:
win32: 3.0.0
ffi: ^2.1.0
path: ^1.8.3
http: ^1.1.0
crypto: ^3.0.2
archive: ^3.3.7
ini: ^2.1.0
shelf_proxy: ^1.0.2
process_run: ^0.13.1
dev_dependencies:
flutter_lints: ^2.0.1