Release 9.2.0

This commit is contained in:
Alessandro Autiero
2024-07-06 18:43:52 +02:00
parent 45b8629207
commit e3b8d7d182
91 changed files with 3871 additions and 3132 deletions

View File

@@ -1,2 +1,3 @@
const String kDefaultBackendHost = "127.0.0.1";
const int kDefaultBackendPort = 3551;
const int kDefaultBackendPort = 3551;
const int kDefaultXmppPort = 80;

View File

@@ -5,9 +5,9 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart';
extension FortniteVersionExtension on FortniteVersion {
static DateTime _marker = DateTime.fromMicrosecondsSinceEpoch(0);
static String _marker = "FortniteClient.mod";
static File? findExecutable(Directory directory, String name) {
static File? findFile(Directory directory, String name) {
try{
final result = directory.listSync(recursive: true)
.firstWhere((element) => path.basename(element.path) == name);
@@ -18,39 +18,24 @@ extension FortniteVersionExtension on FortniteVersion {
}
Future<File?> get shippingExecutable async {
final result = findExecutable(location, "FortniteClient-Win64-Shipping.exe");
final result = findFile(location, "FortniteClient-Win64-Shipping.exe");
if(result == null) {
return null;
}
final lastModified = await _getLastModifiedTime(result);
if(lastModified != _marker) {
await Isolate.run(() => patchHeadless(result));
await _setLastModifiedTime(result);
final marker = findFile(location, _marker);
if(marker != null) {
return result;
}
await Isolate.run(() => patchHeadless(result));
await File("${location.path}\\$_marker").create();
return result;
}
Future<void> _setLastModifiedTime(File result) async {
try {
await result.setLastModified(_marker);
}catch(_) {
// Ignored
}
}
File? get launcherExecutable => findFile(location, "FortniteLauncher.exe");
Future<DateTime?> _getLastModifiedTime(File result) async {
try {
return await result.lastModified();
}catch(_) {
return null;
}
}
File? get eacExecutable => findFile(location, "FortniteClient-Win64-Shipping_EAC.exe");
File? get launcherExecutable => findExecutable(location, "FortniteLauncher.exe");
File? get eacExecutable => findExecutable(location, "FortniteClient-Win64-Shipping_EAC.exe");
File? get splashBitmap => findExecutable(location, "Splash.bmp");
File? get splashBitmap => findFile(location, "Splash.bmp");
}

View File

@@ -1,15 +1,17 @@
import 'dart:io';
import 'dart:isolate';
import 'package:version/version.dart';
class FortniteBuild {
final String identifier;
final String version;
final Version version;
final String link;
final bool available;
FortniteBuild({
required this.identifier,
required this.version,
required this.link
required this.link,
required this.available
});
}

View File

@@ -0,0 +1,47 @@
class FortniteServer {
final String id;
final String name;
final String description;
final String author;
final String ip;
final String version;
final String? password;
final DateTime timestamp;
final bool discoverable;
FortniteServer({
required this.id,
required this.name,
required this.description,
required this.author,
required this.ip,
required this.version,
required this.password,
required this.timestamp,
required this.discoverable
});
factory FortniteServer.fromJson(json) => FortniteServer(
id: json["id"],
name: json["name"],
description: json["description"],
author: json["author"],
ip: json["ip"],
version: json["version"],
password: json["password"],
timestamp: json.containsKey("json") ? DateTime.parse(json["timestamp"]) : DateTime.now(),
discoverable: json["discoverable"] ?? false
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"description": description,
"author": author,
"ip": ip,
"version": version,
"password": password,
"timestamp": timestamp.toString(),
"discoverable": discoverable
};
}

View File

@@ -1,17 +1,22 @@
import 'dart:io';
import 'package:version/version.dart';
class FortniteVersion {
String name;
Version content;
Directory location;
FortniteVersion.fromJson(json)
: name = json["name"],
: content = Version.parse(json["content"]),
location = Directory(json["location"]);
FortniteVersion({required this.name, required this.location});
FortniteVersion({required this.content, required this.location});
Map<String, dynamic> toJson() => {
'name': name,
'content': content.toString(),
'location': location.path
};
@override
bool operator ==(Object other) => other is FortniteVersion && this.content == other.content;
}

View File

@@ -13,6 +13,7 @@ class GameInstance {
bool launched;
bool movedToVirtualDesktop;
bool tokenError;
bool killed;
GameInstance? child;
GameInstance({
@@ -22,9 +23,19 @@ class GameInstance {
required this.eacPid,
required this.serverType,
required this.child
}): tokenError = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
}): tokenError = false, killed = false, launched = false, movedToVirtualDesktop = false, injectedDlls = [];
void kill() {
GameInstance? child = this;
while(child != null) {
child._kill();
child = child.child;
}
}
void _kill() {
launched = true;
killed = true;
Process.killPid(gamePid, ProcessSignal.sigabrt);
if(launcherPid != null) {
Process.killPid(launcherPid!, ProcessSignal.sigabrt);
@@ -33,19 +44,6 @@ class GameInstance {
Process.killPid(eacPid!, ProcessSignal.sigabrt);
}
}
bool get nestedHosting {
GameInstance? child = this;
while(child != null) {
if(child.serverType != null) {
return true;
}
child = child.child;
}
return false;
}
}
enum GameServerType {

View File

@@ -4,6 +4,11 @@ class ServerResult {
final StackTrace? stackTrace;
ServerResult(this.type, {this.error, this.stackTrace});
@override
String toString() {
return 'ServerResult{type: $type, error: $error, stackTrace: $stackTrace}';
}
}
enum ServerResultType {
@@ -21,7 +26,8 @@ enum ServerResultType {
freePortError,
pingingRemote,
pingingLocal,
pingError;
pingError,
processError;
bool get isError => name.contains("Error");

View File

@@ -26,6 +26,7 @@ Future<bool> isBackendPortFree() async => await pingBackend(kDefaultBackendHost,
Future<bool> freeBackendPort() async {
await killProcessByPort(kDefaultBackendPort);
await killProcessByPort(kDefaultXmppPort);
final standardResult = await isBackendPortFree();
if(standardResult) {
return true;
@@ -35,21 +36,24 @@ Future<bool> freeBackendPort() async {
}
Future<Uri?> pingBackend(String host, int port, [bool https=false]) async {
var hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
var declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
final hostName = host.replaceFirst("http://", "").replaceFirst("https://", "");
final declaredScheme = host.startsWith("http://") ? "http" : host.startsWith("https://") ? "https" : null;
try{
var uri = Uri(
final 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(_){
log("[BACKEND] Pinging $uri...");
final client = HttpClient()
..connectionTimeout = const Duration(seconds: 10);
final request = await client.getUrl(uri);
await request.close().timeout(const Duration(seconds: 10));
log("[BACKEND] Ping successful");
return uri;
}catch(error){
log("[BACKEND] Cannot ping backend: $error");
return https || declaredScheme != null || isLocalHost(host) ? null : await pingBackend(host, port, true);
}
}
@@ -59,16 +63,16 @@ Stream<String?> watchMatchmakingIp() async* {
return;
}
var observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
final observer = matchmakerConfigFile.parent.watch(events: FileSystemEvent.modify);
yield* observer.where((event) => event.path == matchmakerConfigFile.path).asyncMap((event) async {
try {
var config = Config.fromString(await matchmakerConfigFile.readAsString());
var ip = config.get("GameServer", "ip");
final config = Config.fromString(await matchmakerConfigFile.readAsString());
final ip = config.get("GameServer", "ip");
if(ip == null) {
return null;
}
var port = config.get("GameServer", "port");
final port = config.get("GameServer", "port");
if(port == null) {
return null;
}
@@ -89,14 +93,14 @@ Stream<String?> watchMatchmakingIp() async* {
}
Future<void> writeMatchmakingIp(String text) async {
var exists = await matchmakerConfigFile.exists();
final exists = await matchmakerConfigFile.exists();
if(!exists) {
return;
}
_semaphore.acquire();
var splitIndex = text.indexOf(":");
var ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
final splitIndex = text.indexOf(":");
final ip = splitIndex != -1 ? text.substring(0, splitIndex) : text;
var port = splitIndex != -1 ? text.substring(splitIndex + 1) : kDefaultGameServerPort;
if(port.isBlank) {
port = kDefaultGameServerPort;
@@ -104,7 +108,7 @@ Future<void> writeMatchmakingIp(String text) async {
_lastIp = ip;
_lastPort = port;
var config = Config.fromString(await matchmakerConfigFile.readAsString());
final config = Config.fromString(await matchmakerConfigFile.readAsString());
config.set("GameServer", "ip", ip);
config.set("GameServer", "port", port);
await matchmakerConfigFile.writeAsString(config.toString(), flush: true);

View File

@@ -8,6 +8,7 @@ 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';
import 'package:version/version.dart';
const String kStopBuildDownloadSignal = "kill";
@@ -23,7 +24,7 @@ Dio _buildDioInstance() {
return dio;
}
final String _archiveSourceUrl = "http://185.203.216.3/versions.json";
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$");
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";
@@ -41,15 +42,35 @@ Future<List<FortniteBuild>> fetchBuilds(ignored) async {
return [];
}
final data = jsonDecode(response.data ?? "{}");
var results = <FortniteBuild>[];
for(final entry in data.entries) {
results.add(FortniteBuild(
identifier: entry.key,
version: "${entry.value["title"]} (${entry.key})",
link: entry.value["url"]
));
for (final line in response.data?.split("\n") ?? []) {
if (!line.startsWith("|")) {
continue;
}
var parts = line.substring(1, line.length - 1).split("|");
if (parts.isEmpty) {
continue;
}
var versionName = parts.first.trim();
final separator = versionName.indexOf("-");
if(separator != -1) {
versionName = versionName.substring(0, separator);
}
final link = parts.last.trim();
try {
results.add(FortniteBuild(
version: Version.parse(versionName),
link: link,
available: link.endsWith(".zip") || link.endsWith(".rar")
));
} on FormatException {
// Ignore
}
}
return results;
}

View File

@@ -0,0 +1,29 @@
import 'dart:io';
import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1);
File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true);
if(file.existsSync()) {
file.deleteSync();
}
file.createSync();
return file;
}
void log(String message) async {
try {
await _semaphore.acquire();
print(message);
await launcherLogFile.writeAsString("$message\n", mode: FileMode.append, flush: true);
}catch(error) {
print("[LOGGER_ERROR] An error occurred while logging: $error");
}finally {
_semaphore.release();
}
}

View File

@@ -6,7 +6,7 @@ Directory get installationDirectory =>
Directory get dllsDirectory => Directory("${installationDirectory.path}\\dlls");
Directory get assetsDirectory {
var directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
final directory = Directory("${installationDirectory.path}\\data\\flutter_assets\\assets");
if(directory.existsSync()) {
return directory;
}

View File

@@ -104,7 +104,7 @@ Future<bool> startElevatedProcess({required String executable, required String a
return shellResult == 1;
}
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name}) async {
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
final argsOrEmpty = args ?? [];
if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
@@ -115,6 +115,7 @@ Future<Process> startProcess({required File executable, List<String>? args, bool
tempScriptFile.path,
[],
workingDirectory: executable.parent.path,
environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window
);
@@ -202,6 +203,7 @@ Future<bool> watchProcess(int pid) async {
return await completer.future;
}
// TODO: Template
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev';