25 Commits

Author SHA1 Message Date
Alessandro Autiero
d42946c44b 10.0.3 2024-12-09 22:28:24 +01:00
Alessandro Autiero
0a59a32c1b 10.0.2 2024-12-09 14:36:43 +01:00
Alessandro Autiero
2046cb14f6 10.0 2024-12-09 12:59:14 +01:00
Alessandro Autiero
e3f7a1d2cc 10.0 2024-12-09 12:49:21 +01:00
Alessandro Autiero
cd6752ed3f Merge pull request #169 from Auties00/_onLoggedIn
10.0
2024-12-09 12:44:14 +01:00
Alessandro Autiero
e1df46efd9 10.0 2024-12-09 12:42:49 +01:00
Alessandro Autiero
dccd05e57f Merge pull request #168 from Auties00/_onLoggedIn
10.0
2024-12-09 12:15:20 +01:00
Alessandro Autiero
eb7745cc4d 10.0 2024-12-09 12:14:41 +01:00
Alessandro Autiero
7d5e17642a Merge pull request #166 from Auties00/_onLoggedIn
Switched to starfall.dll
2024-12-08 20:42:54 +01:00
Alessandro Autiero
6f91ad0404 Switched to starfall.dll 2024-12-08 20:41:31 +01:00
Alessandro Autiero
0c38528e77 Merge pull request #118 from Auties00/_onLoggedIn
On logged in
2024-10-21 22:34:47 +02:00
Alessandro Autiero
dfebe74518 Switched to sinum 2024-10-21 20:32:23 +02:00
Alessandro Autiero
bfe15e43d9 Released 9.2.7 2024-09-14 12:37:56 +02:00
Alessandro Autiero
62dae468bf Merge pull request #98 from Auties00/_onLoggedIn
Released 9.2.6
2024-09-12 17:49:12 +02:00
Alessandro Autiero
a9af28273a Released 9.2.6 2024-09-12 15:46:24 +02:00
Alessandro Autiero
232bf8fbfc Update README.md 2024-08-18 22:35:17 +02:00
Alessandro Autiero
a787c4efc9 Merge pull request #86 from Auties00/_onLoggedIn
Release 9.2.5
2024-08-18 22:34:36 +02:00
Alessandro Autiero
4c3fe9bc65 Released 9.2.5 2024-08-18 20:29:09 +02:00
Alessandro Autiero
3f88d5ed80 Create .gitattributes 2024-07-31 11:54:02 +02:00
Alessandro Autiero
582270849e Released 9.2.4 2024-07-10 15:40:52 +02:00
Alessandro Autiero
1ef4e76768 Small fix to display errors and warnings from backend 2024-07-10 15:19:20 +02:00
Alessandro Autiero
cd8c8e6dd9 Release 9.2.3 2024-07-10 15:11:49 +02:00
Alessandro Autiero
170a878e79 Merge pull request #69 from Auties00/_onLoggedIn
Release 9.2.2
2024-07-09 22:38:47 +02:00
Alessandro Autiero
a2505011d9 Release 9.2.2 2024-07-09 20:38:01 +02:00
Alessandro Autiero
3e2c2e96b1 Release 9.2.1 2024-07-07 10:17:07 +02:00
69 changed files with 2282 additions and 2000 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
backend/**/* linguist-vendored

View File

@@ -8,6 +8,8 @@ Join our discord at https://discord.gg/reboot
- COMMON: Shared business logic for CLI and GUI modules - COMMON: Shared business logic for CLI and GUI modules
- CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart - CLI: Work in progress command line interface to host a Fortnite Server on a Windows VPS easily, developed in Dart
- GUI: Stable graphical user interface to play and host Fortnite S0-14 - GUI: Stable graphical user interface to play and host Fortnite S0-14
![image](https://github.com/user-attachments/assets/7ff5d49e-8920-41ad-a805-188d84ad6ec4)
## Installation ## Installation

8
archive/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Builds Archive
Builds are stored on a Cloudflare R2 instance at `https://builds.rebootfn.org/versions.json`.
If you want to move them to another AWS-compatible object storage, run:
```
move.ps1
```
and provide the required parameters.

98
archive/move.ps1 Normal file
View File

@@ -0,0 +1,98 @@
param(
[Parameter(Mandatory=$true)]
[string]$UrlListPath, # Path to a text file with one URL per line
[Parameter(Mandatory=$true)]
[string]$BucketName, # Name of the R2 bucket
[Parameter(Mandatory=$true)]
[string]$AccessKey, # Your R2 access key
[Parameter(Mandatory=$true)]
[string]$SecretKey, # Your R2 secret key
[Parameter(Mandatory=$true)]
[string]$EndPointURL, # Your R2 endpoint URL, e.g. https://<account_id>.r2.cloudflarestorage.com
[Parameter(Mandatory=$false)]
[int]$MaxConcurrentConnections = 16, # Number of concurrent connections for each file download
[Parameter(Mandatory=$false)]
[int]$SplitCount = 16, # Number of segments to split the download into
[Parameter(Mandatory=$false)]
[string]$AwsRegion = "auto" # Region; often "auto" works for R2, but can be set if needed
)
# Set AWS environment variables for this session
$Env:AWS_ACCESS_KEY_ID = $AccessKey
$Env:AWS_SECRET_ACCESS_KEY = $SecretKey
$Env:AWS_REGION = $AwsRegion # If required, or leave as "auto"
# Read all URLs from file
$Urls = Get-Content $UrlListPath | Where-Object { $_ -and $_. Trim() -ne "" }
# Ensure aria2 is available
if (-not (Get-Command aria2c -ErrorAction SilentlyContinue)) {
Write-Error "aria2c not found in PATH. Please install aria2."
exit 1
}
# Ensure aws CLI is available
if (-not (Get-Command aws -ErrorAction SilentlyContinue)) {
Write-Error "aws CLI not found in PATH. Please install AWS CLI."
exit 1
}
function Process-Url {
param(
[string]$Url,
[string]$BucketName,
[string]$EndPointURL,
[int]$MaxConcurrentConnections,
[int]$SplitCount
)
# Extract the filename from the URL
$FileName = Split-Path -Leaf $Url
try {
Write-Host "Downloading: $Url"
# Use aria2c to download with multiple connections
& aria2c `
--max-connection-per-server=$MaxConcurrentConnections `
--split=$SplitCount `
--out=$FileName `
--check-certificate=false `
--header="Cookie: _c_t_c=1" `
$Url
if (!(Test-Path $FileName)) {
Write-Host "Failed to download $Url"
return
}
Write-Host "Uploading $FileName to R2 bucket: $BucketName"
& aws s3 cp $FileName "s3://$BucketName/$FileName" --endpoint-url $EndPointURL
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to upload $FileName to R2"
return
}
Write-Host "Upload successful. Deleting local file: $FileName"
Remove-Item $FileName -Force
Write-Host "Completed processing of $FileName."
} catch {
Write-Host "Error processing $Url"
Write-Host $_
}
}
# Process each URL sequentially here. If you'd like to run multiple URLs in parallel,
# you could replace the foreach loop with a ForEach-Object -Parallel block.
foreach ($Url in $Urls) {
Process-Url -Url $Url -BucketName $BucketName -EndPointURL $EndPointURL -MaxConcurrentConnections $MaxConcurrentConnections -SplitCount $SplitCount
}

85
archive/versions.txt Normal file
View File

@@ -0,0 +1,85 @@
https://builds.rebootfn.org/1.7.2.zip
https://builds.rebootfn.org/1.8.rar
https://builds.rebootfn.org/1.8.1.rar
https://builds.rebootfn.org/1.8.2.rar
https://builds.rebootfn.org/1.9.rar
https://builds.rebootfn.org/1.9.1.rar
https://builds.rebootfn.org/1.10.rar
https://builds.rebootfn.org/1.11.zip
https://builds.rebootfn.org/2.1.0.zip
https://builds.rebootfn.org/2.2.0.rar
https://builds.rebootfn.org/2.3.rar
https://builds.rebootfn.org/2.4.0.zip
https://builds.rebootfn.org/2.4.2.zip
https://builds.rebootfn.org/2.5.0.rar
https://builds.rebootfn.org/3.0.zip
https://builds.rebootfn.org/3.1.rar
https://builds.rebootfn.org/3.1.1.zip
https://builds.rebootfn.org/3.2.zip
https://builds.rebootfn.org/3.3.rar
https://builds.rebootfn.org/3.5.rar
https://builds.rebootfn.org/3.6.zip
https://builds.rebootfn.org/4.0.zip
https://builds.rebootfn.org/4.1.zip
https://builds.rebootfn.org/4.2.zip
https://builds.rebootfn.org/4.4.rar
https://builds.rebootfn.org/4.5.rar
https://builds.rebootfn.org/5.00.rar
https://builds.rebootfn.org/5.0.1.rar
https://builds.rebootfn.org/5.10.rar
https://builds.rebootfn.org/5.21.rar
https://builds.rebootfn.org/5.30.rar
https://builds.rebootfn.org/5.40.rar
https://builds.rebootfn.org/6.00.rar
https://builds.rebootfn.org/6.01.rar
https://builds.rebootfn.org/6.1.1.rar
https://builds.rebootfn.org/6.02.rar
https://builds.rebootfn.org/6.2.1.rar
https://builds.rebootfn.org/6.10.rar
https://builds.rebootfn.org/6.10.1.rar
https://builds.rebootfn.org/6.10.2.rar
https://builds.rebootfn.org/6.21.rar
https://builds.rebootfn.org/6.22.rar
https://builds.rebootfn.org/6.30.rar
https://builds.rebootfn.org/6.31.rar
https://builds.rebootfn.org/7.00.rar
https://builds.rebootfn.org/7.10.rar
https://builds.rebootfn.org/7.20.rar
https://builds.rebootfn.org/7.30.zip
https://builds.rebootfn.org/7.40.rar
https://builds.rebootfn.org/8.00.zip
https://builds.rebootfn.org/8.20.rar
https://builds.rebootfn.org/8.30.rar
https://builds.rebootfn.org/8.40.zip
https://builds.rebootfn.org/8.50.zip
https://builds.rebootfn.org/8.51.rar
https://builds.rebootfn.org/9.00.zip
https://builds.rebootfn.org/9.01.zip
https://builds.rebootfn.org/9.10.rar
https://builds.rebootfn.org/9.21.zip
https://builds.rebootfn.org/9.30.zip
https://builds.rebootfn.org/9.40.zip
https://builds.rebootfn.org/9.41.rar
https://builds.rebootfn.org/10.00.zip
https://builds.rebootfn.org/10.10.zip
https://builds.rebootfn.org/10.20.zip
https://builds.rebootfn.org/10.31.zip
https://builds.rebootfn.org/10.40.rar
https://builds.rebootfn.org/11.00.zip
https://builds.rebootfn.org/11.31.rar
https://builds.rebootfn.org/12.00.rar
https://builds.rebootfn.org/12.21.zip
https://builds.rebootfn.org/12.50.zip
https://builds.rebootfn.org/12.61.zip
https://builds.rebootfn.org/13.00.rar
https://builds.rebootfn.org/13.40.zip
https://builds.rebootfn.org/14.00.rar
https://builds.rebootfn.org/14.40.rar
https://builds.rebootfn.org/14.60.rar
https://builds.rebootfn.org/15.30.rar
https://builds.rebootfn.org/16.40.rar
https://builds.rebootfn.org/17.30.zip
https://builds.rebootfn.org/17.50.zip
https://builds.rebootfn.org/18.40.zip
https://builds.rebootfn.org/19.10.rar
https://builds.rebootfn.org/20.40.zip"

2
backend/index.js vendored
View File

@@ -35,7 +35,7 @@ express.use(require("./structure/matchmaking.js"));
express.use(require("./structure/cloudstorage.js")); express.use(require("./structure/cloudstorage.js"));
express.use(require("./structure/mcp.js")); express.use(require("./structure/mcp.js"));
const port = process.env.PORT || 3551; const port = 3551;
express.listen(port, () => { express.listen(port, () => {
console.log("LawinServer started listening on port", port); console.log("LawinServer started listening on port", port);

View File

@@ -7,7 +7,7 @@ Future<bool> startServerCli(String? host, int? port, ServerType type) async {
stdout.writeln("Starting backend server..."); stdout.writeln("Starting backend server...");
switch(type){ switch(type){
case ServerType.local: case ServerType.local:
var result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort); final result = await pingBackend(host ?? kDefaultBackendHost, port ?? kDefaultBackendPort);
if(result == null){ if(result == null){
throw Exception("Local backend server is not running"); throw Exception("Local backend server is not running");
} }

View File

@@ -1,4 +1,5 @@
const String kDefaultPlayerName = "Player"; const String kDefaultPlayerName = "Player";
const String kDefaultHostName = "Host";
const String kDefaultGameServerHost = "127.0.0.1"; const String kDefaultGameServerHost = "127.0.0.1";
const String kDefaultGameServerPort = "7777"; const String kDefaultGameServerPort = "7777";
const String kInitializedLine = "Game Engine Initialized"; const String kInitializedLine = "Game Engine Initialized";
@@ -11,7 +12,7 @@ const List<String> kCorruptedBuildErrors = [
"Critical error", "Critical error",
"when 0 bytes remain", "when 0 bytes remain",
"Pak chunk signature verification failed!", "Pak chunk signature verification failed!",
"Couldn't find pak signature file" "LogWindows:Error: Fatal error!"
]; ];
const List<String> kCannotConnectErrors = [ const List<String> kCannotConnectErrors = [
"port 3551 failed: Connection refused", "port 3551 failed: Connection refused",

View File

@@ -9,9 +9,22 @@ extension FortniteVersionExtension on FortniteVersion {
static File? findFile(Directory directory, String name) { static File? findFile(Directory directory, String name) {
try{ try{
final result = directory.listSync(recursive: true) for(final child in directory.listSync()) {
.firstWhere((element) => path.basename(element.path) == name); if(child is Directory) {
return File(result.path); if(!path.basename(child.path).startsWith("\.")) {
final result = findFile(child, name);
if(result != null) {
return result;
}
}
}else if(child is File) {
if(path.basename(child.path) == name) {
return child;
}
}
}
return null;
}catch(_){ }catch(_){
return null; return null;
} }

View File

@@ -1,6 +1,9 @@
enum InjectableDll { enum InjectableDll {
console, console,
cobalt, starfall,
reboot, reboot,
memory }
extension InjectableDllVersionAware on InjectableDll {
bool get isVersionDependent => this == InjectableDll.reboot;
} }

View File

@@ -17,13 +17,15 @@ class FortniteBuild {
class FortniteBuildDownloadProgress { class FortniteBuildDownloadProgress {
final double progress; final double progress;
final int? minutesLeft; final int? timeLeft;
final bool extracting; final bool extracting;
final int speed;
FortniteBuildDownloadProgress({ FortniteBuildDownloadProgress({
required this.progress, required this.progress,
required this.extracting, required this.extracting,
this.minutesLeft, required this.timeLeft,
required this.speed
}); });
} }

View File

@@ -1,10 +1,11 @@
import 'dart:io'; import 'dart:io';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:version/version.dart';
class GameInstance { class GameInstance {
final String versionName; final Version version;
final int gamePid; final int gamePid;
final int? launcherPid; final int? launcherPid;
final int? eacPid; final int? eacPid;
@@ -17,7 +18,7 @@ class GameInstance {
GameInstance? child; GameInstance? child;
GameInstance({ GameInstance({
required this.versionName, required this.version,
required this.gamePid, required this.gamePid,
required this.launcherPid, required this.launcherPid,
required this.eacPid, required this.eacPid,

View File

@@ -26,8 +26,7 @@ enum ServerResultType {
freePortError, freePortError,
pingingRemote, pingingRemote,
pingingLocal, pingingLocal,
pingError, pingError;
processError;
bool get isError => name.contains("Error"); bool get isError => name.contains("Error");

View File

@@ -15,10 +15,19 @@ final Semaphore _semaphore = Semaphore();
String? _lastIp; String? _lastIp;
String? _lastPort; String? _lastPort;
Future<Process> startEmbeddedBackend(bool detached) async => startProcess( Future<Process> startEmbeddedBackend(bool detached, {void Function(String)? onError}) async {
final process = await startProcess(
executable: backendStartExecutable, executable: backendStartExecutable,
window: detached, window: detached,
); );
process.stdOutput.listen((message) => log("[BACKEND] Message: $message"));
process.stdError.listen((error) {
log("[BACKEND] Error: $error");
onError?.call(error);
});
process.exitCode.then((exitCode) => log("[BACKEND] Exit code: $exitCode"));
return process;
}
Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort); Future<HttpServer> startRemoteBackendProxy(Uri uri) async => await serve(proxyHandler(uri), kDefaultBackendHost, kDefaultBackendPort);

View File

@@ -3,165 +3,248 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_common/src/extension/types.dart'; import 'package:reboot_common/src/extension/types.dart';
import 'package:uuid/uuid.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:http/http.dart' as http;
const String kStopBuildDownloadSignal = "kill"; const String kStopBuildDownloadSignal = "kill";
final Dio _dio = _buildDioInstance(); final Uri _archiveSourceUrl = Uri.parse("https://builds.rebootfn.org/versions.json");
Dio _buildDioInstance() { final int _ariaPort = 6800;
final dio = Dio(); final Uri _ariaEndpoint = Uri.parse('http://localhost:$_ariaPort/jsonrpc');
final httpClientAdapter = dio.httpClientAdapter as IOHttpClientAdapter; final Duration _ariaMaxSpawnTime = const Duration(seconds: 10);
httpClientAdapter.createHttpClient = () { final String _ariaSecret = "RebootLauncher";
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) => true;
return client;
};
return dio;
}
final String _archiveSourceUrl = "https://raw.githubusercontent.com/simplyblk/Fortnitebuilds/main/README.md";
final RegExp _rarProgressRegex = RegExp("^((100)|(\\d{1,2}(.\\d*)?))%\$"); 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";
const String _genericError = "The build downloader is not working correctly";
const int _maxErrors = 100;
Future<List<FortniteBuild>> fetchBuilds(ignored) async { Future<List<FortniteBuild>> fetchBuilds(ignored) async {
final response = await _dio.get<String>( final response = await http.get(_archiveSourceUrl);
_archiveSourceUrl,
options: Options(
responseType: ResponseType.plain
)
);
if (response.statusCode != 200) { if (response.statusCode != 200) {
return []; return [];
} }
var results = <FortniteBuild>[]; return jsonDecode(response.body)
for (final line in response.data?.split("\n") ?? []) { .map((entry) {
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 { try {
results.add(FortniteBuild( final fileUrl = entry as String;
version: Version.parse(versionName), final fileName = Uri.parse(fileUrl).pathSegments.last;
link: link, final fileNameWithoutExtension = path.basenameWithoutExtension(fileName);
available: link.endsWith(".zip") || link.endsWith(".rar") return FortniteBuild(
)); version: Version.parse(fileNameWithoutExtension),
} on FormatException { link: entry,
// Ignore available: true
);
}catch(_) {
return null;
} }
} })
.whereType<FortniteBuild>()
return results; .toList();
} }
Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async { Future<void> downloadArchiveBuild(FortniteBuildDownloadOptions options) async {
final fileName = options.build.link.substring(options.build.link.lastIndexOf("/") + 1);
final outputFile = File("${options.destination.path}\\.build\\$fileName");
try { try {
final stopped = _setupLifecycle(options); final stopped = _setupLifecycle(options);
final outputDir = Directory("${options.destination.path}\\.build"); await outputFile.parent.create(recursive: true);
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 downloadItemCompleter = Completer<File>();
final response = _downloadArchive(options, stopped, tempFile, startTime);
await Future.any([stopped.future, response]);
if(!stopped.isCompleted) {
await _extractArchive(stopped, extension, tempFile, options);
}
delete(outputDir); await _startAriaServer();
}catch(error) { final downloadId = await _startAriaDownload(options, outputFile);
_onError(error, options); Timer.periodic(const Duration(seconds: 5), (Timer timer) async {
}
}
Future<void> _downloadArchive(FortniteBuildDownloadOptions options, Completer stopped, File tempFile, int startTime, [int? byteStart = null, int errorsCount = 0]) async {
var received = byteStart ?? 0;
try { try {
await _dio.download( final statusRequestId = Uuid().toString().replaceAll("-", "");
options.build.link, final statusRequest = {
tempFile.path, "jsonrcp": "2.0",
onReceiveProgress: (data, length) { "id": statusRequestId,
if(stopped.isCompleted) { "method": "aria2.tellStatus",
throw StateError("Download interrupted"); "params": [
"token:${_ariaSecret}",
downloadId
]
};
final statusResponse = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
final statusResponseJson = jsonDecode(statusResponse.body) as Map?;
if(statusResponseJson == null) {
downloadItemCompleter.completeError("Invalid download status (invalid JSON)");
timer.cancel();
return;
} }
received = data; final result = statusResponseJson["result"];
final percentage = (received / length) * 100; final files = result["files"] as List?;
_onProgress(startTime, percentage < 1 ? null : DateTime.now().millisecondsSinceEpoch, percentage, false, options); if(files == null || files.isEmpty) {
}, downloadItemCompleter.completeError("Download aborted");
deleteOnError: false, timer.cancel();
options: Options( return;
validateStatus: (statusCode) {
if(statusCode == 200) {
return true;
} }
if(statusCode == 403 || statusCode == 503) { final error = result["errorCode"];
throw _deniedConnectionError; if(error != null) {
final errorCode = int.tryParse(error);
if(errorCode == 0) {
final path = File(files[0]["path"]);
downloadItemCompleter.complete(path);
}else if(errorCode == 3) {
downloadItemCompleter.completeError("This build is not available yet");
}else {
final errorMessage = result["errorMessage"];
downloadItemCompleter.completeError("$errorMessage (error code $errorCode)");
} }
if(statusCode == 404) { timer.cancel();
throw _unavailableError; return;
} }
throw _genericError; final speed = int.parse(result["downloadSpeed"] ?? "0");
}, final completedLength = int.parse(files[0]["completedLength"] ?? "0");
headers: byteStart == null || byteStart <= 0 ? { final totalLength = int.parse(files[0]["length"] ?? "0");
"Cookie": "_c_t_c=1"
} : { final percentage = completedLength * 100 / totalLength;
"Cookie": "_c_t_c=1", final minutesLeft = speed == 0 ? -1 : ((totalLength - completedLength) / speed / 60).round();
"Range": "bytes=${byteStart}-" _onProgress(
}, options.port,
) percentage,
speed,
minutesLeft,
false
); );
}catch(error) { }catch(error) {
if(stopped.isCompleted) { throw "Invalid download status (${error})";
return;
} }
});
if(errorsCount > _maxErrors || error.toString().contains(_deniedConnectionError) || error.toString().contains(_unavailableError)) { await Future.any([stopped.future, downloadItemCompleter.future]);
if(!stopped.isCompleted) {
final extension = path.extension(fileName);
await _extractArchive(stopped, extension, await downloadItemCompleter.future, options);
}else {
await _stopAriaDownload(downloadId);
}
}catch(error) {
_onError(error, options); _onError(error, options);
return; }finally {
} delete(outputFile);
await _downloadArchive(options, stopped, tempFile, startTime, received, errorsCount + 1);
} }
} }
Future<void> _startAriaServer() async {
await stopDownloadServer();
final aria2c = File("${assetsDirectory.path}\\build\\aria2c.exe");
if(!aria2c.existsSync()) {
throw "Missing aria2c.exe";
}
final process = await startProcess(
executable: aria2c,
args: [
"--max-connection-per-server=${Platform.numberOfProcessors}",
"--split=${Platform.numberOfProcessors}",
"--enable-rpc",
"--rpc-listen-all=true",
"--rpc-allow-origin-all",
"--rpc-secret=$_ariaSecret",
"--rpc-listen-port=$_ariaPort",
"--file-allocation=none"
],
window: false
);
process.stdOutput.listen((message) => log("[ARIA] Message: $message"));
process.stdError.listen((error) => log("[ARIA] Error: $error"));
process.exitCode.then((exitCode) => log("[ARIA] Exit code: $exitCode"));
for(var i = 0; i < _ariaMaxSpawnTime.inSeconds; i++) {
if(await _isAriaRunning()) {
return;
}
await Future.delayed(const Duration(seconds: 1));
}
throw "cannot start download server (timeout exceeded)";
}
Future<bool> _isAriaRunning() async {
try {
final statusRequestId = Uuid().toString().replaceAll("-", "");
final statusRequest = {
"jsonrcp": "2.0",
"id": statusRequestId,
"method": "aria2.getVersion",
"params": [
"token:${_ariaSecret}"
]
};
final response = await http.post(_ariaEndpoint, body: jsonEncode(statusRequest));
return response.statusCode == 200;
}catch(_) {
return false;
}
}
Future<String> _startAriaDownload(FortniteBuildDownloadOptions options, File outputFile) async {
http.Response? addDownloadResponse;
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.addUri",
"params": [
"token:${_ariaSecret}",
[options.build.link],
{
"dir": outputFile.parent.path,
"out": path.basename(outputFile.path)
}
]
};
addDownloadResponse = await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
final addDownloadResponseJson = jsonDecode(addDownloadResponse.body);
final downloadId = addDownloadResponseJson is Map ? addDownloadResponseJson['result'] : null;
if(downloadId == null) {
throw "Start failed (${addDownloadResponse.body})";
}
return downloadId;
}catch(error) {
throw "Start failed (${addDownloadResponse?.body ?? error})";
}
}
Future<void> _stopAriaDownload(String downloadId) async {
try {
final addDownloadRequestId = Uuid().toString().replaceAll("-", "");
final addDownloadRequest = {
"jsonrcp": "2.0",
"id": addDownloadRequestId,
"method": "aria2.forceRemove",
"params": [
"token:${_ariaSecret}",
downloadId
]
};
await http.post(_ariaEndpoint, body: jsonEncode(addDownloadRequest));
stopDownloadServer();
}catch(error) {
throw "Stop failed (${error})";
}
}
Future<void> stopDownloadServer() async {
await killProcessByPort(_ariaPort);
}
Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async { Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File tempFile, FortniteBuildDownloadOptions options) async {
final startTime = DateTime.now().millisecondsSinceEpoch;
Process? process; Process? process;
switch (extension.toLowerCase()) { switch (extension.toLowerCase()) {
case ".zip": case ".zip":
final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe"); final sevenZip = File("${assetsDirectory.path}\\build\\7zip.exe");
if(!sevenZip.existsSync()) { if(!sevenZip.existsSync()) {
throw "Corrupted installation: missing 7zip.exe"; throw "Missing 7zip.exe";
} }
process = await startProcess( process = await startProcess(
@@ -176,10 +259,15 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
); );
var completed = false; var completed = false;
process.stdOutput.listen((data) { process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
if(data.toLowerCase().contains("everything is ok")) { if(data.toLowerCase().contains("everything is ok")) {
completed = true; completed = true;
_onProgress(startTime, now, 100, true, options); _onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt); process?.kill(ProcessSignal.sigabrt);
return; return;
} }
@@ -190,7 +278,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
} }
final percentage = int.parse(element.substring(0, element.length - 1)).toDouble(); final percentage = int.parse(element.substring(0, element.length - 1)).toDouble();
_onProgress(startTime, now, percentage, true, options); _onProgress(
options.port,
percentage,
0,
-1,
true
);
}); });
process.stdError.listen((data) { process.stdError.listen((data) {
if(!data.isBlank) { if(!data.isBlank) {
@@ -206,7 +300,7 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
case ".rar": case ".rar":
final winrar = File("${assetsDirectory.path}\\build\\winrar.exe"); final winrar = File("${assetsDirectory.path}\\build\\winrar.exe");
if(!winrar.existsSync()) { if(!winrar.existsSync()) {
throw "Corrupted installation: missing winrar.exe"; throw "Missing winrar.exe";
} }
process = await startProcess( process = await startProcess(
@@ -221,11 +315,16 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
); );
var completed = false; var completed = false;
process.stdOutput.listen((data) { process.stdOutput.listen((data) {
final now = DateTime.now().millisecondsSinceEpoch;
data = data.replaceAll("\r", "").replaceAll("\b", "").trim(); data = data.replaceAll("\r", "").replaceAll("\b", "").trim();
if(data == "All OK") { if(data == "All OK") {
completed = true; completed = true;
_onProgress(startTime, now, 100, true, options); _onProgress(
options.port,
100,
0,
-1,
true
);
process?.kill(ProcessSignal.sigabrt); process?.kill(ProcessSignal.sigabrt);
return; return;
} }
@@ -236,7 +335,13 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
} }
final percentage = int.parse(element).toDouble(); final percentage = int.parse(element).toDouble();
_onProgress(startTime, now, percentage, true, options); _onProgress(
options.port,
percentage,
0,
-1,
true
);
}); });
process.stdError.listen((data) { process.stdError.listen((data) {
if(!data.isBlank) { if(!data.isBlank) {
@@ -257,21 +362,22 @@ Future<void> _extractArchive(Completer<dynamic> stopped, String extension, File
process.kill(ProcessSignal.sigabrt); process.kill(ProcessSignal.sigabrt);
} }
void _onProgress(int startTime, int? now, double percentage, bool extracting, FortniteBuildDownloadOptions options) { void _onProgress(SendPort port, double percentage, int speed, int minutesLeft, bool extracting) {
if(percentage == 0) { if(percentage == 0) {
options.port.send(FortniteBuildDownloadProgress( port.send(FortniteBuildDownloadProgress(
progress: percentage, progress: percentage,
extracting: extracting extracting: extracting,
timeLeft: null,
speed: speed
)); ));
return; return;
} }
final msLeft = now == null ? null : startTime + (now - startTime) * 100 / percentage - now; port.send(FortniteBuildDownloadProgress(
final minutesLeft = msLeft == null ? null : (msLeft / 1000 / 60).round();
options.port.send(FortniteBuildDownloadProgress(
progress: percentage, progress: percentage,
extracting: extracting, extracting: extracting,
minutesLeft: minutesLeft timeLeft: minutesLeft,
speed: speed
)); ));
} }
@@ -292,3 +398,4 @@ Completer<dynamic> _setupLifecycle(FortniteBuildDownloadOptions options) {
options.port.send(lifecyclePort.sendPort); options.port.send(lifecyclePort.sendPort);
return stopped; return stopped;
} }

View File

@@ -6,13 +6,16 @@ import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
bool _watcher = false; bool _watcher = false;
final File rebootDllFile = File("${dllsDirectory.path}\\reboot.dll"); final File rebootBeforeS20DllFile = File("${dllsDirectory.path}\\reboot.dll");
const String kRebootDownloadUrl = final File rebootAboveS20DllFile = File("${dllsDirectory.path}\\rebootS20.dll");
"http://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Release.zip"; const String kRebootBelowS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/Reboot.zip";
const String kRebootAboveS20DownloadUrl =
"https://nightly.link/Milxnor/Project-Reboot-3.0/workflows/msbuild/master/RebootS20.zip";
Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async { Future<bool> hasRebootDllUpdate(int? lastUpdateMs, {int hours = 24, bool force = false}) async {
final lastUpdate = await _getLastUpdate(lastUpdateMs); final lastUpdate = await _getLastUpdate(lastUpdateMs);
final exists = await rebootDllFile.exists(); final exists = await rebootBeforeS20DllFile.exists() && await rebootAboveS20DllFile.exists();
final now = DateTime.now(); final now = DateTime.now();
return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours); return force || !exists || (hours > 0 && lastUpdate != null && now.difference(lastUpdate).inHours > hours);
} }
@@ -28,9 +31,8 @@ Future<void> downloadCriticalDll(String name, String outputPath) async {
await output.writeAsBytes(response.bodyBytes, flush: true); await output.writeAsBytes(response.bodyBytes, flush: true);
} }
Future<int> downloadRebootDll(String url) async { Future<void> downloadRebootDll(File file, String url) async {
Directory? outputDir; Directory? outputDir;
final now = DateTime.now();
try { try {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if(response.statusCode != 200) { if(response.statusCode != 200) {
@@ -42,8 +44,7 @@ Future<int> downloadRebootDll(String url) async {
await tempZip.writeAsBytes(response.bodyBytes, flush: true); await tempZip.writeAsBytes(response.bodyBytes, flush: true);
await extractFileToDisk(tempZip.path, outputDir.path); await extractFileToDisk(tempZip.path, outputDir.path);
final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path); final rebootDll = File(outputDir.listSync().firstWhere((element) => path.extension(element.path) == ".dll").path);
await rebootDllFile.writeAsBytes(await rebootDll.readAsBytes(), flush: true); await file.writeAsBytes(await rebootDll.readAsBytes(), flush: true);
return now.millisecondsSinceEpoch;
} finally{ } finally{
if(outputDir != null) { if(outputDir != null) {
delete(outputDir); delete(outputDir);
@@ -63,7 +64,7 @@ Stream<String> watchDlls() async* {
} }
_watcher = true; _watcher = true;
await for(final event in rebootDllFile.parent.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) { await for(final event in dllsDirectory.watch(events: FileSystemEvent.delete | FileSystemEvent.move)) {
if (event.path.endsWith(".dll")) { if (event.path.endsWith(".dll")) {
yield event.path; yield event.path;
} }

View File

@@ -7,7 +7,7 @@ final File launcherLogFile = _createLoggingFile();
final Semaphore _semaphore = Semaphore(1); final Semaphore _semaphore = Semaphore(1);
File _createLoggingFile() { File _createLoggingFile() {
final file = File("${logsDirectory.path}\\launcher.log"); final file = File("${installationDirectory.path}\\launcher.log");
file.parent.createSync(recursive: true); file.parent.createSync(recursive: true);
if(file.existsSync()) { if(file.existsSync()) {
file.deleteSync(); file.deleteSync();

View File

@@ -14,9 +14,6 @@ Directory get assetsDirectory {
return installationDirectory; return installationDirectory;
} }
Directory get logsDirectory =>
Directory("${installationDirectory.path}\\logs");
Directory get settingsDirectory => Directory get settingsDirectory =>
Directory("${installationDirectory.path}\\settings"); Directory("${installationDirectory.path}\\settings");

View File

@@ -1,15 +1,14 @@
// ignore_for_file: non_constant_identifier_names // ignore_for_file: non_constant_identifier_names
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:ffi'; import 'dart:ffi';
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:ffi/ffi.dart'; import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as path;
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:sync/semaphore.dart';
import 'package:win32/win32.dart'; import 'package:win32/win32.dart';
final _ntdll = DynamicLibrary.open('ntdll.dll'); final _ntdll = DynamicLibrary.open('ntdll.dll');
@@ -96,62 +95,53 @@ Future<bool> startElevatedProcess({required String executable, required String a
var shellInput = calloc<SHELLEXECUTEINFO>(); var shellInput = calloc<SHELLEXECUTEINFO>();
shellInput.ref.lpFile = executable.toNativeUtf16(); shellInput.ref.lpFile = executable.toNativeUtf16();
shellInput.ref.lpParameters = args.toNativeUtf16(); shellInput.ref.lpParameters = args.toNativeUtf16();
shellInput.ref.nShow = window ? SW_SHOWNORMAL : SW_HIDE; shellInput.ref.nShow = window ? SHOW_WINDOW_CMD.SW_SHOWNORMAL : SHOW_WINDOW_CMD.SW_HIDE;
shellInput.ref.fMask = ES_AWAYMODE_REQUIRED; shellInput.ref.fMask = EXECUTION_STATE.ES_AWAYMODE_REQUIRED;
shellInput.ref.lpVerb = "runas".toNativeUtf16(); shellInput.ref.lpVerb = "runas".toNativeUtf16();
shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>(); shellInput.ref.cbSize = sizeOf<SHELLEXECUTEINFO>();
var shellResult = ShellExecuteEx(shellInput); return ShellExecuteEx(shellInput) == 1;
return shellResult == 1;
} }
Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async { Future<Process> startProcess({required File executable, List<String>? args, bool useTempBatch = true, bool window = false, String? name, Map<String, String>? environment}) async {
log("[PROCESS] Starting process on ${executable.path} with $args (useTempBatch: $useTempBatch, window: $window, name: $name, environment: $environment)");
final argsOrEmpty = args ?? []; final argsOrEmpty = args ?? [];
final workingDirectory = _getWorkingDirectory(executable);
if(useTempBatch) { if(useTempBatch) {
final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process"); final tempScriptDirectory = await tempDirectory.createTemp("reboot_launcher_process");
final tempScriptFile = File("${tempScriptDirectory.path}/process.bat"); final tempScriptFile = File("${tempScriptDirectory.path}\\process.bat");
final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}'; final command = window ? 'cmd.exe /k ""${executable.path}" ${argsOrEmpty.join(" ")}"' : '"${executable.path}" ${argsOrEmpty.join(" ")}';
await tempScriptFile.writeAsString(command, flush: true); await tempScriptFile.writeAsString(command, flush: true);
final process = await Process.start( final process = await Process.start(
tempScriptFile.path, tempScriptFile.path,
[], [],
workingDirectory: executable.parent.path, workingDirectory: workingDirectory,
environment: environment, environment: environment,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window runInShell: window
); );
return _withLogger(name, executable, process, window); return _ExtendedProcess(process, true);
} }
final process = await Process.start( final process = await Process.start(
executable.path, executable.path,
args ?? [], args ?? [],
workingDirectory: executable.parent.path, workingDirectory: workingDirectory,
mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal, mode: window ? ProcessStartMode.detachedWithStdio : ProcessStartMode.normal,
runInShell: window runInShell: window
); );
return _withLogger(name, executable, process, window); return _ExtendedProcess(process, true);
} }
_ExtendedProcess _withLogger(String? name, File executable, Process process, bool window) { String? _getWorkingDirectory(File executable) {
final extendedProcess = _ExtendedProcess(process, true); try {
final loggingFile = File("${logsDirectory.path}\\${name ?? path.basenameWithoutExtension(executable.path)}-${DateTime.now().millisecondsSinceEpoch}.log"); log("[PROCESS] Calculating working directory for $executable");
loggingFile.parent.createSync(recursive: true); final workingDirectory = executable.parent.resolveSymbolicLinksSync();
if(loggingFile.existsSync()) { log("[PROCESS] Using working directory: $workingDirectory");
loggingFile.deleteSync(); return workingDirectory;
}catch(error) {
log("[PROCESS] Cannot infer working directory: $error");
return null;
} }
final semaphore = Semaphore(1);
void logEvent(String event) async {
await semaphore.acquire();
await loggingFile.writeAsString("$event\n", mode: FileMode.append, flush: true);
semaphore.release();
}
extendedProcess.stdOutput.listen(logEvent);
extendedProcess.stdError.listen(logEvent);
if(!window) {
extendedProcess.exitCode.then((value) => logEvent("Process terminated with exit code: $value\n"));
}
return extendedProcess;
} }
final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd), final _NtResumeProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
@@ -161,89 +151,92 @@ final _NtSuspendProcess = _ntdll.lookupFunction<Int32 Function(IntPtr hWnd),
int Function(int hWnd)>('NtSuspendProcess'); int Function(int hWnd)>('NtSuspendProcess');
bool suspend(int pid) { bool suspend(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtSuspendProcess(processHandle); try {
return _NtSuspendProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle); CloseHandle(processHandle);
return result == 0; }
} }
bool resume(int pid) { bool resume(int pid) {
final processHandle = OpenProcess(PROCESS_SUSPEND_RESUME, FALSE, pid); final processHandle = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_SUSPEND_RESUME, FALSE, pid);
final result = _NtResumeProcess(processHandle); try {
return _NtResumeProcess(processHandle) == 0;
} finally {
CloseHandle(processHandle); CloseHandle(processHandle);
return result == 0; }
} }
void _watchProcess(int pid) {
final processHandle = OpenProcess(SYNCHRONIZE, FALSE, pid); Future<void> watchProcess(int pid) => Isolate.run(() {
final processHandle = OpenProcess(FILE_ACCESS_RIGHTS.SYNCHRONIZE, FALSE, pid);
if (processHandle == 0) {
return;
}
try { try {
WaitForSingleObject(processHandle, INFINITE); WaitForSingleObject(processHandle, INFINITE);
}finally { }finally {
CloseHandle(processHandle); CloseHandle(processHandle);
} }
} });
Future<bool> watchProcess(int pid) async { List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool logging, String additionalArgs) {
var completer = Completer<bool>(); log("[PROCESS] Generating reboot args");
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 await completer.future;
}
// TODO: Template
List<String> createRebootArgs(String username, String password, bool host, GameServerType hostType, bool log, String additionalArgs) {
if(password.isEmpty) { if(password.isEmpty) {
username = '${_parseUsername(username, host)}@projectreboot.dev'; username = '${_parseUsername(username, host)}@projectreboot.dev';
} }
password = password.isNotEmpty ? password : "Rebooted"; password = password.isNotEmpty ? password : "Rebooted";
final args = [ final args = LinkedHashMap<String, String>(
"-epicapp=Fortnite", equals: (a, b) => a.toUpperCase() == b.toUpperCase(),
"-epicenv=Prod", hashCode: (a) => a.toUpperCase().hashCode
"-epiclocale=en-us", );
"-epicportal", args.addAll({
"-skippatchcheck", "-epicapp": "Fortnite",
"-nobe", "-epicenv": "Prod",
"-fromfl=eac", "-epiclocale": "en-us",
"-fltoken=3db3ba5dcbd2e16703f3978d", "-epicportal": "",
"-caldera=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ", "-skippatchcheck": "",
"-AUTH_LOGIN=$username", "-nobe": "",
"-AUTH_PASSWORD=${password.isNotEmpty ? password : "Rebooted"}", "-fromfl": "eac",
"-AUTH_TYPE=epic" "-fltoken": "3db3ba5dcbd2e16703f3978d",
]; "-caldera": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2NvdW50X2lkIjoiYmU5ZGE1YzJmYmVhNDQwN2IyZjQwZWJhYWQ4NTlhZDQiLCJnZW5lcmF0ZWQiOjE2Mzg3MTcyNzgsImNhbGRlcmFHdWlkIjoiMzgxMGI4NjMtMmE2NS00NDU3LTliNTgtNGRhYjNiNDgyYTg2IiwiYWNQcm92aWRlciI6IkVhc3lBbnRpQ2hlYXQiLCJub3RlcyI6IiIsImZhbGxiYWNrIjpmYWxzZX0.VAWQB67RTxhiWOxx7DBjnzDnXyyEnX7OljJm-j2d88G_WgwQ9wrE6lwMEHZHjBd1ISJdUO1UVUqkfLdU5nofBQ",
"-AUTH_LOGIN": username,
"-AUTH_PASSWORD": password.isNotEmpty ? password : "Rebooted",
"-AUTH_TYPE": "epic"
});
if(log) { if(logging) {
args.add("-log"); args["-log"] = "";
} }
if(host) { if(host) {
args.addAll([ args["-nosplash"] = "";
"-nosplash", args["-nosound"] = "";
"-nosound"
]);
if(hostType == GameServerType.headless){ if(hostType == GameServerType.headless){
args.add("-nullrhi"); args["-nullrhi"] = "";
} }
} }
if(additionalArgs.isNotEmpty){ log("[PROCESS] Default args: $args");
args.addAll(additionalArgs.split(" ")); log("[PROCESS] Adding custom args: $additionalArgs");
for(final additionalArg in additionalArgs.split(" ")) {
log("[PROCESS] Processing custom arg: $additionalArg");
final separatorIndex = additionalArg.indexOf("=");
final argName = separatorIndex == -1 ? additionalArg : additionalArg.substring(0, separatorIndex);
log("[PROCESS] Custom arg key: $argName");
final argValue = separatorIndex == -1 || separatorIndex + 1 >= additionalArg.length ? "" : additionalArg.substring(separatorIndex + 1);
log("[PROCESS] Custom arg value: $argValue");
args[argName] = argValue;
log("[PROCESS] Updated args: $args");
} }
return args; log("[PROCESS] Final args result: $args");
return args.entries
.map((entry) => entry.value.isEmpty ? entry.key : "${entry.key}=${entry.value}")
.toList();
} }
void handleGameOutput({ void handleGameOutput({
@@ -257,16 +250,22 @@ void handleGameOutput({
required void Function() onBuildCorrupted, required void Function() onBuildCorrupted,
}) { }) {
if (line.contains(kShutdownLine)) { if (line.contains(kShutdownLine)) {
log("[FORTNITE_OUTPUT_HANDLER] Detected shutdown: $line");
onShutdown(); onShutdown();
}else if(kCorruptedBuildErrors.any((element) => line.contains(element))){ }else if(kCorruptedBuildErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected corrupt build: $line");
onBuildCorrupted(); onBuildCorrupted();
}else if(kCannotConnectErrors.any((element) => line.contains(element))){ }else if(kCannotConnectErrors.any((element) => line.contains(element))){
log("[FORTNITE_OUTPUT_HANDLER] Detected cannot connect error: $line");
onTokenError(); onTokenError();
}else if(kLoggedInLines.every((entry) => line.contains(entry))) { }else if(kLoggedInLines.every((entry) => line.contains(entry))) {
log("[FORTNITE_OUTPUT_HANDLER] Detected logged in: $line");
onLoggedIn(); onLoggedIn();
}else if(line.contains(kGameFinishedLine) && host) { }else if(line.contains(kGameFinishedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected match end: $line");
onMatchEnd(); onMatchEnd();
}else if(line.contains(kDisplayInitializedLine) && host) { }else if(line.contains(kDisplayInitializedLine) && host) {
log("[FORTNITE_OUTPUT_HANDLER] Detected display attach: $line");
onDisplayAttached(); onDisplayAttached();
} }
} }
@@ -299,7 +298,14 @@ final class _ExtendedProcess implements Process {
@override @override
Future<int> get exitCode => _delegate.exitCode; Future<int> get exitCode {
try {
return _delegate.exitCode;
}catch(_) {
return watchProcess(_delegate.pid)
.then((_) => -1);
}
}
@override @override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal); bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => _delegate.kill(signal);

View File

@@ -7,19 +7,18 @@ environment:
sdk: ">=3.0.0 <=4.0.0" sdk: ">=3.0.0 <=4.0.0"
dependencies: dependencies:
dio: ^5.3.2 win32: ^5.5.4
win32: 3.0.0 ffi: ^2.1.3
ffi: ^2.1.0 path: ^1.9.0
path: ^1.8.3 http: ^1.2.2
http: ^1.1.0 crypto: ^3.0.5
crypto: ^3.0.2 archive: ^3.6.1
archive: ^3.3.7
ini: ^2.1.0 ini: ^2.1.0
shelf_proxy: ^1.0.2 shelf_proxy: ^1.0.2
sync: ^0.3.0 sync: ^0.3.0
uuid: ^3.0.6 uuid: ^4.5.1
shelf_web_socket: ^2.0.0 shelf_web_socket: ^2.0.0
version: ^3.0.2 version: ^3.0.2
dev_dependencies: dev_dependencies:
flutter_lints: ^2.0.1 flutter_lints: ^5.0.0

View File

@@ -1,16 +1,39 @@
# reboot_launcher
Launcher for project reboot # Reboot Launcher
Welcome to the **Reboot Launcher**!
This is a GUI application developed as part of the **Reboot Project**.
## Getting Started ## Getting Started
This project is a starting point for a Flutter application. ### Running the Project
To launch the project in development mode, simply run:
```
flutter run
```
A few resources to get you started if this is your first Flutter project: ### Building the Project
To create a production-ready build, use:
```
flutter build
```
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) ### Packaging the Project
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) To package the application for distribution, run:
```
package.bat
```
For help getting started with Flutter development, view the ## Requirements
[online documentation](https://docs.flutter.dev/), which offers tutorials, - [Flutter SDK](https://flutter.dev/docs/get-started/install)
samples, guidance on mobile development, and a full API reference. - Supported operating systems: Windows
## Other platforms
Native support for these platforms is not currently planned, but Linux support is a priority for the 10.0 release cycle
- [Linux Tutorial using Proton](https://www.reddit.com/r/linux_gaming/comments/1fwa4l8/guide_running_a_fortnite_private_server_to_play/)
- No tutorials are available for MacOS(got lost when the Reboot discord was banned), but it's possible to run Reboot using a compatibility layer
## Contributing
Contributions are welcome! Feel free to open an issue or submit a pull request.

Binary file not shown.

BIN
gui/assets/build/aria2c.exe Normal file

Binary file not shown.

View File

@@ -1,2 +0,0 @@
taskkill /f /im winrar.exe
taskkill /f /im tar.exe

View File

@@ -1,735 +0,0 @@
[Code]
// https://github.com/DomGries/InnoDependencyInstaller
// types and variables
type
TDependency_Entry = record
Filename: String;
Parameters: String;
Title: String;
URL: String;
Checksum: String;
ForceSuccess: Boolean;
RestartAfter: Boolean;
end;
var
Dependency_Memo: String;
Dependency_List: array of TDependency_Entry;
Dependency_NeedToRestart, Dependency_ForceX86: Boolean;
Dependency_DownloadPage: TDownloadWizardPage;
procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean);
var
Dependency: TDependency_Entry;
DependencyCount: Integer;
begin
Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title;
Dependency.Filename := Filename;
Dependency.Parameters := Parameters;
Dependency.Title := Title;
if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
Dependency.URL := '';
end else begin
Dependency.URL := URL;
end;
Dependency.Checksum := Checksum;
Dependency.ForceSuccess := ForceSuccess;
Dependency.RestartAfter := RestartAfter;
DependencyCount := GetArrayLength(Dependency_List);
SetArrayLength(Dependency_List, DependencyCount + 1);
Dependency_List[DependencyCount] := Dependency;
end;
<event('InitializeWizard')>
procedure Dependency_InitializeWizard;
begin
Dependency_DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
end;
<event('PrepareToInstall')>
function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String;
var
DependencyCount, DependencyIndex, ResultCode: Integer;
Retry: Boolean;
TempValue: String;
begin
DependencyCount := GetArrayLength(Dependency_List);
if DependencyCount > 0 then begin
Dependency_DownloadPage.Show;
for DependencyIndex := 0 to DependencyCount - 1 do begin
if Dependency_List[DependencyIndex].URL <> '' then begin
Dependency_DownloadPage.Clear;
Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum);
Retry := True;
while Retry do begin
Retry := False;
try
Dependency_DownloadPage.Download;
except
if Dependency_DownloadPage.AbortedByUser then begin
Result := Dependency_List[DependencyIndex].Title;
DependencyIndex := DependencyCount;
end else begin
case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
IDABORT: begin
Result := Dependency_List[DependencyIndex].Title;
DependencyIndex := DependencyCount;
end;
IDRETRY: begin
Retry := True;
end;
end;
end;
end;
end;
end;
end;
if Result = '' then begin
for DependencyIndex := 0 to DependencyCount - 1 do begin
Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, '');
Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1);
while True do begin
ResultCode := 0;
#ifdef Dependency_CustomExecute
if {#Dependency_CustomExecute}(ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, ResultCode) then begin
#else
if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
#endif
if Dependency_List[DependencyIndex].RestartAfter then begin
if DependencyIndex = DependencyCount - 1 then begin
Dependency_NeedToRestart := True;
end else begin
NeedsRestart := True;
Result := Dependency_List[DependencyIndex].Title;
end;
break;
end else if (ResultCode = 0) or Dependency_List[DependencyIndex].ForceSuccess then begin // ERROR_SUCCESS (0)
break;
end else if ResultCode = 1641 then begin // ERROR_SUCCESS_REBOOT_INITIATED (1641)
NeedsRestart := True;
Result := Dependency_List[DependencyIndex].Title;
break;
end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010)
Dependency_NeedToRestart := True;
break;
end;
end;
case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependency_List[DependencyIndex].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
IDABORT: begin
Result := Dependency_List[DependencyIndex].Title;
break;
end;
IDIGNORE: begin
break;
end;
end;
end;
if Result <> '' then begin
break;
end;
end;
if NeedsRestart then begin
TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"';
if WizardNoIcons then begin
TempValue := TempValue + ' /NOICONS';
end;
RegWriteStringValue(HKA, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', '{#SetupSetting("AppName")}', TempValue);
end;
end;
Dependency_DownloadPage.Hide;
end;
end;
#ifndef Dependency_NoUpdateReadyMemo
<event('UpdateReadyMemo')>
#endif
function Dependency_UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
begin
Result := '';
if MemoUserInfoInfo <> '' then begin
Result := Result + MemoUserInfoInfo + Newline + NewLine;
end;
if MemoDirInfo <> '' then begin
Result := Result + MemoDirInfo + Newline + NewLine;
end;
if MemoTypeInfo <> '' then begin
Result := Result + MemoTypeInfo + Newline + NewLine;
end;
if MemoComponentsInfo <> '' then begin
Result := Result + MemoComponentsInfo + Newline + NewLine;
end;
if MemoGroupInfo <> '' then begin
Result := Result + MemoGroupInfo + Newline + NewLine;
end;
if MemoTasksInfo <> '' then begin
Result := Result + MemoTasksInfo;
end;
if Dependency_Memo <> '' then begin
if MemoTasksInfo = '' then begin
Result := Result + SetupMessage(msgReadyMemoTasks);
end;
Result := Result + FmtMessage(Dependency_Memo, [Space]);
end;
end;
<event('NeedRestart')>
function Dependency_NeedRestart: Boolean;
begin
Result := Dependency_NeedToRestart;
end;
function Dependency_IsX64: Boolean;
begin
Result := not Dependency_ForceX86 and Is64BitInstallMode;
end;
function Dependency_String(const x86, x64: String): String;
begin
if Dependency_IsX64 then begin
Result := x64;
end else begin
Result := x86;
end;
end;
function Dependency_ArchSuffix: String;
begin
Result := Dependency_String('', '_x64');
end;
function Dependency_ArchTitle: String;
begin
Result := Dependency_String(' (x86)', ' (x64)');
end;
function Dependency_IsNetCoreInstalled(const Version: String): Boolean;
var
ResultCode: Integer;
begin
// source code: https://github.com/dotnet/deployment-tools/tree/main/src/clickonce/native/projects/NetCoreCheck
if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe') then begin
ExtractTemporaryFile('netcorecheck' + Dependency_ArchSuffix + '.exe');
end;
Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + Dependency_ArchSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
end;
procedure Dependency_AddDotNet35;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1
if not IsDotNetInstalled(net35, 1) then begin
Dependency_Add('dotnetfx35.exe',
'/lang:enu /passive /norestart',
'.NET Framework 3.5 Service Pack 1',
'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe',
'', False, False);
end;
end;
procedure Dependency_AddDotNet40;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net40
if not IsDotNetInstalled(net4full, 0) then begin
Dependency_Add('dotNetFx40_Full_setup.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.0',
'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe',
'', False, False);
end;
end;
procedure Dependency_AddDotNet45;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net452
if not IsDotNetInstalled(net452, 0) then begin
Dependency_Add('dotnetfx45.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.5.2',
'https://go.microsoft.com/fwlink/?LinkId=397707',
'', False, False);
end;
end;
procedure Dependency_AddDotNet46;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net462
if not IsDotNetInstalled(net462, 0) then begin
Dependency_Add('dotnetfx46.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.6.2',
'https://go.microsoft.com/fwlink/?linkid=780596',
'', False, False);
end;
end;
procedure Dependency_AddDotNet47;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net472
if not IsDotNetInstalled(net472, 0) then begin
Dependency_Add('dotnetfx47.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.7.2',
'https://go.microsoft.com/fwlink/?LinkId=863262',
'', False, False);
end;
end;
procedure Dependency_AddDotNet48;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net48
if not IsDotNetInstalled(net48, 0) then begin
Dependency_Add('dotnetfx48.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.8',
'https://go.microsoft.com/fwlink/?LinkId=2085155',
'', False, False);
end;
end;
procedure Dependency_AddDotNet481;
var
Version: Cardinal;
begin
// https://dotnet.microsoft.com/download/dotnet-framework/net481
if not RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Version) or (Version < 533320) then begin
Dependency_Add('dotnetfx481.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Framework 4.8.1',
'https://go.microsoft.com/fwlink/?LinkId=2203304',
'', False, False);
end;
end;
procedure Dependency_AddNetCore31;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 3.1.32') then begin
Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/de4b3438-24a2-4d1d-a845-97355cf97b71/515abb880478b49f7c1bced8fbf07b16/dotnet-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddNetCore31Asp;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 3.1.32') then begin
Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/63b482d2-04b2-4dd4-baaf-d1e78de80738/40321091c872f4e77337b68fc61a5a07/aspnetcore-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddNetCore31Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet-core/3.1
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 3.1.32') then begin
Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 3.1.32' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3f353d2c-0431-48c5-bdf6-fbbe8f901bb5/542a4af07c1df5136a98a1c2df6f3d62/windowsdesktop-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 5.0.17') then begin
Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/54683c13-6b04-4d7d-b4d4-1f055b50ea43/e99048e2840d57040e8312058853a5b9/dotnet-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a0832b5a-6900-442b-af79-6ffddddd6ba4/e2df0b25dd851ee0b38a86947dd0e42e/dotnet-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 5.0.17') then begin
Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4bfa247d-321d-4b29-a34b-62320849059b/8df7a17d9aad4044efe9b5b1c423e82c/aspnetcore-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3789ec90-2717-424f-8b9c-3adbbcea6c16/2085cc5ff077b8789ff938015392e406/aspnetcore-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet50Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/5.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 5.0.17') then begin
Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 5.0.17' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b6fe5f2a-95f4-46f1-9824-f5994f10bc69/db5ec9b47ec877b5276f83a185fdb6a0/windowsdesktop-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3aa4e942-42cd-4bf5-afe7-fc23bd9c69c5/64da54c8864e473c19a7d3de15790418/windowsdesktop-runtime-5.0.17-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 6.0.20') then begin
Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3be5ee3a-c171-4cd2-ab98-00ca5c11eb8c/6fd31294b0c6c670ab5c060592935203/dotnet-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3cfb6d2a-afbe-4ae7-8e5b-776f350654cc/6e8d858a60fe15381f3c84d8ca66c4a7/dotnet-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 6.0.20') then begin
Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0e37c76c-53b4-4eea-8f5c-6ad2f8d5fe3c/88a8620329ced1aee271992a5b56d236/aspnetcore-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/be9f67fd-60af-45b1-9bca-a7bcc0e86e7e/6a750f7d7432937b3999bb4c5325062a/aspnetcore-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet60Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/6.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 6.0.20') then begin
Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 6.0.20' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/0413b619-3eb2-4178-a78e-8d1aafab1a01/5247f08ea3c13849b68074a2142fbf31/windowsdesktop-runtime-6.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1146f414-17c7-4184-8b10-1addfa5315e4/39db5573efb029130add485566320d74/windowsdesktop-runtime-6.0.20-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 7.0.9') then begin
Dependency_Add('dotnet70' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/305a85f5-2b0d-459b-b2ea-caf71b98d25d/805edc610efa49432e5e268bbba4eacb/dotnet-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/73058888-02a4-4f6d-b3cd-845531c2d7d0/a785e54b7f12046c00714b2ba759e173/dotnet-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 7.0.9') then begin
Dependency_Add('dotnet70asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/6ec3b357-31df-4b18-948f-4979a5b4b99f/fdeec71fc7f0f34ecfa0cb8b2b897da0/aspnetcore-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/edd9c9b1-0c49-4297-9197-9392b2462318/d06fedaefb256d801ce94ade76af3ad9/aspnetcore-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet70Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/7.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 7.0.9') then begin
Dependency_Add('dotnet70desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 7.0.9' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/139b19d0-2d39-48ce-b59a-aec437509c20/ea6a2711eec53660c3b14d78b9fb2963/windowsdesktop-runtime-7.0.9-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/7727acb3-25ca-473b-a392-75afeb33cab7/f11f0477fd2fcfbb3111881377d0c9bb/windowsdesktop-runtime-7.0.9-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.NETCore.App -v 8.0.3') then begin
Dependency_Add('dotnet80' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c8d7a77c-5647-4e38-9ed8-edf82328497d/56130e071ac13c3660b0f3a0d60914c7/dotnet-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/961dfc84-ea72-48a2-b3f4-b82cefc34580/6ac50b6bf244a2c5481ad705a92cf843/dotnet-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80Asp;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.AspNetCore.App -v 8.0.3') then begin
Dependency_Add('dotnet80asp' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'ASP.NET Core Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/e1efd12b-9598-4b70-ad83-496563ae3f7c/da67696e4232886f52d50bb8ecda5ab1/aspnetcore-runtime-8.0.3-win-x86.zip', 'https://download.visualstudio.microsoft.com/download/pr/e91876a9-1760-42cb-a6f4-97c57e9cca52/b433fcf4768929539f17e1908cb315bf/aspnetcore-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDotNet80Desktop;
begin
// https://dotnet.microsoft.com/download/dotnet/8.0
if not Dependency_IsNetCoreInstalled('-n Microsoft.WindowsDesktop.App -v 8.0.3') then begin
Dependency_Add('dotnet80desktop' + Dependency_ArchSuffix + '.exe',
'/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
'.NET Desktop Runtime 8.0.3' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/c629f243-5125-4751-a5ff-e78fa45646b1/85777e3e3f58f863d884fd4b8a1453f2/windowsdesktop-runtime-8.0.3-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/51bc18ac-0594-412d-bd63-18ece4c91ac4/90b47b97c3bfe40a833791b166697e67/windowsdesktop-runtime-8.0.3-win-x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2005;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26347
if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin
Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe',
'/q',
'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'),
'', False, False);
end;
end;
procedure Dependency_AddVC2008;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26368
if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin
Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe',
'/q',
'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2010;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=26999
if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin
Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2012;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=30679
if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin
Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2013;
begin
// https://support.microsoft.com/en-us/help/4032938
if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin
Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddVC2015To2022;
begin
// https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist
if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 30, 30704, 0)) then begin
Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe',
'/passive /norestart',
'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle,
Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddDirectX;
begin
#ifdef Dependency_Files_DirectX
ExtractTemporaryFile('dxwebsetup.exe');
#endif
// https://www.microsoft.com/en-us/download/details.aspx?id=35
Dependency_Add('dxwebsetup.exe',
'/q',
'DirectX Runtime',
'https://download.microsoft.com/download/1/7/1/1718CCC4-6315-4D8E-9543-8E28A4E18C4C/dxwebsetup.exe',
'', True, False);
end;
procedure Dependency_AddSql2008Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=30438
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin
Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2008 R2 Service Pack 2 Express',
Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2012Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=56042
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin
Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2012 Service Pack 4 Express',
Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2014Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=57473
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin
Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2014 Service Pack 3 Express',
Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'),
'', False, False);
end;
end;
procedure Dependency_AddSql2016Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=103447
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 6404, 1)) < 0) then begin
Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2016 Service Pack 3 Express',
'https://download.microsoft.com/download/f/a/8/fa83d147-63d1-449c-b22d-5fef9bd5bb46/SQLServer2016-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2017Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=55994
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin
Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2017 Express',
'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2019Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=101064
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin
Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2019 Express',
'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe',
'', False, False);
end;
end;
procedure Dependency_AddSql2022Express;
var
Version: String;
PackedVersion: Int64;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=104781
if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(16, 0, 1000, 6)) < 0) then begin
Dependency_Add('sql2022express' + Dependency_ArchSuffix + '.exe',
'/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER',
'SQL Server 2022 Express',
'https://go.microsoft.com/fwlink/p/?linkid=2216019',
'', False, False);
end;
end;
procedure Dependency_AddWebView2;
begin
// https://developer.microsoft.com/en-us/microsoft-edge/webview2
if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin
Dependency_Add('MicrosoftEdgeWebview2Setup.exe',
'/silent /install',
'WebView2 Runtime',
'https://go.microsoft.com/fwlink/p/?LinkId=2124703',
'', False, False);
end;
end;
procedure Dependency_AddAccessDatabaseEngine2010;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=13255
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\14.0\Access Connectivity Engine\Engines\ACE') then begin
Dependency_Add('AccessDatabaseEngine2010' + Dependency_ArchSuffix + '.exe',
'/quiet',
'Microsoft Access Database Engine 2010' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine.exe', 'https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe'),
'', False, False);
end;
end;
procedure Dependency_AddAccessDatabaseEngine2016;
begin
// https://www.microsoft.com/en-us/download/details.aspx?id=54920
if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\16.0\Access Connectivity Engine\Engines\ACE') then begin
Dependency_Add('AccessDatabaseEngine2016' + Dependency_ArchSuffix + '.exe',
'/quiet',
'Microsoft Access Database Engine 2016' + Dependency_ArchTitle,
Dependency_String('https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe'),
'', False, False);
end;
end;
[Files]
#ifdef Dependency_Path_NetCoreCheck
; download netcorecheck.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x86
; download netcorecheck_x64.exe: https://www.nuget.org/packages/Microsoft.NET.Tools.NETCoreCheck.x64
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck.exe"; Flags: dontcopy noencryption
Source: "{#Dependency_Path_NetCoreCheck}netcorecheck_x64.exe"; Flags: dontcopy noencryption
#endif
#ifdef Dependency_Path_DirectX
Source: "{#Dependency_Path_DirectX}dxwebsetup.exe"; Flags: dontcopy noencryption
#endif

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -76,10 +76,10 @@
"playGameServerCustomContent": "Enter IP", "playGameServerCustomContent": "Enter IP",
"settingsName": "Settings", "settingsName": "Settings",
"settingsClientName": "Internal files", "settingsClientName": "Internal files",
"settingsClientDescription": "Configure the internal files used by the launcher for Fortnite", "settingsClientDescription": "Configure the internal files used by the launcher",
"settingsClientOptionsName": "Options", "settingsClientOptionsName": "Options",
"settingsClientOptionsDescription": "Configure additional options for Fortnite", "settingsClientOptionsDescription": "Configure additional options for Fortnite",
"settingsClientConsoleName": "Unreal engine console", "settingsClientConsoleName": "Unreal engine patcher",
"settingsClientConsoleDescription": "Unlocks the Unreal Engine Console", "settingsClientConsoleDescription": "Unlocks the Unreal Engine Console",
"settingsClientConsoleKeyName": "Unreal engine console key", "settingsClientConsoleKeyName": "Unreal engine console key",
"settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console", "settingsClientConsoleKeyDescription": "The keyboard key used to open the Unreal Engine console",
@@ -88,24 +88,25 @@
"settingsClientMemoryName": "Memory patcher", "settingsClientMemoryName": "Memory patcher",
"settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak", "settingsClientMemoryDescription": "Prevents the client from crashing because of a memory leak",
"settingsClientArgsName": "Custom launch arguments", "settingsClientArgsName": "Custom launch arguments",
"settingsClientArgsDescription": "Additional arguments to use when launching the game", "settingsClientArgsDescription": "Additional arguments to use when launching Fortnite",
"settingsClientArgsPlaceholder": "Arguments...", "settingsClientArgsPlaceholder": "Arguments...",
"settingsServerName": "Internal files", "settingsServerName": "Internal files",
"settingsServerSubtitle": "Configure the internal files used by the launcher for the game server", "settingsServerSubtitle": "Configure the internal files used by the launcher for the game server",
"settingsServerOptionsName": "Options", "settingsServerOptionsName": "Options",
"settingsServerOptionsSubtitle": "Configure additional options for the game server", "settingsServerOptionsSubtitle": "Configure additional options for the game server",
"settingsServerTypeName": "Type", "settingsServerTypeName": "Game server type",
"settingsServerTypeDescription": "The type of game server to inject", "settingsServerTypeDescription": "The type of game server to inject",
"settingsServerTypeEmbeddedName": "Embedded", "settingsServerTypeEmbeddedName": "Embedded",
"settingsServerTypeCustomName": "Custom", "settingsServerTypeCustomName": "Custom",
"settingsServerFileName": "Implementation", "settingsOldServerFileName": "Game server",
"settingsServerFileDescription": "The file injected to create the game server", "settingsServerFileDescription": "The file injected to create the game server",
"settingsServerPortName": "Port", "settingsServerPortName": "Port",
"settingsServerPortDescription": "The port the launcher expects the game server to be hosted on", "settingsServerPortDescription": "The port the launcher expects the game server to be hosted on",
"settingsServerMirrorName": "Update mirror", "settingsServerOldMirrorName": "Update mirror (Before season 20)",
"settingsServerNewMirrorName": "Update mirror (Season 20 and above)",
"settingsServerMirrorDescription": "The URL used to update the game server dll", "settingsServerMirrorDescription": "The URL used to update the game server dll",
"settingsServerMirrorPlaceholder": "mirror", "settingsServerMirrorPlaceholder": "mirror",
"settingsServerTimerName": "Update timer", "settingsServerTimerName": "Game server updater",
"settingsServerTimerSubtitle": "Determines when the game server should be updated", "settingsServerTimerSubtitle": "Determines when the game server should be updated",
"settingsUtilsName": "Launcher", "settingsUtilsName": "Launcher",
"settingsUtilsSubtitle": "This section contains settings related to the launcher", "settingsUtilsSubtitle": "This section contains settings related to the launcher",
@@ -118,9 +119,7 @@
"settingsUtilsResetDefaultsName": "Reset settings", "settingsUtilsResetDefaultsName": "Reset settings",
"settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values", "settingsUtilsResetDefaultsSubtitle": "Resets the launcher's settings to their default values",
"settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible", "settingsUtilsDialogTitle": "Do you want to reset all the setting in this tab to their default values? This action is irreversible",
"settingsUtilsResetDefaultsContent": "Reset",
"settingsUtilsDialogSecondaryAction": "Close", "settingsUtilsDialogSecondaryAction": "Close",
"settingsUtilsDialogPrimaryAction": "Reset",
"selectFortniteName": "Fortnite version", "selectFortniteName": "Fortnite version",
"selectFortniteDescription": "Select the version of Fortnite you want to use", "selectFortniteDescription": "Select the version of Fortnite you want to use",
"manageVersionsName": "Manage versions", "manageVersionsName": "Manage versions",
@@ -147,6 +146,7 @@
"defaultServerName": "Reboot Game Server", "defaultServerName": "Reboot Game Server",
"defaultServerDescription": "Just another server", "defaultServerDescription": "Just another server",
"downloadingDll": "Downloading {name} dll...", "downloadingDll": "Downloading {name} dll...",
"dllAlreadyExists": "The {name} was already downloaded",
"downloadDllSuccess": "The {name} dll was downloaded successfully", "downloadDllSuccess": "The {name} dll was downloaded successfully",
"downloadDllError": "An error occurred while downloading {name}: {error}", "downloadDllError": "An error occurred while downloading {name}: {error}",
"downloadDllRetry": "Retry", "downloadDllRetry": "Retry",
@@ -156,6 +156,7 @@
"launchingGameClientAndServer": "Launching the game client and server...", "launchingGameClientAndServer": "Launching the game client and server...",
"startGameServer": "Start a game server", "startGameServer": "Start a game server",
"usernameOrEmail": "Username/Email", "usernameOrEmail": "Username/Email",
"invalidEmail": "Invalid email",
"usernameOrEmailPlaceholder": "Type your username or email", "usernameOrEmailPlaceholder": "Type your username or email",
"password": "Password", "password": "Password",
"passwordPlaceholder": "Type your password, if you want to use one", "passwordPlaceholder": "Type your password, if you want to use one",
@@ -215,6 +216,7 @@
"downloadedVersion": "The download was completed successfully!", "downloadedVersion": "The download was completed successfully!",
"download": "Download", "download": "Download",
"downloading": "Downloading...", "downloading": "Downloading...",
"startingDownload": "Starting download...",
"extracting": "Extracting...", "extracting": "Extracting...",
"buildProgress": "{progress}%", "buildProgress": "{progress}%",
"buildInstallationDirectory": "Installation directory", "buildInstallationDirectory": "Installation directory",
@@ -234,7 +236,7 @@
"startGame": "Start fortnite", "startGame": "Start fortnite",
"stopGame": "Close fortnite", "stopGame": "Close fortnite",
"waitingForGameServer": "Waiting for the game server to boot up...", "waitingForGameServer": "Waiting for the game server to boot up...",
"gameServerStartWarning": "The game server was started successfully, but Reboot didn't load", "gameServerStartWarning": "Unsupported version: the game server crashed while setting up the server",
"gameServerStartLocalWarning": "The game server was started successfully, but other players can't join", "gameServerStartLocalWarning": "The game server was started successfully, but other players can't join",
"gameServerStarted": "The game server was started successfully", "gameServerStarted": "The game server was started successfully",
"gameClientStarted": "The game client was started successfully", "gameClientStarted": "The game client was started successfully",
@@ -261,6 +263,7 @@
"missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings", "missingCustomDllError": "The custom {dll}.dll doesn't exist: check your settings",
"tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})", "tokenError": "Cannot log in into Fortnite: authentication error (injected dlls: {dlls})",
"unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}", "unknownFortniteError": "An unknown error occurred while launching Fortnite: {error}",
"fortniteCrashError": "The {name} crashed after being launched",
"serverNoLongerAvailableUnnamed": "The previous server is no longer available", "serverNoLongerAvailableUnnamed": "The previous server is no longer available",
"noServerFound": "No server found: invalid or expired link", "noServerFound": "No server found: invalid or expired link",
"settingsUtilsThemeName": "Theme", "settingsUtilsThemeName": "Theme",
@@ -320,8 +323,11 @@
"none": "none", "none": "none",
"openLog": "Open log", "openLog": "Open log",
"backendProcessError": "The backend shut down unexpectedly", "backendProcessError": "The backend shut down unexpectedly",
"backendErrorMessage": "The backend reported an unexpected error",
"welcomeTitle": "Welcome to Reboot Launcher", "welcomeTitle": "Welcome to Reboot Launcher",
"welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab", "welcomeDescription": "If you have never used a Fortnite game server, or this launcher in particular, please click on take a tour\nPlease don't ask for support on Discord without taking the tour: this helps me prioritize real bugs\nYou can always take the tour again in the Info tab",
"hostAccountText": "The host tab shows different credentials compared to the play tab.\nIf you are advanced user, you can set a different email and password\nhere if the backend you are using needs authentication.",
"hostAccountAction": "I understand",
"welcomeAction": "Take the tour", "welcomeAction": "Take the tour",
"startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.", "startOnboardingText": "Start by choosing a username: this will be visible to other players on Fortnite.\nIf you are advanced user, you can set the email and password here if the backend\nyou are using supports authentication.",
"startOnboardingActionLabel": "Let's do it", "startOnboardingActionLabel": "Let's do it",
@@ -359,9 +365,14 @@
"promptBackendDetachedActionLabel": "Next", "promptBackendDetachedActionLabel": "Next",
"promptInfoTabText": "The Info tab contains useful links to report bugs and receive support", "promptInfoTabText": "The Info tab contains useful links to report bugs and receive support",
"promptInfoTabActionLabel": "Next", "promptInfoTabActionLabel": "Next",
"promptSettingsTabText": "The Settings tab contains options to customize and reset the launcher", "promptSettingsTabText": "The Settings tab contains options to customize the launcher",
"promptSettingsTabActionLabel": "Done", "promptSettingsTabActionLabel": "Done",
"automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!", "automaticGameServerDialogContent": "The launcher detected that you are not running a game server, but that your matchmaker is set to your local machine. If you don't want to join another player's server, you should start a game server. This is necessary to be able to play!",
"automaticGameServerDialogIgnore": "Ignore", "automaticGameServerDialogIgnore": "Ignore",
"automaticGameServerDialogStart": "Start server" "automaticGameServerDialogStart": "Start server",
"gameResetDefaultsName": "Reset",
"gameResetDefaultsDescription": "Resets the game's settings to their default values",
"gameResetDefaultsContent": "Reset",
"selectFile": "Select a file",
"reset": "Reset"
} }

View File

@@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:flutter_gen/gen_l10n/reboot_localizations.dart'; import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
@@ -12,16 +11,16 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/error.dart'; import 'package:reboot_launcher/src/messenger/implementation/error.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/implementation/home_page.dart'; import 'package:reboot_launcher/src/page/implementation/home_page.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/url_protocol.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
import 'package:url_protocol/url_protocol.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -146,33 +145,34 @@ Future<Object?> _initVersion() async {
Future<Object?> _initUrlHandler() async { Future<Object?> _initUrlHandler() async {
try { try {
registerProtocolHandler(kCustomUrlSchema, arguments: ['%s']); registerUrlProtocol(kCustomUrlSchema, arguments: ['%s']);
return null; return null;
}catch(error) { }catch(error) {
return error; return error;
} }
} }
void _initWindow() => doWhenWindowReady(() async { Future<void> _initWindow() async {
try { try {
await SystemTheme.accentColor.load(); await SystemTheme.accentColor.load();
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await Window.initialize(); await Window.initialize();
var settingsController = Get.find<SettingsController>(); var settingsController = Get.find<SettingsController>();
var size = Size(settingsController.width, settingsController.height); var size = Size(settingsController.width, settingsController.height);
appWindow.size = size; await windowManager.setSize(size);
var offsetX = settingsController.offsetX; var offsetX = settingsController.offsetX;
var offsetY = settingsController.offsetY; var offsetY = settingsController.offsetY;
if(offsetX != null && offsetY != null){ if(offsetX != null && offsetY != null) {
appWindow.position = Offset( final position = Offset(
offsetX, offsetX,
offsetY offsetY
); );
await windowManager.setPosition(position);
}else { }else {
appWindow.alignment = Alignment.center; await windowManager.setAlignment(Alignment.center);
} }
await windowManager.setPreventClose(true);
appWindow.minSize = const Size(kDefaultWindowWidth, kDefaultWindowHeight); await windowManager.setResizable(true);
if(isWin11) { if(isWin11) {
await Window.setEffect( await Window.setEffect(
effect: WindowEffect.acrylic, effect: WindowEffect.acrylic,
@@ -183,17 +183,18 @@ void _initWindow() => doWhenWindowReady(() async {
}catch(error, stackTrace) { }catch(error, stackTrace) {
onError(error, stackTrace, false); onError(error, stackTrace, false);
}finally { }finally {
appWindow.show(); windowManager.show();
} }
}); }
Future<List<Object>> _initStorage() async { Future<List<Object>> _initStorage() async {
final errors = <Object>[]; final errors = <Object>[];
try { try {
await GetStorage("game_storage", settingsDirectory.path).initStorage; await GetStorage(GameController.storageName, settingsDirectory.path).initStorage;
await GetStorage("backend_storage", settingsDirectory.path).initStorage; await GetStorage(BackendController.storageName, settingsDirectory.path).initStorage;
await GetStorage("settings_storage", settingsDirectory.path).initStorage; await GetStorage(SettingsController.storageName, settingsDirectory.path).initStorage;
await GetStorage("hosting_storage", settingsDirectory.path).initStorage; await GetStorage(HostingController.storageName, settingsDirectory.path).initStorage;
await GetStorage(DllController.storageName, settingsDirectory.path).initStorage;
}catch(error) { }catch(error) {
appWithNoStorage = true; appWithNoStorage = true;
errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage"); errors.add("The Reboot Launcher configuration in ${settingsDirectory.path} cannot be accessed: running with in memory storage");
@@ -225,6 +226,12 @@ Future<List<Object>> _initStorage() async {
errors.add(error); errors.add(error);
} }
try {
Get.put(DllController());
}catch(error) {
errors.add(error);
}
return errors; return errors;
} }

View File

@@ -2,32 +2,39 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/util/keyboard.dart';
class BackendController extends GetxController { class BackendController extends GetxController {
late final GetStorage? storage; static const String storageName = "v2_backend_storage";
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041);
late final GetStorage? _storage;
late final TextEditingController host; late final TextEditingController host;
late final TextEditingController port; late final TextEditingController port;
late final Rx<ServerType> type; late final Rx<ServerType> type;
late final TextEditingController gameServerAddress; late final TextEditingController gameServerAddress;
late final FocusNode gameServerAddressFocusNode; late final FocusNode gameServerAddressFocusNode;
late final Rx<PhysicalKeyboardKey> consoleKey;
late final RxBool started; late final RxBool started;
late final RxBool detached; late final RxBool detached;
StreamSubscription? worker; StreamSubscription? worker;
int? embeddedProcessPid;
HttpServer? localServer; HttpServer? localServer;
HttpServer? remoteServer; HttpServer? remoteServer;
BackendController() { BackendController() {
storage = appWithNoStorage ? null : GetStorage("backend_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
started = RxBool(false); started = RxBool(false);
type = Rx(ServerType.values.elementAt(storage?.read("type") ?? 0)); type = Rx(ServerType.values.elementAt(_storage?.read("type") ?? 0));
type.listen((value) { type.listen((value) {
host.text = _readHost(); host.text = _readHost();
port.text = _readPort(); port.text = _readPort();
storage?.write("type", value.index); _storage?.write("type", value.index);
if (!started.value) { if (!started.value) {
return; return;
} }
@@ -36,13 +43,13 @@ class BackendController extends GetxController {
}); });
host = TextEditingController(text: _readHost()); host = TextEditingController(text: _readHost());
host.addListener(() => host.addListener(() =>
storage?.write("${type.value.name}_host", host.text)); _storage?.write("${type.value.name}_host", host.text));
port = TextEditingController(text: _readPort()); port = TextEditingController(text: _readPort());
port.addListener(() => port.addListener(() =>
storage?.write("${type.value.name}_port", port.text)); _storage?.write("${type.value.name}_port", port.text));
detached = RxBool(storage?.read("detached") ?? false); detached = RxBool(_storage?.read("detached") ?? false);
detached.listen((value) => storage?.write("detached", value)); detached.listen((value) => _storage?.write("detached", value));
final address = storage?.read("game_server_address"); final address = _storage?.read("game_server_address");
gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address); gameServerAddress = TextEditingController(text: address == null || address.isEmpty ? "127.0.0.1" : address);
var lastValue = gameServerAddress.text; var lastValue = gameServerAddress.text;
writeMatchmakingIp(lastValue); writeMatchmakingIp(lastValue);
@@ -54,7 +61,7 @@ class BackendController extends GetxController {
lastValue = newValue; lastValue = newValue;
gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length); gameServerAddress.selection = TextSelection.collapsed(offset: newValue.length);
storage?.write("game_server_address", newValue); _storage?.write("game_server_address", newValue);
writeMatchmakingIp(newValue); writeMatchmakingIp(newValue);
}); });
watchMatchmakingIp().listen((event) { watchMatchmakingIp().listen((event) {
@@ -63,6 +70,37 @@ class BackendController extends GetxController {
} }
}); });
gameServerAddressFocusNode = FocusNode(); gameServerAddressFocusNode = FocusNode();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
await defaultInput.parent.create(recursive: true);
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
} }
void joinLocalhost() { void joinLocalhost() {
@@ -72,18 +110,19 @@ class BackendController extends GetxController {
void reset() async { void reset() async {
type.value = ServerType.values.elementAt(0); type.value = ServerType.values.elementAt(0);
for (final type in ServerType.values) { for (final type in ServerType.values) {
storage?.write("${type.name}_host", null); _storage?.write("${type.name}_host", null);
storage?.write("${type.name}_port", null); _storage?.write("${type.name}_port", null);
} }
host.text = type.value != ServerType.remote ? kDefaultBackendHost : ""; host.text = type.value != ServerType.remote ? kDefaultBackendHost : "";
port.text = kDefaultBackendPort.toString(); port.text = kDefaultBackendPort.toString();
gameServerAddress.text = "127.0.0.1"; gameServerAddress.text = "127.0.0.1";
consoleKey.value = _kDefaultConsoleKey;
detached.value = false; detached.value = false;
} }
String _readHost() { String _readHost() {
String? value = storage?.read("${type.value.name}_host"); String? value = _storage?.read("${type.value.name}_host");
if (value != null && value.isNotEmpty) { if (value != null && value.isNotEmpty) {
return value; return value;
} }
@@ -96,24 +135,20 @@ class BackendController extends GetxController {
} }
String _readPort() => String _readPort() =>
storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString(); _storage?.read("${type.value.name}_port") ?? kDefaultBackendPort.toString();
Stream<ServerResult> start() async* { Stream<ServerResult> start({required void Function() onExit, required void Function(String) onError}) async* {
try { try {
if(started.value) { if(started.value) {
return; return;
} }
final serverType = type.value;
final hostData = this.host.text.trim(); final hostData = this.host.text.trim();
final portData = this.port.text.trim(); final portData = this.port.text.trim();
if(type() != ServerType.local) {
started.value = true; started.value = true;
if(serverType != ServerType.local || portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting); yield ServerResult(ServerResultType.starting);
}else {
started.value = false;
if(portData != kDefaultBackendPort.toString()) {
yield ServerResult(ServerResultType.starting);
}
} }
if (hostData.isEmpty) { if (hostData.isEmpty) {
@@ -135,7 +170,7 @@ class BackendController extends GetxController {
return; return;
} }
if ((type() != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) { if ((serverType != ServerType.local || portData != kDefaultBackendPort.toString()) && !(await isBackendPortFree())) {
yield ServerResult(ServerResultType.freeingPort); yield ServerResult(ServerResultType.freeingPort);
final result = await freeBackendPort(); final result = await freeBackendPort();
yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError); yield ServerResult(result ? ServerResultType.freePortSuccess : ServerResultType.freePortError);
@@ -145,14 +180,21 @@ class BackendController extends GetxController {
} }
} }
switch(type()){ switch(serverType){
case ServerType.embedded: case ServerType.embedded:
final process = await startEmbeddedBackend(detached.value); final process = await startEmbeddedBackend(detached.value, onError: (errorMessage) {
watchProcess(process.pid) if(started.value) {
.asStream() started.value = false;
.asBroadcastStream() onError(errorMessage);
.where((_) => !started()) }
.map((_) => ServerResult(ServerResultType.processError)); });
watchProcess(process.pid).then((_) {
if(started.value) {
started.value = false;
onExit();
}
});
embeddedProcessPid = process.pid;
break; break;
case ServerType.remote: case ServerType.remote:
yield ServerResult(ServerResultType.pingingRemote); yield ServerResult(ServerResultType.pingingRemote);
@@ -166,8 +208,20 @@ class BackendController extends GetxController {
remoteServer = await startRemoteBackendProxy(uriResult); remoteServer = await startRemoteBackendProxy(uriResult);
break; break;
case ServerType.local: case ServerType.local:
if(portData != kDefaultBackendPort.toString()) { if(portNumber != kDefaultBackendPort) {
yield ServerResult(ServerResultType.pingingLocal);
final uriResult = await pingBackend(kDefaultBackendHost, portNumber);
if(uriResult == null) {
yield ServerResult(ServerResultType.pingError);
started.value = false;
return;
}
localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData")); localServer = await startRemoteBackendProxy(Uri.parse("http://$kDefaultBackendHost:$portData"));
}else {
// If the local server is running on port 3551 there is no reverse proxy running
// We only need to check if everything is working
started.value = false;
} }
break; break;
@@ -206,7 +260,11 @@ class BackendController extends GetxController {
try{ try{
switch(type()){ switch(type()){
case ServerType.embedded: case ServerType.embedded:
killProcessByPort(kDefaultBackendPort); final embeddedProcessPid = this.embeddedProcessPid;
if(embeddedProcessPid != null) {
Process.killPid(embeddedProcessPid, ProcessSignal.sigterm);
this.embeddedProcessPid = null;
}
break; break;
case ServerType.remote: case ServerType.remote:
await remoteServer?.close(force: true); await remoteServer?.close(force: true);
@@ -228,11 +286,14 @@ class BackendController extends GetxController {
} }
} }
Stream<ServerResult> toggle() async* { Stream<ServerResult> toggle({required void Function() onExit, required void Function(String) onError}) async* {
if(started()) { if(started()) {
yield* stop(); yield* stop();
}else { }else {
yield* start(); yield* start(
onExit: onExit,
onError: onError
);
} }
} }
} }

View File

@@ -0,0 +1,252 @@
import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart';
import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/util/translations.dart';
import 'package:version/version.dart';
class DllController extends GetxController {
static const String storageName = "v2_dll_storage";
late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController gameServerPort;
late final Rx<UpdateTimer> timer;
late final TextEditingController beforeS20Mirror;
late final TextEditingController aboveS20Mirror;
late final RxBool customGameServer;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
DllController() {
_storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.starfall);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
beforeS20Mirror = TextEditingController(text: _storage?.read("update_url") ?? kRebootBelowS20DownloadUrl);
beforeS20Mirror.addListener(() => _storage?.write("update_url", beforeS20Mirror.text));
aboveS20Mirror = TextEditingController(text: _storage?.read("old_update_url") ?? kRebootAboveS20DownloadUrl);
aboveS20Mirror.addListener(() => _storage?.write("new_update_url", aboveS20Mirror.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? getDefaultDllPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
}
void resetGame() {
gameServerDll.text = getDefaultDllPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = getDefaultDllPath(InjectableDll.console);
backendDll.text = getDefaultDllPath(InjectableDll.starfall);
}
void resetServer() {
gameServerPort.text = kDefaultGameServerPort;
timer.value = UpdateTimer.hour;
beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
aboveS20Mirror.text = kRebootAboveS20DownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
timestamp.value = null;
updateGameServerDll();
}
Future<bool> updateGameServerDll({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateGameServerDll(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateGameServerDll(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
await Future.wait(
[
downloadRebootDll(rebootBeforeS20DllFile, beforeS20Mirror.text),
downloadRebootDll(rebootAboveS20DllFile, aboveS20Mirror.text),
Future.delayed(const Duration(seconds: 1))
],
eagerError: false
);
timestamp.value = DateTime.now().millisecondsSinceEpoch;
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
infoBarEntry = showRebootInfoBar(
translations.downloadDllError(error.toString(), "reboot.dll"),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () async {
infoBarEntry?.close();
updateGameServerDll(
force: true,
silent: silent
);
},
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(Version version, InjectableDll dll) {
final defaultPath = canonicalize(getDefaultDllPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
return (File(gameServerDll.text), true);
}
return (version.major >= 20 ? rebootAboveS20DllFile : rebootBeforeS20DllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.starfall:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
}
}
String getDefaultDllPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false, bool force = false}) async {
log("[DLL] Asking for $filePath(silent: $silent)");
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName.contains("reboot")) {
log("[DLL] Downloading reboot.dll...");
return await updateGameServerDll(
silent: silent
);
}
if(!force && File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error =
error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(error.toString(), fileName),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
}

View File

@@ -1,18 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/util/keyboard.dart'; import 'package:reboot_launcher/main.dart';
import '../../main.dart';
class GameController extends GetxController { class GameController extends GetxController {
static const PhysicalKeyboardKey _kDefaultConsoleKey = PhysicalKeyboardKey(0x00070041); static const String storageName = "v2_game_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final TextEditingController username; late final TextEditingController username;
@@ -22,10 +18,9 @@ class GameController extends GetxController {
late final Rxn<FortniteVersion> _selectedVersion; late final Rxn<FortniteVersion> _selectedVersion;
late final RxBool started; late final RxBool started;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rx<PhysicalKeyboardKey> consoleKey;
GameController() { GameController() {
_storage = appWithNoStorage ? null : GetStorage("game_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]"); Iterable decodedVersionsJson = jsonDecode(_storage?.read("versions") ?? "[]");
final decodedVersions = decodedVersionsJson final decodedVersions = decodedVersionsJson
.map((entry) => FortniteVersion.fromJson(entry)) .map((entry) => FortniteVersion.fromJson(entry))
@@ -41,41 +36,9 @@ class GameController extends GetxController {
password = TextEditingController(text: _storage?.read("password") ?? ""); password = TextEditingController(text: _storage?.read("password") ?? "");
password.addListener(() => _storage?.write("password", password.text)); password.addListener(() => _storage?.write("password", password.text));
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? ""); customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_storage?.write("custom_launch_args", customLaunchArgs.text));
started = RxBool(false); started = RxBool(false);
instance = Rxn(); instance = Rxn();
consoleKey = Rx(_readConsoleKey());
_writeConsoleKey(consoleKey.value);
consoleKey.listen((newValue) {
_storage?.write("console_key", newValue.usbHidUsage);
_writeConsoleKey(newValue);
});
}
PhysicalKeyboardKey _readConsoleKey() {
final consoleKeyValue = _storage?.read("console_key");
if(consoleKeyValue == null) {
return _kDefaultConsoleKey;
}
final consoleKeyNumber = int.tryParse(consoleKeyValue.toString());
if(consoleKeyNumber == null) {
return _kDefaultConsoleKey;
}
final consoleKey = PhysicalKeyboardKey(consoleKeyNumber);
if(!consoleKey.isUnrealEngineKey) {
return _kDefaultConsoleKey;
}
return consoleKey;
}
Future<void> _writeConsoleKey(PhysicalKeyboardKey keyValue) async {
final defaultInput = File("${backendDirectory.path}\\CloudStorage\\DefaultInput.ini");
await defaultInput.parent.create(recursive: true);
await defaultInput.writeAsString("[/Script/Engine.InputSettings]\n+ConsoleKeys=Tilde\n+ConsoleKeys=${keyValue.unrealEngineName}", flush: true);
} }
void reset() { void reset() {
@@ -83,6 +46,7 @@ class GameController extends GetxController {
password.text = ""; password.text = "";
customLaunchArgs.text = ""; customLaunchArgs.text = "";
versions.value = []; versions.value = [];
_selectedVersion.value = null;
instance.value = null; instance.value = null;
} }

View File

@@ -12,8 +12,12 @@ import 'package:sync/semaphore.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class HostingController extends GetxController { class HostingController extends GetxController {
static const String storageName = "v2_hosting_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String uuid; late final String uuid;
late final TextEditingController accountUsername;
late final TextEditingController accountPassword;
late final TextEditingController name; late final TextEditingController name;
late final FocusNode nameFocusNode; late final FocusNode nameFocusNode;
late final TextEditingController description; late final TextEditingController description;
@@ -28,12 +32,17 @@ class HostingController extends GetxController {
late final RxBool published; late final RxBool published;
late final Rxn<GameInstance> instance; late final Rxn<GameInstance> instance;
late final Rxn<Set<FortniteServer>> servers; late final Rxn<Set<FortniteServer>> servers;
late final TextEditingController customLaunchArgs;
late final Semaphore _semaphore; late final Semaphore _semaphore;
HostingController() { HostingController() {
_storage = appWithNoStorage ? null : GetStorage("hosting_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
uuid = _storage?.read("uuid") ?? const Uuid().v4(); uuid = _storage?.read("uuid") ?? const Uuid().v4();
_storage?.write("uuid", uuid); _storage?.write("uuid", uuid);
accountUsername = TextEditingController(text: _storage?.read("account_username") ?? kDefaultHostName);
accountUsername.addListener(() => _storage?.write("account_username", accountUsername.text));
accountPassword = TextEditingController(text: _storage?.read("account_password") ?? "");
accountPassword.addListener(() => _storage?.write("account_password", password.text));
name = TextEditingController(text: _storage?.read("name")); name = TextEditingController(text: _storage?.read("name"));
name.addListener(() => _storage?.write("name", name.text)); name.addListener(() => _storage?.write("name", name.text));
description = TextEditingController(text: _storage?.read("description")); description = TextEditingController(text: _storage?.read("description"));
@@ -53,16 +62,34 @@ class HostingController extends GetxController {
published = RxBool(false); published = RxBool(false);
showPassword = RxBool(false); showPassword = RxBool(false);
instance = Rxn(); instance = Rxn();
final supabase = Supabase.instance.client;
servers = Rxn(); servers = Rxn();
_listenServers();
customLaunchArgs = TextEditingController(text: _storage?.read("custom_launch_args") ?? "");
customLaunchArgs.addListener(() => _storage?.write("custom_launch_args", customLaunchArgs.text));
_semaphore = Semaphore();
}
void _listenServers([int attempt = 0]) {
log("[SUPABASE] Listening...");
final supabase = Supabase.instance.client;
supabase.from("hosting_v2") supabase.from("hosting_v2")
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet()) .map((event) => event.map((element) => FortniteServer.fromJson(element)).where((element) => element.ip.isNotEmpty).toSet())
.listen((event) { .listen(
_onNewServer,
onError: (error) async {
log("[SUPABASE] Error: ${error}");
await Future.delayed(Duration(seconds: attempt * 5));
_listenServers(attempt + 1);
},
cancelOnError: true
);
}
void _onNewServer(Set<FortniteServer> event) {
log("[SUPABASE] New event: ${event}");
servers.value = event; servers.value = event;
published.value = event.any((element) => element.id == uuid); published.value = event.any((element) => element.id == uuid);
});
_semaphore = Semaphore();
} }
Future<void> publishServer(String author, String version) async { Future<void> publishServer(String author, String version) async {
@@ -131,14 +158,16 @@ class HostingController extends GetxController {
} }
void reset() { void reset() {
accountUsername.text = kDefaultHostName;
accountPassword.text = "";
name.text = ""; name.text = "";
description.text = ""; description.text = "";
showPassword.value = false; showPassword.value = false;
discoverable.value = false; discoverable.value = false;
started.value = false;
instance.value = null; instance.value = null;
type.value = GameServerType.headless; type.value = GameServerType.headless;
autoRestart.value = true; autoRestart.value = true;
customLaunchArgs.text = "";
} }
FortniteServer? findServerById(String uuid) { FortniteServer? findServerById(String uuid) {

View File

@@ -1,11 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:path/path.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
@@ -15,37 +13,19 @@ import 'package:version/version.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
class SettingsController extends GetxController { class SettingsController extends GetxController {
static const String storageName = "v2_settings_storage";
late final GetStorage? _storage; late final GetStorage? _storage;
late final String originalDll;
late final TextEditingController gameServerDll;
late final TextEditingController unrealEngineConsoleDll;
late final TextEditingController backendDll;
late final TextEditingController memoryLeakDll;
late final TextEditingController gameServerPort;
late final RxString language; late final RxString language;
late final Rx<ThemeMode> themeMode; late final Rx<ThemeMode> themeMode;
late final RxnInt timestamp;
late final Rx<UpdateStatus> status;
late final Rx<UpdateTimer> timer;
late final TextEditingController url;
late final RxBool customGameServer;
late final RxBool firstRun; late final RxBool firstRun;
late final Map<String, Future<bool>> _operations;
late double width; late double width;
late double height; late double height;
late double? offsetX; late double? offsetX;
late double? offsetY; late double? offsetY;
InfoBarEntry? infoBarEntry;
Future<bool>? _updater;
SettingsController() { SettingsController() {
_storage = appWithNoStorage ? null : GetStorage("settings_storage"); _storage = appWithNoStorage ? null : GetStorage(storageName);
gameServerDll = _createController("game_server", InjectableDll.reboot);
unrealEngineConsoleDll = _createController("unreal_engine_console", InjectableDll.console);
backendDll = _createController("backend", InjectableDll.cobalt);
memoryLeakDll = _createController("memory_leak", InjectableDll.memory);
gameServerPort = TextEditingController(text: _storage?.read("game_server_port") ?? kDefaultGameServerPort);
gameServerPort.addListener(() => _storage?.write("game_server_port", gameServerPort.text));
width = _storage?.read("width") ?? kDefaultWindowWidth; width = _storage?.read("width") ?? kDefaultWindowWidth;
height = _storage?.read("height") ?? kDefaultWindowHeight; height = _storage?.read("height") ?? kDefaultWindowHeight;
offsetX = _storage?.read("offset_x"); offsetX = _storage?.read("offset_x");
@@ -54,25 +34,8 @@ class SettingsController extends GetxController {
themeMode.listen((value) => _storage?.write("theme", value.index)); themeMode.listen((value) => _storage?.write("theme", value.index));
language = RxString(_storage?.read("language") ?? currentLocale); language = RxString(_storage?.read("language") ?? currentLocale);
language.listen((value) => _storage?.write("language", value)); language.listen((value) => _storage?.write("language", value));
timestamp = RxnInt(_storage?.read("ts"));
timestamp.listen((value) => _storage?.write("ts", value));
final timerIndex = _storage?.read("timer");
timer = Rx(timerIndex == null ? UpdateTimer.hour : UpdateTimer.values.elementAt(timerIndex));
timer.listen((value) => _storage?.write("timer", value.index));
url = TextEditingController(text: _storage?.read("update_url") ?? kRebootDownloadUrl);
url.addListener(() => _storage?.write("update_url", url.text));
status = Rx(UpdateStatus.waiting);
customGameServer = RxBool(_storage?.read("custom_game_server") ?? false);
customGameServer.listen((value) => _storage?.write("custom_game_server", value));
firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true); firstRun = RxBool(_storage?.read("first_run_tutorial") ?? true);
firstRun.listen((value) => _storage?.write("first_run_tutorial", value)); firstRun.listen((value) => _storage?.write("first_run_tutorial", value));
_operations = {};
}
TextEditingController _createController(String key, InjectableDll dll) {
final controller = TextEditingController(text: _storage?.read(key) ?? _getDefaultPath(dll));
controller.addListener(() => _storage?.write(key, controller.text));
return controller;
} }
void saveWindowSize(Size size) { void saveWindowSize(Size size) {
@@ -87,32 +50,18 @@ class SettingsController extends GetxController {
_storage?.write("offset_y", offsetY); _storage?.write("offset_y", offsetY);
} }
void reset(){
gameServerDll.text = _getDefaultPath(InjectableDll.reboot);
unrealEngineConsoleDll.text = _getDefaultPath(InjectableDll.console);
backendDll.text = _getDefaultPath(InjectableDll.cobalt);
memoryLeakDll.text = _getDefaultPath(InjectableDll.memory);
gameServerPort.text = kDefaultGameServerPort;
timestamp.value = null;
timer.value = UpdateTimer.never;
url.text = kRebootDownloadUrl;
status.value = UpdateStatus.waiting;
customGameServer.value = false;
updateReboot();
}
Future<void> notifyLauncherUpdate() async { Future<void> notifyLauncherUpdate() async {
if(appVersion == null) { if (appVersion == null) {
return; return;
} }
final pubspec = await _getPubspecYaml(); final pubspec = await _getPubspecYaml();
if(pubspec == null) { if (pubspec == null) {
return; return;
} }
final latestVersion = Version.parse(pubspec["version"]); final latestVersion = Version.parse(pubspec["version"]);
if(latestVersion <= appVersion) { if (latestVersion <= appVersion) {
return; return;
} }
@@ -125,7 +74,8 @@ class SettingsController extends GetxController {
child: Text(translations.updateAvailableAction), child: Text(translations.updateAvailableAction),
onPressed: () { onPressed: () {
infoBar.close(); infoBar.close();
launchUrl(Uri.parse("https://github.com/Auties00/reboot_launcher/releases")); launchUrl(Uri.parse(
"https://github.com/Auties00/reboot_launcher/releases"));
}, },
) )
); );
@@ -133,201 +83,16 @@ class SettingsController extends GetxController {
Future<dynamic> _getPubspecYaml() async { Future<dynamic> _getPubspecYaml() async {
try { try {
final pubspecResponse = await http.get(Uri.parse("https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml")); final pubspecResponse = await http.get(Uri.parse(
if(pubspecResponse.statusCode != 200) { "https://raw.githubusercontent.com/Auties00/reboot_launcher/master/gui/pubspec.yaml"));
if (pubspecResponse.statusCode != 200) {
return null; return null;
} }
return loadYaml(pubspecResponse.body); return loadYaml(pubspecResponse.body);
}catch(error) { } catch (error) {
log("[UPDATER] Cannot check for updates: $error"); log("[UPDATER] Cannot check for updates: $error");
return null; return null;
} }
} }
Future<bool> updateReboot({bool force = false, bool silent = false}) async {
if(_updater != null) {
return await _updater!;
}
final result = _updateReboot(force, silent);
_updater = result;
return await result;
}
Future<bool> _updateReboot(bool force, bool silent) async {
try {
if(customGameServer.value) {
status.value = UpdateStatus.success;
return true;
}
final needsUpdate = await hasRebootDllUpdate(
timestamp.value,
hours: timer.value.hours,
force: force
);
if(!needsUpdate) {
status.value = UpdateStatus.success;
return true;
}
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadingDll("reboot"),
loading: true,
duration: null
);
}
timestamp.value = await downloadRebootDll(url.text);
status.value = UpdateStatus.success;
infoBarEntry?.close();
if(!silent) {
infoBarEntry = showRebootInfoBar(
translations.downloadDllSuccess("reboot"),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
infoBarEntry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
status.value = UpdateStatus.error;
showRebootInfoBar(
translations.downloadDllError("reboot.dll", error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
action: Button(
onPressed: () => updateReboot(
force: true,
silent: silent
),
child: Text(translations.downloadDllRetry),
)
);
return false;
}finally {
_updater = null;
}
}
(File, bool) getInjectableData(InjectableDll dll) {
final defaultPath = canonicalize(_getDefaultPath(dll));
switch(dll){
case InjectableDll.reboot:
if(customGameServer.value) {
final file = File(gameServerDll.text);
if(file.existsSync()) {
return (file, true);
}
}
return (rebootDllFile, false);
case InjectableDll.console:
final ue4ConsoleFile = File(unrealEngineConsoleDll.text);
return (ue4ConsoleFile, canonicalize(ue4ConsoleFile.path) != defaultPath);
case InjectableDll.cobalt:
final backendFile = File(backendDll.text);
return (backendFile, canonicalize(backendFile.path) != defaultPath);
case InjectableDll.memory:
final memoryLeakFile = File(memoryLeakDll.text);
return (memoryLeakFile, canonicalize(memoryLeakFile.path) != defaultPath);
}
}
String _getDefaultPath(InjectableDll dll) => "${dllsDirectory.path}\\${dll.name}.dll";
Future<bool> downloadCriticalDllInteractive(String filePath, {bool silent = false}) {
log("[DLL] Asking for $filePath(silent: $silent)");
final old = _operations[filePath];
if(old != null) {
log("[DLL] Download task already exists");
return old;
}
log("[DLL] Creating new download task...");
final newRun = _downloadCriticalDllInteractive(filePath, silent);
_operations[filePath] = newRun;
return newRun;
}
Future<bool> _downloadCriticalDllInteractive(String filePath, bool silent) async {
final fileName = basename(filePath).toLowerCase();
log("[DLL] File name: $fileName");
InfoBarEntry? entry;
try {
if (fileName == "reboot.dll") {
log("[DLL] Downloading reboot.dll...");
return await updateReboot(
silent: silent
);
}
if(File(filePath).existsSync()) {
log("[DLL] File already exists");
return true;
}
final fileNameWithoutExtension = basenameWithoutExtension(filePath);
if(!silent) {
entry = showRebootInfoBar(
translations.downloadingDll(fileNameWithoutExtension),
loading: true,
duration: null
);
}
await downloadCriticalDll(fileName, filePath);
entry?.close();
if(!silent) {
entry = await showRebootInfoBar(
translations.downloadDllSuccess(fileNameWithoutExtension),
severity: InfoBarSeverity.success,
duration: infoBarShortDuration
);
}
return true;
}catch(message) {
log("[DLL] Error: $message");
entry?.close();
var error = message.toString();
error = error.contains(": ") ? error.substring(error.indexOf(": ") + 2) : error;
error = error.toLowerCase();
final completer = Completer();
await showRebootInfoBar(
translations.downloadDllError(fileName, error.toString()),
duration: infoBarLongDuration,
severity: InfoBarSeverity.error,
onDismissed: () => completer.complete(null),
action: Button(
onPressed: () async {
await downloadCriticalDllInteractive(filePath);
completer.complete(null);
},
child: Text(translations.downloadDllRetry),
)
);
await completer.future;
return false;
}finally {
_operations.remove(fileName);
}
}
}
extension _UpdateTimerExtension on UpdateTimer {
int get hours {
switch(this) {
case UpdateTimer.never:
return -1;
case UpdateTimer.hour:
return 1;
case UpdateTimer.day:
return 24;
case UpdateTimer.week:
return 24 * 7;
}
}
} }

View File

@@ -300,7 +300,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _primaryButton => Button( Widget get _primaryButton => Button(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor) backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
), ),
onPressed: widget.onTap!, onPressed: widget.onTap!,
child: Text(widget.text!), child: Text(widget.text!),
@@ -308,7 +308,7 @@ class _DialogButtonState extends State<DialogButton> {
Widget get _secondaryButton => Button( Widget get _secondaryButton => Button(
style: widget.color != null ? ButtonStyle( style: widget.color != null ? ButtonStyle(
backgroundColor: ButtonState.all(widget.color!) backgroundColor: WidgetStateProperty.all(widget.color!)
) : null, ) : null,
onPressed: widget.onTap ?? _onDefaultSecondaryActionTap, onPressed: widget.onTap ?? _onDefaultSecondaryActionTap,
child: Text(widget.text ?? translations.defaultDialogSecondaryAction), child: Text(widget.text ?? translations.defaultDialogSecondaryAction),

View File

@@ -23,19 +23,18 @@ InfoBarEntry showRebootInfoBar(dynamic text, {
Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox( Widget _buildOverlay(text, Widget? action, bool loading, InfoBarSeverity severity) => ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: _height minHeight: _height
), ),
child: Mica( child: Mica(
elevation: 1, elevation: 1,
child: InfoBar( child: InfoBar(
title: Row( title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if(text is Widget) Expanded(
text, child: text is Widget ? text : Text(text)
if(text is String) ),
Text(text),
if(action != null) if(action != null)
action action
], ],

View File

@@ -18,9 +18,12 @@ void onError(Object exception, StackTrace? stackTrace, bool framework) {
} }
lastError = exception.toString(); lastError = exception.toString();
final route = ModalRoute.of(pageKey.currentContext!); if(inDialog){
if(route != null && !route.isCurrent){ final context = pageKey.currentContext;
Navigator.of(pageKey.currentContext!).pop(false); if(context != null) {
Navigator.of(context).pop(false);
inDialog = false;
}
} }
WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog( WidgetsBinding.instance.addPostFrameCallback((timeStamp) => showRebootDialog(

View File

@@ -17,6 +17,7 @@ import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/version_selector.dart'; import 'package:reboot_launcher/src/widget/version_selector.dart';
void startOnboarding() { void startOnboarding() {
final gameController = Get.find<GameController>();
final settingsController = Get.find<SettingsController>(); final settingsController = Get.find<SettingsController>();
settingsController.firstRun.value = false; settingsController.firstRun.value = false;
profileOverlayKey.currentState!.showOverlay( profileOverlayKey.currentState!.showOverlay(
@@ -27,7 +28,7 @@ void startOnboarding() {
label: translations.startOnboardingActionLabel, label: translations.startOnboardingActionLabel,
onTap: () async { onTap: () async {
onClose(); onClose();
await showProfileForm(context); await showProfileForm(context, gameController.username, gameController.password);
_promptPlayPage(); _promptPlayPage();
} }
) )
@@ -62,7 +63,7 @@ void _promptPlayVersion() {
onTap: () async { onTap: () async {
onClose(); onClose();
if(!hasBuilds) { if(!hasBuilds) {
await VersionSelector.openDownloadDialog(closable: false); await VersionSelector.openDownloadDialog();
} }
_promptServerBrowserPage(); _promptServerBrowserPage();
} }
@@ -78,6 +79,22 @@ void _promptServerBrowserPage() {
context: context, context: context,
label: translations.promptServerBrowserPageActionLabel, label: translations.promptServerBrowserPageActionLabel,
onTap: () { onTap: () {
onClose();
_promptHostAccount();
}
)
);
}
void _promptHostAccount() {
pageIndex.value = RebootPageType.host.index;
profileOverlayKey.currentState!.showOverlay(
text: translations.hostAccountText,
offset: Offset(27.5, 17.5),
actionBuilder: (context, onClose) => _buildActionButton(
context: context,
label: translations.hostAccountAction,
onTap: () async {
onClose(); onClose();
_promptHostPage(); _promptHostPage();
} }
@@ -86,7 +103,6 @@ void _promptServerBrowserPage() {
} }
void _promptHostPage() { void _promptHostPage() {
pageIndex.value = RebootPageType.host.index;
pageOverlayTargetKey.currentState!.showOverlay( pageOverlayTargetKey.currentState!.showOverlay(
text: translations.promptHostPageText, text: translations.promptHostPageText,
actionBuilder: (context, onClose) => _buildActionButton( actionBuilder: (context, onClose) => _buildActionButton(
@@ -339,7 +355,7 @@ Widget _buildActionButton({
required void Function() onTap, required void Function() onTap,
}) => Button( }) => Button(
style: themed ? ButtonStyle( style: themed ? ButtonStyle(
backgroundColor: ButtonState.all(FluentTheme.of(context).accentColor) backgroundColor: WidgetStateProperty.all(FluentTheme.of(context).accentColor)
) : null, ) : null,
child: Text(label), child: Text(label),
onPressed: onTap onPressed: onTap

View File

@@ -1,3 +1,4 @@
import 'package:email_validator/email_validator.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/material.dart' show Icons; import 'package:flutter/material.dart' show Icons;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -5,13 +6,11 @@ import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
final GameController _gameController = Get.find<GameController>(); Future<bool> showProfileForm(BuildContext context, TextEditingController username, TextEditingController password) async{
Future<bool> showProfileForm(BuildContext context) async{
final showPassword = RxBool(false); final showPassword = RxBool(false);
final oldUsername = _gameController.username.text; final oldUsername = username.text;
final showPasswordTrailing = RxBool(oldUsername.isNotEmpty); final showPasswordTrailing = RxBool(oldUsername.isNotEmpty);
final oldPassword = _gameController.password.text; final oldPassword = password.text;
final result = await showRebootDialog<bool?>( final result = await showRebootDialog<bool?>(
builder: (context) => Obx(() => FormDialog( builder: (context) => Obx(() => FormDialog(
content: Column( content: Column(
@@ -23,7 +22,18 @@ Future<bool> showProfileForm(BuildContext context) async{
label: translations.usernameOrEmail, label: translations.usernameOrEmail,
child: TextFormBox( child: TextFormBox(
placeholder: translations.usernameOrEmailPlaceholder, placeholder: translations.usernameOrEmailPlaceholder,
controller: _gameController.username, validator: (text) {
if(password.text.isEmpty) {
return null;
}
if(EmailValidator.validate(username.text)) {
return null;
}
return translations.invalidEmail;
},
controller: username,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
enableSuggestions: true, enableSuggestions: true,
autofocus: true, autofocus: true,
@@ -35,7 +45,7 @@ Future<bool> showProfileForm(BuildContext context) async{
label: translations.password, label: translations.password,
child: TextFormBox( child: TextFormBox(
placeholder: translations.passwordPlaceholder, placeholder: translations.passwordPlaceholder,
controller: _gameController.password, controller: password,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
obscureText: !showPassword.value, obscureText: !showPassword.value,
enableSuggestions: false, enableSuggestions: false,
@@ -44,8 +54,8 @@ Future<bool> showProfileForm(BuildContext context) async{
suffix: Button( suffix: Button(
onPressed: () => showPassword.value = !showPassword.value, onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle( style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()), shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent) backgroundColor: WidgetStateProperty.all(Colors.transparent)
), ),
child: Icon( child: Icon(
showPassword.value ? Icons.visibility_off : Icons.visibility, showPassword.value ? Icons.visibility_off : Icons.visibility,
@@ -75,7 +85,7 @@ Future<bool> showProfileForm(BuildContext context) async{
return true; return true;
} }
_gameController.username.text = oldUsername; username.text = oldUsername;
_gameController.password.text = oldPassword; password.text = oldPassword;
return false; return false;
} }

View File

@@ -15,10 +15,40 @@ import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/cryptography.dart'; import 'package:reboot_launcher/src/util/cryptography.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart';
final List<InfoBarEntry> _infoBars = [];
extension ServerControllerDialog on BackendController { extension ServerControllerDialog on BackendController {
void cancelInteractive() {
worker?.cancel(); // Do not await or it will hang
_infoBars.forEach((infoBar) => infoBar.close());
_infoBars.clear();
}
Future<bool> toggleInteractive() async { Future<bool> toggleInteractive() async {
final stream = toggle(); cancelInteractive();
final stream = toggle(
onExit: () {
cancelInteractive();
_showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
},
onError: (errorMessage) {
cancelInteractive();
_showRebootInfoBar(
translations.backendErrorMessage,
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
);
}
);
final completer = Completer<bool>(); final completer = Completer<bool>();
InfoBarEntry? entry; InfoBarEntry? entry;
worker = stream.listen((event) { worker = stream.listen((event) {
@@ -38,105 +68,100 @@ extension ServerControllerDialog on BackendController {
log("[BACKEND] Handling event: $event"); log("[BACKEND] Handling event: $event");
switch (event.type) { switch (event.type) {
case ServerResultType.starting: case ServerResultType.starting:
return showRebootInfoBar( return _showRebootInfoBar(
translations.startingServer, translations.startingServer,
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.startSuccess: case ServerResultType.startSuccess:
return showRebootInfoBar( return _showRebootInfoBar(
type.value == ServerType.local ? translations.checkedServer : translations.startedServer, type.value == ServerType.local ? translations.checkedServer : translations.startedServer,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
case ServerResultType.startError: case ServerResultType.startError:
print(event.stackTrace); print(event.stackTrace);
return showRebootInfoBar( return _showRebootInfoBar(
type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError), type.value == ServerType.local ? translations.localServerError(event.error ?? translations.unknownError) : translations.startServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
case ServerResultType.stopping: case ServerResultType.stopping:
return showRebootInfoBar( return _showRebootInfoBar(
translations.stoppingServer, translations.stoppingServer,
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.stopSuccess: case ServerResultType.stopSuccess:
return showRebootInfoBar( return _showRebootInfoBar(
translations.stoppedServer, translations.stoppedServer,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
); );
case ServerResultType.stopError: case ServerResultType.stopError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.stopServerError(event.error ?? translations.unknownError), translations.stopServerError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
case ServerResultType.missingHostError: case ServerResultType.missingHostError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.missingHostNameError, translations.missingHostNameError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.missingPortError: case ServerResultType.missingPortError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.missingPortError, translations.missingPortError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.illegalPortError: case ServerResultType.illegalPortError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.illegalPortError, translations.illegalPortError,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.freeingPort: case ServerResultType.freeingPort:
return showRebootInfoBar( return _showRebootInfoBar(
translations.freeingPort, translations.freeingPort,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.freePortSuccess: case ServerResultType.freePortSuccess:
return showRebootInfoBar( return _showRebootInfoBar(
translations.freedPort, translations.freedPort,
severity: InfoBarSeverity.success, severity: InfoBarSeverity.success,
duration: infoBarShortDuration duration: infoBarShortDuration
); );
case ServerResultType.freePortError: case ServerResultType.freePortError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.freePortError(event.error ?? translations.unknownError), translations.freePortError(event.error ?? translations.unknownError),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration duration: infoBarLongDuration
); );
case ServerResultType.pingingRemote: case ServerResultType.pingingRemote:
return showRebootInfoBar( return _showRebootInfoBar(
translations.pingingServer(ServerType.remote.name), translations.pingingServer(ServerType.remote.name),
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.pingingLocal: case ServerResultType.pingingLocal:
return showRebootInfoBar( return _showRebootInfoBar(
translations.pingingServer(type.value.name), translations.pingingServer(type.value.name),
severity: InfoBarSeverity.info, severity: InfoBarSeverity.info,
loading: true, loading: true,
duration: null duration: null
); );
case ServerResultType.pingError: case ServerResultType.pingError:
return showRebootInfoBar( return _showRebootInfoBar(
translations.pingError(type.value.name), translations.pingError(type.value.name),
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
); );
case ServerResultType.processError:
return showRebootInfoBar(
translations.backendProcessError,
severity: InfoBarSeverity.error
);
} }
} }
Future<void> joinServerInteractive(String uuid, FortniteServer server) async { Future<void> joinServerInteractive(String uuid, FortniteServer server) async {
if(!kDebugMode && uuid == server.id) { if(!kDebugMode && uuid == server.id) {
showRebootInfoBar( _showRebootInfoBar(
translations.joinSelfServer, translations.joinSelfServer,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -147,7 +172,7 @@ extension ServerControllerDialog on BackendController {
final gameController = Get.find<GameController>(); final gameController = Get.find<GameController>();
final version = gameController.getVersionByName(server.version.toString()); final version = gameController.getVersionByName(server.version.toString());
if(version == null) { if(version == null) {
showRebootInfoBar( _showRebootInfoBar(
translations.cannotJoinServerVersion(server.version.toString()), translations.cannotJoinServerVersion(server.version.toString()),
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -176,7 +201,7 @@ extension ServerControllerDialog on BackendController {
} }
if(!checkPassword(confirmPassword, hashedPassword)) { if(!checkPassword(confirmPassword, hashedPassword)) {
showRebootInfoBar( _showRebootInfoBar(
translations.wrongServerPassword, translations.wrongServerPassword,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -199,7 +224,7 @@ extension ServerControllerDialog on BackendController {
return true; return true;
} }
showRebootInfoBar( _showRebootInfoBar(
translations.offlineServer, translations.offlineServer,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.error severity: InfoBarSeverity.error
@@ -232,8 +257,8 @@ extension ServerControllerDialog on BackendController {
suffix: !showPasswordTrailing.value ? null : Button( suffix: !showPasswordTrailing.value ? null : Button(
onPressed: () => showPassword.value = !showPassword.value, onPressed: () => showPassword.value = !showPassword.value,
style: ButtonStyle( style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()), shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent) backgroundColor: WidgetStateProperty.all(Colors.transparent)
), ),
child: Icon( child: Icon(
showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular showPassword.value ? FluentIcons.eye_off_24_regular : FluentIcons.eye_24_regular
@@ -268,10 +293,31 @@ extension ServerControllerDialog on BackendController {
FlutterClipboard.controlC(decryptedIp); FlutterClipboard.controlC(decryptedIp);
} }
controller.selectedVersion = version; controller.selectedVersion = version;
WidgetsBinding.instance.addPostFrameCallback((_) => showRebootInfoBar( WidgetsBinding.instance.addPostFrameCallback((_) => _showRebootInfoBar(
embedded ? translations.joinedServer(author) : translations.copiedIp, embedded ? translations.joinedServer(author) : translations.copiedIp,
duration: infoBarLongDuration, duration: infoBarLongDuration,
severity: InfoBarSeverity.success severity: InfoBarSeverity.success
)); ));
} }
InfoBarEntry _showRebootInfoBar(dynamic text, {
InfoBarSeverity severity = InfoBarSeverity.info,
bool loading = false,
Duration? duration = infoBarShortDuration,
void Function()? onDismissed,
Widget? action
}) {
final result = showRebootInfoBar(
text,
severity: severity,
loading: loading,
duration: duration,
onDismissed: onDismissed,
action: action
);
if(severity == InfoBarSeverity.info || severity == InfoBarSeverity.success) {
_infoBars.add(result);
}
return result;
}
} }

View File

@@ -8,9 +8,10 @@ import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/util/types.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:universal_disk_space/universal_disk_space.dart';
import 'package:windows_taskbar/windows_taskbar.dart'; import 'package:windows_taskbar/windows_taskbar.dart';
class AddVersionDialog extends StatefulWidget { class AddVersionDialog extends StatefulWidget {
@@ -32,22 +33,20 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final Rxn<FortniteBuild> _build = Rxn(); final Rxn<FortniteBuild> _build = Rxn();
final RxnInt _timeLeft = RxnInt(); final RxnInt _timeLeft = RxnInt();
final Rxn<double> _progress = Rxn(); final Rxn<double> _progress = Rxn();
final RxInt _speed = RxInt(0);
late DiskSpace _diskSpace;
late Future<List<FortniteBuild>> _fetchFuture; late Future<List<FortniteBuild>> _fetchFuture;
late Future _diskFuture;
Isolate? _isolate;
SendPort? _downloadPort; SendPort? _downloadPort;
Object? _error; Object? _error;
StackTrace? _stackTrace; StackTrace? _stackTrace;
@override @override
void initState() { void initState() {
_fetchFuture = compute(fetchBuilds, null); _fetchFuture = compute(fetchBuilds, null).then((value) {
_diskSpace = DiskSpace(); _updateFormDefaults();
_diskFuture = _diskSpace.scan() return value;
.then((_) => _updateFormDefaults()); });
super.initState(); super.initState();
} }
@@ -59,9 +58,8 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
} }
void _cancelDownload() { void _cancelDownload() {
Process.run('${assetsDirectory.path}\\build\\stop.bat', []);
_downloadPort?.send(kStopBuildDownloadSignal); _downloadPort?.send(kStopBuildDownloadSignal);
_isolate?.kill(priority: Isolate.immediate); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress);
} }
@override @override
@@ -71,7 +69,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
switch(_status.value){ switch(_status.value){
case _DownloadStatus.form: case _DownloadStatus.form:
return FutureBuilder( return FutureBuilder(
future: Future.wait([_fetchFuture, _diskFuture]).then((_) async => await _fetchFuture), future: _fetchFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace)); WidgetsBinding.instance.addPostFrameCallback((_) => _onDownloadError(snapshot.error, snapshot.stackTrace));
@@ -102,7 +100,12 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
return ErrorDialog( return ErrorDialog(
exception: _error ?? Exception(translations.unknownError), exception: _error ?? Exception(translations.unknownError),
stackTrace: _stackTrace, stackTrace: _stackTrace,
errorMessageBuilder: (exception) => translations.downloadVersionError(exception.toString()) errorMessageBuilder: (exception) {
var error = exception.toString();
error = error.after("Error: ")?.replaceAll(":", ",") ?? error.after(": ") ?? error;
error = error.toLowerCase();
return translations.downloadVersionError(error);
}
); );
case _DownloadStatus.done: case _DownloadStatus.done:
return InfoDialog( return InfoDialog(
@@ -151,7 +154,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
final communicationPort = ReceivePort(); final communicationPort = ReceivePort();
communicationPort.listen((message) { communicationPort.listen((message) {
if(message is FortniteBuildDownloadProgress) { if(message is FortniteBuildDownloadProgress) {
_onProgress(build, message.progress, message.minutesLeft, message.extracting); _onProgress(build, message);
}else if(message is SendPort) { }else if(message is SendPort) {
_downloadPort = message; _downloadPort = message;
}else { }else {
@@ -165,7 +168,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
); );
final errorPort = ReceivePort(); final errorPort = ReceivePort();
errorPort.listen((message) => _onDownloadError(message, null)); errorPort.listen((message) => _onDownloadError(message, null));
_isolate = await Isolate.spawn( await Isolate.spawn(
downloadArchiveBuild, downloadArchiveBuild,
options, options,
onError: errorPort.sendPort, onError: errorPort.sendPort,
@@ -205,23 +208,24 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
_stackTrace = stackTrace; _stackTrace = stackTrace;
} }
void _onProgress(FortniteBuild build, double progress, int? timeLeft, bool extracting) { void _onProgress(FortniteBuild build, FortniteBuildDownloadProgress message) {
if (!mounted) { if (!mounted) {
return; return;
} }
if(progress >= 100 && extracting) { if(message.progress >= 100 && message.extracting) {
_onDownloadComplete(build); _onDownloadComplete(build);
return; return;
} }
_status.value = extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading; _status.value = message.extracting ? _DownloadStatus.extracting : _DownloadStatus.downloading;
if(progress >= 0) { if(message.progress >= 0) {
WindowsTaskbar.setProgress(progress.round(), 100); WindowsTaskbar.setProgress(message.progress.round(), 100);
} }
_timeLeft.value = timeLeft; _timeLeft.value = message.timeLeft;
_progress.value = progress; _progress.value = message.progress;
_speed.value = message.speed;
} }
Widget get _progressBody { Widget get _progressBody {
@@ -232,16 +236,18 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
_status.value == _DownloadStatus.downloading ? translations.downloading : translations.extracting, _statusText,
style: FluentTheme.maybeOf(context)?.typography.body, style: FluentTheme.maybeOf(context)?.typography.body,
textAlign: TextAlign.start, textAlign: TextAlign.start,
), ),
), ),
if(_progress.value != null)
const SizedBox( const SizedBox(
height: 8.0, height: 8.0,
), ),
if(_progress.value != null)
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
@@ -264,7 +270,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ProgressBar(value: (_progress.value ?? 0).toDouble()) child: ProgressBar(value: _progress.value?.toDouble())
), ),
const SizedBox( const SizedBox(
@@ -274,7 +280,20 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
); );
} }
Widget _buildFormBody(List<FortniteBuild> builds) => Column( String get _statusText {
if (_status.value != _DownloadStatus.downloading) {
return translations.extracting;
}
if (_progress.value == null) {
return translations.startingDownload;
}
return translations.downloading;
}
Widget _buildFormBody(List<FortniteBuild> builds) {
return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -292,7 +311,8 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle, windowTitle: _source.value == _BuildSource.local ? translations.gameFolderPlaceWindowTitle : translations.buildInstallationDirectoryWindowTitle,
controller: _pathController, controller: _pathController,
validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination, validator: _source.value == _BuildSource.local ? _checkGameFolder : _checkDownloadDestination,
folder: true folder: true,
allowNavigator: true
), ),
const SizedBox( const SizedBox(
@@ -300,6 +320,7 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
) )
], ],
); );
}
String? _checkGameFolder(text) { String? _checkGameFolder(text) {
if (text == null || text.isEmpty) { if (text == null || text.isEmpty) {
@@ -421,16 +442,16 @@ class _AddVersionDialogState extends State<AddVersionDialog> {
_build.value = null; _build.value = null;
} }
if(_source.value != _BuildSource.local && _diskSpace.disks.isNotEmpty) { final disks = WindowsDisk.available();
await _fetchFuture; if(_source.value != _BuildSource.local && disks.isNotEmpty) {
final bestDisk = _diskSpace.disks final bestDisk = disks.reduce((first, second) => first.freeBytesAvailable > second.freeBytesAvailable ? first : second);
.reduce((first, second) => first.availableSpace > second.availableSpace ? first : second);
final build = _build.value; final build = _build.value;
if(build == null){ if(build == null){
return; return;
} }
final pathText = "${bestDisk.devicePath}\\FortniteBuilds\\${build.version}"; print("${bestDisk.path}\\FortniteBuilds\\${build.version}");
final pathText = "${bestDisk.path}FortniteBuilds\\${build.version}";
_pathController.text = pathText; _pathController.text = pathText;
_pathController.selection = TextSelection.collapsed(offset: pathText.length); _pathController.selection = TextSelection.collapsed(offset: pathText.length);
} }

View File

@@ -1,5 +1,9 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart';
abstract class RebootPage extends StatefulWidget { abstract class RebootPage extends StatefulWidget {
const RebootPage({super.key}); const RebootPage({super.key});
@@ -19,15 +23,30 @@ abstract class RebootPage extends StatefulWidget {
} }
abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> { abstract class RebootPageState<T extends RebootPage> extends State<T> with AutomaticKeepAliveClientMixin<T> {
final SettingsController _settingsController = Get.find<SettingsController>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
var buttonWidget = button; var buttonWidget = button;
if(buttonWidget == null) { if(buttonWidget == null) {
return _listView; return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: _listView
)
],
);
} }
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: Column(
children: [ children: [
Expanded( Expanded(
child: _listView, child: _listView,
@@ -42,9 +61,41 @@ abstract class RebootPageState<T extends RebootPage> extends State<T> with Autom
child: buttonWidget child: buttonWidget
) )
], ],
),
),
],
); );
} }
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
ListView get _listView => ListView.builder( ListView get _listView => ListView.builder(
itemCount: settings.length, itemCount: settings.length,
itemBuilder: (context, index) => settings[index], itemBuilder: (context, index) => settings[index],

View File

@@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart'; import 'package:reboot_launcher/src/messenger/implementation/data.dart';
@@ -43,7 +42,6 @@ class BackendPage extends RebootPage {
} }
class _BackendPageState extends RebootPageState<BackendPage> { class _BackendPageState extends RebootPageState<BackendPage> {
final GameController _gameController = Get.find<GameController>();
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
InfoBarEntry? _infoBarEntry; InfoBarEntry? _infoBarEntry;
@@ -56,7 +54,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
} }
if(keyEvent.physicalKey.isUnrealEngineKey) { if(keyEvent.physicalKey.isUnrealEngineKey) {
_gameController.consoleKey.value = keyEvent.physicalKey; _backendController.consoleKey.value = keyEvent.physicalKey;
} }
_infoBarEntry?.close(); _infoBarEntry?.close();
@@ -154,9 +152,9 @@ class _BackendPageState extends RebootPageState<BackendPage> {
contentWidth: null, contentWidth: null,
content: Row( content: Row(
children: [ children: [
Text( Obx(() => Text(
_backendController.detached.value ? translations.on : translations.off _backendController.detached.value ? translations.on : translations.off
), )),
const SizedBox( const SizedBox(
width: 16.0 width: 16.0
), ),
@@ -194,7 +192,7 @@ class _BackendPageState extends RebootPageState<BackendPage> {
duration: null duration: null
); );
}, },
child: Text(_gameController.consoleKey.value.unrealEnginePrettyName ?? ""), child: Text(_backendController.consoleKey.value.unrealEnginePrettyName ?? ""),
), ),
) )
); );

View File

@@ -276,8 +276,8 @@ class _BrowsePageState extends RebootPageState<BrowsePage> {
_filterControllerStream.add(""); _filterControllerStream.add("");
}, },
style: ButtonStyle( style: ButtonStyle(
backgroundColor: ButtonState.all(Colors.transparent), backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: ButtonState.all(Border()) shape: WidgetStateProperty.all(Border())
), ),
child: _searchBarIconData child: _searchBarIconData
); );

View File

@@ -3,13 +3,14 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show MaterialPage; import 'package:flutter/material.dart' show MaterialPage;
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
@@ -26,6 +27,7 @@ import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/info_bar_area.dart'; import 'package:reboot_launcher/src/widget/info_bar_area.dart';
import 'package:reboot_launcher/src/widget/profile_tile.dart'; import 'package:reboot_launcher/src/widget/profile_tile.dart';
import 'package:reboot_launcher/src/widget/title_bar.dart'; import 'package:reboot_launcher/src/widget/title_bar.dart';
import 'package:version/version.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey(); final GlobalKey<OverlayTargetState> profileOverlayKey = GlobalKey();
@@ -42,8 +44,10 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin { class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepAliveClientMixin {
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
final GlobalKey _searchKey = GlobalKey(); final GlobalKey _searchKey = GlobalKey();
final FocusNode _searchFocusNode = FocusNode(); final FocusNode _searchFocusNode = FocusNode();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
@@ -56,7 +60,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override @override
void initState() { void initState() {
super.initState(); super.initState();
windowManager.setPreventClose(true);
windowManager.addListener(this); windowManager.addListener(this);
_syncPageViewWithNavigator(); _syncPageViewWithNavigator();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -109,7 +112,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
return; return;
} }
var result = await pingGameServer(address); final result = await pingGameServer(address);
if(result) { if(result) {
return; return;
} }
@@ -133,28 +136,71 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
dllsDirectory.createSync(recursive: true); dllsDirectory.createSync(recursive: true);
} }
final dummy = Version.parse("1");
final dummyS20 = Version.parse("20");
for(final injectable in InjectableDll.values) { for(final injectable in InjectableDll.values) {
final (file, custom) = _settingsController.getInjectableData(injectable); _downloadDll(dummy, injectable);
if(!custom) { if(injectable.isVersionDependent) {
_settingsController.downloadCriticalDllInteractive( _downloadDll(dummyS20, injectable);
file.path,
silent: true
);
} }
} }
watchDlls().listen((filePath) => showDllDeletedDialog(() { watchDlls().listen((filePath) => showDllDeletedDialog(() {
_settingsController.downloadCriticalDllInteractive(filePath); _dllController.downloadCriticalDllInteractive(filePath);
})); }));
} }
void _downloadDll(Version version, InjectableDll injectable) {
final (file, custom) = _dllController.getInjectableData(version, injectable);
if(!custom) {
_dllController.downloadCriticalDllInteractive(
file.path,
silent: false
);
}
}
@override @override
void onWindowClose() async { void onWindowClose() async {
try { try {
await _hostingController.discardServer(); await windowManager.hide();
}finally { }catch(error) {
exit(0); // Force closing log("[WINDOW] Cannot hide window: $error");
} }
try {
await _hostingController.discardServer();
}catch(error) {
log("[HOSTING] Cannot discard server on exit: $error");
}
try {
if(_backendController.started.value) {
await _backendController.toggleInteractive();
}
}catch(error) {
log("[BACKEND] Cannot stop backend on exit: $error");
}
try {
_gameController.instance.value?.kill();
}catch(error) {
log("[GAME] Cannot stop game on exit: $error");
}
try {
_hostingController.instance.value?.kill();
}catch(error) {
log("[HOST] Cannot stop host on exit: $error");
}
try {
await stopDownloadServer();
}catch(error) {
log("[ARIA] Cannot stop aria server on exit: $error");
}
exit(0);
} }
@override @override
@@ -218,14 +264,18 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
@override @override
void onWindowResized() { void onWindowResized() {
_settingsController.saveWindowSize(appWindow.size);
_focused.value = true; _focused.value = true;
windowManager.getSize().then((size) {
_settingsController.saveWindowSize(size);
});
} }
@override @override
void onWindowMoved() { void onWindowMoved() {
_settingsController.saveWindowOffset(appWindow.position);
_focused.value = true; _focused.value = true;
windowManager.getPosition().then((position) {
_settingsController.saveWindowOffset(position);
});
} }
@override @override
@@ -296,8 +346,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
}); });
} }
Widget _buildBody() { Widget _buildBody() => Expanded(
return Expanded(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: HomePage.kDefaultPadding, left: HomePage.kDefaultPadding,
@@ -337,7 +386,6 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
) )
), ),
); );
}
Widget _buildBodyContent() => PageView.builder( Widget _buildBodyContent() => PageView.builder(
controller: _pageController, controller: _pageController,
@@ -449,9 +497,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ProfileWidget( Obx(() {
pageIndex.value;
return ProfileWidget(
overlayKey: profileOverlayKey overlayKey: profileOverlayKey
), );
}),
_autoSuggestBox, _autoSuggestBox,
const SizedBox(height: 12.0), const SizedBox(height: 12.0),
_buildNavigationTrail() _buildNavigationTrail()
@@ -498,7 +549,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
decoration: BoxDecoration( decoration: BoxDecoration(
color: ButtonThemeData.uncheckedInputColor( color: ButtonThemeData.uncheckedInputColor(
FluentTheme.of(context), FluentTheme.of(context),
pageIndex.value == index ? {ButtonStates.hovering} : states, pageIndex.value == index ? {WidgetState.hovered} : states,
transparentWhenNone: true, transparentWhenNone: true,
), ),
borderRadius: BorderRadius.all(Radius.circular(6.0)) borderRadius: BorderRadius.all(Radius.circular(6.0))
@@ -527,12 +578,12 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
stream: pagesController.stream, stream: pagesController.stream,
builder: (context, _) => Button( builder: (context, _) => Button(
style: ButtonStyle( style: ButtonStyle(
padding: ButtonState.all(const EdgeInsets.symmetric( padding: WidgetStateProperty.all(const EdgeInsets.symmetric(
vertical: 12.0, vertical: 12.0,
horizontal: 16.0 horizontal: 16.0
)), )),
backgroundColor: ButtonState.all(Colors.transparent), backgroundColor: WidgetStateProperty.all(Colors.transparent),
shape: ButtonState.all(Border()) shape: WidgetStateProperty.all(Border())
), ),
onPressed: appStack.isEmpty && !inDialog ? null : () { onPressed: appStack.isEmpty && !inDialog ? null : () {
if(inDialog) { if(inDialog) {
@@ -554,7 +605,7 @@ class _HomePageState extends State<HomePage> with WindowListener, AutomaticKeepA
); );
GestureDetector get _draggableArea => GestureDetector( GestureDetector get _draggableArea => GestureDetector(
onDoubleTap: appWindow.maximizeOrRestore, onDoubleTap: windowManager.maximizeOrRestore,
onHorizontalDragStart: (_) => windowManager.startDragging(), onHorizontalDragStart: (_) => windowManager.startDragging(),
onVerticalDragStart: (_) => windowManager.startDragging() onVerticalDragStart: (_) => windowManager.startDragging()
); );

View File

@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/main.dart'; import 'package:reboot_launcher/main.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
@@ -52,7 +53,7 @@ class HostPage extends RebootPage {
class _HostingPageState extends RebootPageState<HostPage> { class _HostingPageState extends RebootPageState<HostPage> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final DllController _dllController = Get.find<DllController>();
late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty); late final RxBool _showPasswordTrailing = RxBool(_hostingController.password.text.isNotEmpty);
@@ -83,7 +84,6 @@ class _HostingPageState extends RebootPageState<HostPage> {
key: hostVersionOverlayTargetKey key: hostVersionOverlayTargetKey
), ),
_options, _options,
_internalFiles,
_share, _share,
_resetDefaults _resetDefaults
]; ];
@@ -153,8 +153,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
suffix: Button( suffix: Button(
onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value, onPressed: () => _hostingController.showPassword.value = !_hostingController.showPassword.value,
style: ButtonStyle( style: ButtonStyle(
shape: ButtonState.all(const CircleBorder()), shape: WidgetStateProperty.all(const CircleBorder()),
backgroundColor: ButtonState.all(Colors.transparent) backgroundColor: WidgetStateProperty.all(Colors.transparent)
), ),
child: Icon( child: Icon(
_hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled, _hostingController.showPassword.value ? FluentIcons.eye_off_24_filled : FluentIcons.eye_24_filled,
@@ -173,9 +173,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null, contentWidth: null,
content: Obx(() => Row( content: Obx(() => Row(
children: [ children: [
Text( Obx(() => Text(
_hostingController.discoverable.value ? translations.on : translations.off _hostingController.discoverable.value ? translations.on : translations.off
), )),
const SizedBox( const SizedBox(
width: 16.0 width: 16.0
), ),
@@ -199,6 +199,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.settingsServerOptionsName), title: Text(translations.settingsServerOptionsName),
subtitle: Text(translations.settingsServerOptionsSubtitle), subtitle: Text(translations.settingsServerOptionsSubtitle),
children: [ children: [
SettingTile(
icon: Icon(
FluentIcons.options_24_regular
),
title: Text(translations.settingsClientArgsName),
subtitle: Text(translations.settingsClientArgsDescription),
content: TextFormBox(
placeholder: translations.settingsClientArgsPlaceholder,
controller: _hostingController.customLaunchArgs,
)
),
SettingTile( SettingTile(
icon: Icon( icon: Icon(
FluentIcons.window_console_20_regular FluentIcons.window_console_20_regular
@@ -224,9 +235,9 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: null, contentWidth: null,
content: Row( content: Row(
children: [ children: [
Text( Obx(() => Text(
_hostingController.autoRestart.value ? translations.on : translations.off _hostingController.autoRestart.value ? translations.on : translations.off
), )),
const SizedBox( const SizedBox(
width: 16.0 width: 16.0
), ),
@@ -246,125 +257,17 @@ class _HostingPageState extends RebootPageState<HostPage> {
contentWidth: 64, contentWidth: 64,
content: TextFormBox( content: TextFormBox(
placeholder: translations.settingsServerPortName, placeholder: translations.settingsServerPortName,
controller: _settingsController.gameServerPort, controller: _dllController.gameServerPort,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textAlign: TextAlign.center, textAlign: TextAlign.center,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly
] ]
) )
),
],
);
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsServerName),
subtitle: Text(translations.settingsServerSubtitle),
children: [
SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _settingsController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_settingsController.customGameServer.value = entry.key;
_settingsController.infoBarEntry?.close();
if(!entry.key) {
_settingsController.updateReboot(
force: true
);
}
}
)).toList()
))
),
Obx(() {
if(!_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return createFileSetting(
title: translations.settingsServerFileName,
description: translations.settingsServerFileDescription,
controller: _settingsController.gameServerDll
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
content: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _settingsController.url,
validator: _checkUpdateUrl
) )
);
}),
Obx(() {
if(_settingsController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_settingsController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_settingsController.timer.value = entry;
_settingsController.infoBarEntry?.close();
_settingsController.updateReboot(
force: true
);
}
)).toList()
))
);
}),
], ],
); );
String? _checkUpdateUrl(String? text) {
if (text == null || text.isEmpty) {
return translations.emptyURL;
}
return null;
}
SettingTile get _share => SettingTile( SettingTile get _share => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.link_24_regular FluentIcons.link_24_regular
@@ -420,7 +323,10 @@ class _HostingPageState extends RebootPageState<HostPage> {
title: Text(translations.hostResetName), title: Text(translations.hostResetName),
subtitle: Text(translations.hostResetDescription), subtitle: Text(translations.hostResetDescription),
content: Button( content: Button(
onPressed: () => showResetDialog(_hostingController.reset), onPressed: () => showResetDialog(() {
_hostingController.reset();
_dllController.resetServer();
}),
child: Text(translations.hostResetContent), child: Text(translations.hostResetContent),
) )
); );
@@ -432,8 +338,8 @@ class _HostingPageState extends RebootPageState<HostPage> {
try { try {
_hostingController.publishServer( _hostingController.publishServer(
_gameController.username.text, _hostingController.accountUsername.text,
_hostingController.instance.value!.versionName _hostingController.instance.value!.version.toString()
); );
} catch(error) { } catch(error) {
_showCannotUpdateGameServer(error); _showCannotUpdateGameServer(error);
@@ -468,13 +374,3 @@ class _HostingPageState extends RebootPageState<HostPage> {
duration: infoBarLongDuration duration: infoBarLongDuration
); );
} }
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

@@ -2,13 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/onboard.dart'; import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/game_start_button.dart'; import 'package:reboot_launcher/src/widget/game_start_button.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:reboot_launcher/src/widget/version_selector_tile.dart'; import 'package:reboot_launcher/src/widget/version_selector_tile.dart';
@@ -35,51 +33,8 @@ class PlayPage extends RebootPage {
} }
class _PlayPageState extends RebootPageState<PlayPage> { class _PlayPageState extends RebootPageState<PlayPage> {
final SettingsController _settingsController = Get.find<SettingsController>();
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildFirstLaunchInfo(),
Expanded(
child: super.build(context),
)
],
);
}
Widget _buildFirstLaunchInfo() => Obx(() {
if(!_settingsController.firstRun.value) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(
bottom: 8.0
),
child: SizedBox(
width: double.infinity,
child: InfoBar(
title: Text(translations.welcomeTitle),
severity: InfoBarSeverity.warning,
isLong: true,
content: SizedBox(
width: double.infinity,
child: Text(translations.welcomeDescription)
),
action: Button(
child: Text(translations.welcomeAction),
onPressed: () => startOnboarding(),
),
onClose: () => _settingsController.firstRun.value = false
),
),
);
});
@override @override
Widget? get button => LaunchButton( Widget? get button => LaunchButton(
startLabel: translations.launchFortnite, startLabel: translations.launchFortnite,
@@ -93,34 +48,9 @@ class _PlayPageState extends RebootPageState<PlayPage> {
key: gameVersionOverlayTargetKey key: gameVersionOverlayTargetKey
), ),
_options, _options,
_internalFiles, _resetDefaults
]; ];
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _settingsController.unrealEngineConsoleDll
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _settingsController.backendDll
),
createFileSetting(
title: translations.settingsClientMemoryName,
description: translations.settingsClientMemoryDescription,
controller: _settingsController.memoryLeakDll
),
],
);
SettingTile get _options => SettingTile( SettingTile get _options => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.options_24_regular FluentIcons.options_24_regular
@@ -141,4 +71,18 @@ class _PlayPageState extends RebootPageState<PlayPage> {
) )
] ]
); );
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.gameResetDefaultsName),
subtitle: Text(translations.gameResetDefaultsDescription),
content: Button(
onPressed: () => showResetDialog(() {
_gameController.reset();
}),
child: Text(translations.gameResetDefaultsContent),
)
);
} }

View File

@@ -4,12 +4,13 @@ import 'package:flutter_gen/gen_l10n/reboot_localizations.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart'; import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart'; import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/implementation/data.dart';
import 'package:reboot_launcher/src/page/abstract/page.dart'; import 'package:reboot_launcher/src/page/abstract/page.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart'; import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_setting_tile.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -34,6 +35,7 @@ class SettingsPage extends RebootPage {
class _SettingsPageState extends RebootPageState<SettingsPage> { class _SettingsPageState extends RebootPageState<SettingsPage> {
final SettingsController _settingsController = Get.find<SettingsController>(); final SettingsController _settingsController = Get.find<SettingsController>();
final DllController _dllController = Get.find<DllController>();
@override @override
Widget? get button => null; Widget? get button => null;
@@ -42,10 +44,235 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
List<Widget> get settings => [ List<Widget> get settings => [
_language, _language,
_theme, _theme,
_resetDefaults, _internalFiles,
_installationDirectory _installationDirectory,
]; ];
SettingTile get _internalFiles => SettingTile(
icon: Icon(
FluentIcons.archive_settings_24_regular
),
title: Text(translations.settingsClientName),
subtitle: Text(translations.settingsClientDescription),
children: [
createFileSetting(
title: translations.settingsClientConsoleName,
description: translations.settingsClientConsoleDescription,
controller: _dllController.unrealEngineConsoleDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.console);
_dllController.unrealEngineConsoleDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
createFileSetting(
title: translations.settingsClientAuthName,
description: translations.settingsClientAuthDescription,
controller: _dllController.backendDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.starfall);
_dllController.backendDll.text = path;
_dllController.downloadCriticalDllInteractive(path, force: true);
}
),
_internalFilesServerType,
_internalFilesUpdateTimer,
_internalFilesServerSource,
_internalFilesNewServerSource,
],
);
Widget get _internalFilesServerType => SettingTile(
icon: Icon(
FluentIcons.games_24_regular
),
title: Text(translations.settingsServerTypeName),
subtitle: Text(translations.settingsServerTypeDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.customGameServer.value ? translations.settingsServerTypeCustomName : translations.settingsServerTypeEmbeddedName),
items: {
false: translations.settingsServerTypeEmbeddedName,
true: translations.settingsServerTypeCustomName
}.entries.map((entry) => MenuFlyoutItem(
text: Text(entry.value),
onPressed: () {
final oldValue = _dllController.customGameServer.value;
if(oldValue == entry.key) {
return;
}
_dllController.customGameServer.value = entry.key;
_dllController.infoBarEntry?.close();
if(!entry.key) {
_dllController.updateGameServerDll(
force: true
);
}
}
)).toList()
))
);
Widget get _internalFilesServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerOldMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.beforeS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.beforeS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return createFileSetting(
title: translations.settingsOldServerFileName,
description: translations.settingsServerFileDescription,
controller: _dllController.gameServerDll,
onReset: () {
final path = _dllController.getDefaultDllPath(InjectableDll.reboot);
_dllController.gameServerDll.text = path;
_dllController.downloadCriticalDllInteractive(path);
}
);
}
});
Widget get _internalFilesNewServerSource => Obx(() {
if(!_dllController.customGameServer.value) {
return SettingTile(
icon: Icon(
FluentIcons.globe_24_regular
),
title: Text(translations.settingsServerNewMirrorName),
subtitle: Text(translations.settingsServerMirrorDescription),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Row(
children: [
Expanded(
child: TextFormBox(
placeholder: translations.settingsServerMirrorPlaceholder,
controller: _dllController.aboveS20Mirror,
onChanged: (value) {
if(Uri.tryParse(value) != null) {
_dllController.updateGameServerDll(force: true);
}
},
),
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _dllController.updateGameServerDll(force: true),
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_download_24_regular
),
)
),
const SizedBox(width: 8.0),
Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () {
_dllController.aboveS20Mirror.text = kRebootBelowS20DownloadUrl;
_dllController.updateGameServerDll(force: true);
},
child: SizedBox.square(
dimension: 30,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
)
],
)
);
}else {
return const SizedBox();
}
});
Widget get _internalFilesUpdateTimer => Obx(() {
if(_dllController.customGameServer.value) {
return const SizedBox.shrink();
}
return SettingTile(
icon: Icon(
FluentIcons.timer_24_regular
),
title: Text(translations.settingsServerTimerName),
subtitle: Text(translations.settingsServerTimerSubtitle),
contentWidth: SettingTile.kDefaultContentWidth + 30,
content: Obx(() => DropDownButton(
onOpen: () => inDialog = true,
onClose: () => inDialog = false,
leading: Text(_dllController.timer.value.text),
items: UpdateTimer.values.map((entry) => MenuFlyoutItem(
text: Text(entry.text),
onPressed: () {
_dllController.timer.value = entry;
_dllController.infoBarEntry?.close();
_dllController.updateGameServerDll(
force: true
);
}
)).toList()
))
);
});
SettingTile get _language => SettingTile( SettingTile get _language => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.local_language_24_regular FluentIcons.local_language_24_regular
@@ -89,18 +316,6 @@ class _SettingsPageState extends RebootPageState<SettingsPage> {
)) ))
); );
SettingTile get _resetDefaults => SettingTile(
icon: Icon(
FluentIcons.arrow_reset_24_regular
),
title: Text(translations.settingsUtilsResetDefaultsName),
subtitle: Text(translations.settingsUtilsResetDefaultsSubtitle),
content: Button(
onPressed: () => showResetDialog(_settingsController.reset),
child: Text(translations.settingsUtilsResetDefaultsContent),
)
);
SettingTile get _installationDirectory => SettingTile( SettingTile get _installationDirectory => SettingTile(
icon: Icon( icon: Icon(
FluentIcons.folder_24_regular FluentIcons.folder_24_regular
@@ -126,3 +341,13 @@ extension _ThemeModeExtension on ThemeMode {
} }
} }
} }
extension _UpdateTimerExtension on UpdateTimer {
String get text {
if (this == UpdateTimer.never) {
return translations.updateGameServerDllNever;
}
return translations.updateGameServerDllEvery(name);
}
}

View File

@@ -1,12 +1,47 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
const Duration _timeout = Duration(seconds: 5); const Duration _timeout = Duration(seconds: 5);
Future<bool> pingGameServer(String address, {Duration? timeout}) async { Completer<bool> pingGameServerOrTimeout(String address, Duration timeout) {
Future<bool> ping(String hostname, int port) async { final completer = Completer<bool>();
final start = DateTime.now();
_pingGameServerOrTimeout(completer, start, timeout, address);
return completer;
}
Future<void> _pingGameServerOrTimeout(Completer<bool> completer, DateTime start, Duration timeout, String address) async {
while (!completer.isCompleted && max(DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch, 0) < timeout.inMilliseconds) {
final result = await pingGameServer(address);
if(result) {
completer.complete(true);
}else {
await Future.delayed(_timeout);
}
}
if(!completer.isCompleted) {
completer.complete(false);
}
}
Future<bool> pingGameServer(String address) async {
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
hostname = "127.0.0.1";
}
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
return await _ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
}
Future<bool> _ping(String hostname, int port) async {
log("[MATCHMAKER] Pinging $hostname:$port"); log("[MATCHMAKER] Pinging $hostname:$port");
RawDatagramSocket? socket; RawDatagramSocket? socket;
try { try {
@@ -34,31 +69,4 @@ Future<bool> pingGameServer(String address, {Duration? timeout}) async {
}finally { }finally {
socket?.close(); socket?.close();
} }
}
final start = DateTime.now();
var firstTime = true;
final split = address.split(":");
var hostname = split[0];
if(isLocalHost(hostname)) {
hostname = "127.0.0.1";
}
final port = int.parse(split.length > 1 ? split[1] : kDefaultGameServerPort);
while (firstTime || (timeout != null && DateTime.now().millisecondsSinceEpoch - start.millisecondsSinceEpoch < timeout.inMilliseconds)) {
final result = await ping(hostname, port)
.timeout(_timeout, onTimeout: () => false);
if(result) {
return true;
}
if(firstTime) {
firstTime = false;
}else {
await Future.delayed(_timeout);
}
}
return false;
} }

View File

@@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:win32/win32.dart'; import 'package:win32/win32.dart';
import 'package:window_manager/window_manager.dart';
final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))'); final RegExp _winBuildRegex = RegExp(r'(?<=\(Build )(.*)(?=\))');
@@ -24,10 +25,13 @@ bool get isWin11 {
return intBuild != null && intBuild > 22000; return intBuild != null && intBuild > 22000;
} }
Future<String?> openFolderPicker(String title) async => Future<String?> openFolderPicker(String title) async {
await FilePicker.platform.getDirectoryPath(dialogTitle: title); FilePicker.platform = FilePickerWindows();
return await FilePicker.platform.getDirectoryPath(dialogTitle: title);
}
Future<String?> openFilePicker(String extension) async { Future<String?> openFilePicker(String extension) async {
FilePicker.platform = FilePickerWindows();
var result = await FilePicker.platform.pickFiles( var result = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowMultiple: false, allowMultiple: false,
@@ -93,7 +97,7 @@ class IVirtualDesktop extends IUnknown {
throw WindowsException(code); throw WindowsException(code);
} }
return convertFromHString(result.value); return _convertFromHString(result.value);
} }
} }
@@ -280,7 +284,7 @@ class _IVirtualDesktopManagerInternal extends IUnknown {
HRESULT Function(Pointer, COMObject, Int8)>>>() HRESULT Function(Pointer, COMObject, Int8)>>>()
.value .value
.asFunction<int Function(Pointer, COMObject, int)>()( .asFunction<int Function(Pointer, COMObject, int)>()(
ptr.ref.lpVtbl, desktop.ptr.ref, convertToHString(newName)); ptr.ref.lpVtbl, desktop.ptr.ref, _convertToHString(newName));
if (code != 0) { if (code != 0) {
throw WindowsException(code); throw WindowsException(code);
} }
@@ -369,7 +373,7 @@ List<int> _getHWnds(int pid, String? excludedWindowName) {
result.ref.excluded = excludedWindowName.toNativeUtf16(); result.ref.excluded = excludedWindowName.toNativeUtf16();
} }
EnumWindows(Pointer.fromFunction<EnumWindowsProc>(_filter, TRUE), result.address); EnumWindows(Pointer.fromFunction<WNDENUMPROC>(_filter, TRUE), result.address);
final length = result.ref.HWndLength; final length = result.ref.HWndLength;
final HWndsPointer = result.ref.HWnd; final HWndsPointer = result.ref.HWnd;
if(HWndsPointer == nullptr) { if(HWndsPointer == nullptr) {
@@ -397,7 +401,7 @@ class VirtualDesktopManager {
} }
final hr = CoInitializeEx( final hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); nullptr, COINIT.COINIT_APARTMENTTHREADED | COINIT.COINIT_DISABLE_OLE1DDE);
if (FAILED(hr)) { if (FAILED(hr)) {
throw WindowsException(hr); throw WindowsException(hr);
} }
@@ -468,3 +472,77 @@ class VirtualDesktopManager {
void setDesktopName(IVirtualDesktop desktop, String newName) => void setDesktopName(IVirtualDesktop desktop, String newName) =>
windowManager.setDesktopName(desktop, newName); windowManager.setDesktopName(desktop, newName);
} }
String _convertFromHString(int hstring) =>
WindowsGetStringRawBuffer(hstring, nullptr).toDartString();
int _convertToHString(String string) {
final hString = calloc<HSTRING>();
final stringPtr = string.toNativeUtf16();
try {
final hr = WindowsCreateString(stringPtr, string.length, hString);
if (FAILED(hr)) throw WindowsException(hr);
return hString.value;
} finally {
free(stringPtr);
free(hString);
}
}
extension WindowManagerExtension on WindowManager {
Future<void> maximizeOrRestore() async => await windowManager.isMaximized() ? windowManager.restore() : windowManager.maximize();
}
class WindowsDisk {
static final String _nullTerminator = String.fromCharCode(0);
final String path;
final int freeBytesAvailable;
final int totalNumberOfBytes;
const WindowsDisk._internal(this.path, this.freeBytesAvailable, this.totalNumberOfBytes);
static List<WindowsDisk> available() {
final buffer = malloc.allocate<Utf16>(MAX_PATH);
try {
final length = GetLogicalDriveStrings(MAX_PATH, buffer);
if (length == 0) {
return [];
}
return buffer.toDartString(length: length)
.split(_nullTerminator)
.where((drive) => drive.length > 1)
.map((driveName) {
final freeBytesAvailable = calloc<Uint64>();
final totalNumberOfBytes = calloc<Uint64>();
final totalNumberOfFreeBytes = calloc<Uint64>();
try {
GetDiskFreeSpaceEx(
driveName.toNativeUtf16(),
freeBytesAvailable,
totalNumberOfBytes,
totalNumberOfFreeBytes
);
return WindowsDisk._internal(
driveName,
freeBytesAvailable.value,
totalNumberOfBytes.value
);
} finally {
calloc.free(freeBytesAvailable);
calloc.free(totalNumberOfBytes);
calloc.free(totalNumberOfFreeBytes);
}
})
.toList(growable: false);
} finally {
calloc.free(buffer);
}
}
@override
String toString() {
return 'WindowsDisk{path: $path, freeBytesAvailable: $freeBytesAvailable, totalNumberOfBytes: $totalNumberOfBytes}';
}
}

View File

@@ -6,3 +6,14 @@ extension IterableExtension<E> on Iterable<E> {
return null; return null;
} }
} }
extension StringExtension on String {
String? after(String leading) {
final index = indexOf(leading);
if(index == -1) {
return null;
}
return substring(index + leading.length);
}
}

View File

@@ -0,0 +1,63 @@
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart';
final _hive = HKEY_CURRENT_USER;
void registerUrlProtocol(String scheme, {String? executable, List<String>? arguments}) {
final prefix = _regPrefix(scheme);
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
final args = _getArguments(arguments).map((a) => _sanitize(a));
final cmd =
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
_regCreateStringKey(_hive, prefix + '\\shell\\open\\command', '', cmd);
}
void unregisterUrlProtocol(String scheme) {
final txtKey = TEXT(_regPrefix(scheme));
try {
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
} finally {
free(txtKey);
}
}
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
final txtKey = TEXT(key);
final txtValue = TEXT(valueName);
final txtData = TEXT(data);
try {
return RegSetKeyValue(
hKey,
txtKey,
txtValue,
REG_VALUE_TYPE.REG_SZ,
txtData,
txtData.length * 2 + 2,
);
} finally {
free(txtKey);
free(txtValue);
free(txtData);
}
}
String _sanitize(String value) {
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
return '"$value"';
}
List<String> _getArguments(List<String>? arguments) {
if (arguments == null) return ['%s'];
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
}
return arguments;
}

View File

@@ -19,10 +19,10 @@ class FileSelector extends StatefulWidget {
required this.controller, required this.controller,
required this.validator, required this.validator,
required this.folder, required this.folder,
required this.allowNavigator,
this.label, this.label,
this.extension, this.extension,
this.validatorMode, this.validatorMode,
this.allowNavigator = true,
Key? key}) Key? key})
: assert(folder || extension != null, "Missing extension for file selector"), : assert(folder || extension != null, "Missing extension for file selector"),
super(key: key); super(key: key);

View File

@@ -1,27 +1,107 @@
import 'dart:io'; import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart' as fluentIcons show FluentIcons;
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons; import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:reboot_launcher/src/widget/file_selector.dart'; import 'package:reboot_launcher/src/widget/file_selector.dart';
import 'package:reboot_launcher/src/widget/setting_tile.dart'; import 'package:reboot_launcher/src/widget/setting_tile.dart';
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller}) => SettingTile( const double _kButtonDimensions = 30;
const double _kButtonSpacing = 8;
// FIXME: If the user clicks on the reset button, the text field checker won't be called
SettingTile createFileSetting({required String title, required String description, required TextEditingController controller, required void Function() onReset}) {
final obx = RxString(controller.text);
controller.addListener(() => obx.value = controller.text);
final selecting = RxBool(false);
return SettingTile(
icon: Icon( icon: Icon(
FluentIcons.document_24_regular FluentIcons.document_24_regular
), ),
title: Text(title), title: Text(title),
subtitle: Text(description), subtitle: Text(description),
content: FileSelector( contentWidth: SettingTile.kDefaultContentWidth + _kButtonDimensions,
content: Row(
children: [
Expanded(
child: FileSelector(
placeholder: translations.selectPathPlaceholder, placeholder: translations.selectPathPlaceholder,
windowTitle: translations.selectPathWindowTitle, windowTitle: translations.selectPathWindowTitle,
controller: controller, controller: controller,
validator: _checkDll, validator: _checkDll,
extension: "dll", extension: "dll",
folder: false, folder: false,
validatorMode: AutovalidateMode.always validatorMode: AutovalidateMode.always,
allowNavigator: false,
),
),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.selectFile,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: () => _onPressed(selecting, controller),
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
fluentIcons.FluentIcons.open_folder_horizontal
),
) )
); ),
),
)),
const SizedBox(width: _kButtonSpacing),
Obx(() => Padding(
padding: EdgeInsets.only(
bottom: _checkDll(obx.value) == null ? 0.0 : 20.0
),
child: Tooltip(
message: translations.reset,
child: Button(
style: ButtonStyle(
padding: WidgetStateProperty.all(EdgeInsets.zero)
),
onPressed: onReset,
child: SizedBox.square(
dimension: _kButtonDimensions,
child: Icon(
FluentIcons.arrow_reset_24_regular
),
)
),
),
))
],
)
);
}
void _onPressed(RxBool selecting, TextEditingController controller) {
if(selecting.value){
return;
}
selecting.value = true;
compute(openFilePicker, "dll")
.then((value) => _updateText(controller, value))
.then((_) => selecting.value = false);
}
void _updateText(TextEditingController controller, String? value) {
final text = value ?? controller.text;
controller.text = text;
controller.selection = TextSelection.collapsed(offset: text.length);
}
String? _checkDll(String? text) { String? _checkDll(String? text) {
if (text == null || text.isEmpty) { if (text == null || text.isEmpty) {

View File

@@ -9,19 +9,19 @@ import 'package:local_notifier/local_notifier.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/backend_controller.dart'; import 'package:reboot_launcher/src/controller/backend_controller.dart';
import 'package:reboot_launcher/src/controller/dll_controller.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart'; import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/controller/settings_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/dialog.dart'; import 'package:reboot_launcher/src/messenger/abstract/dialog.dart';
import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart'; import 'package:reboot_launcher/src/messenger/abstract/info_bar.dart';
import 'package:reboot_launcher/src/messenger/implementation/server.dart'; import 'package:reboot_launcher/src/messenger/implementation/server.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart'; import 'package:reboot_launcher/src/page/pages.dart';
import 'package:reboot_launcher/src/util/matchmaker.dart'; import 'package:reboot_launcher/src/util/matchmaker.dart';
import 'package:reboot_launcher/src/util/os.dart'; import 'package:reboot_launcher/src/util/os.dart';
import 'package:reboot_launcher/src/util/translations.dart'; import 'package:reboot_launcher/src/util/translations.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:version/version.dart';
class LaunchButton extends StatefulWidget { class LaunchButton extends StatefulWidget {
final bool host; final bool host;
@@ -40,11 +40,12 @@ class _LaunchButtonState extends State<LaunchButton> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>(); final HostingController _hostingController = Get.find<HostingController>();
final BackendController _backendController = Get.find<BackendController>(); final BackendController _backendController = Get.find<BackendController>();
final SettingsController _settingsController = Get.find<SettingsController>(); final DllController _dllController = Get.find<DllController>();
InfoBarEntry? _gameClientInfoBar; InfoBarEntry? _gameClientInfoBar;
InfoBarEntry? _gameServerInfoBar; InfoBarEntry? _gameServerInfoBar;
CancelableOperation? _operation; CancelableOperation? _operation;
Completer? _pingOperation;
IVirtualDesktop? _virtualDesktop; IVirtualDesktop? _virtualDesktop;
@override @override
@@ -93,11 +94,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Set started"); log("[${host ? 'HOST' : 'GAME'}] Set started");
log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}"); log("[${host ? 'HOST' : 'GAME'}] Checking dlls: ${InjectableDll.values}");
for (final injectable in InjectableDll.values) { for (final injectable in InjectableDll.values) {
if(await _getDllFileOrStop(injectable, host) == null) { if(await _getDllFileOrStop(version.content, injectable, host) == null) {
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return; return;
} }
} }
@@ -139,6 +136,12 @@ class _LaunchButtonState extends State<LaunchButton> {
}else { }else {
_showLaunchingGameServerWidget(); _showLaunchingGameServerWidget();
} }
} on ProcessException catch (exception, stackTrace) {
_onStop(
reason: _StopReason.corruptedVersionError,
error: exception.toString(),
stackTrace: stackTrace
);
} catch (exception, stackTrace) { } catch (exception, stackTrace) {
_onStop( _onStop(
reason: _StopReason.unknownError, reason: _StopReason.unknownError,
@@ -226,7 +229,7 @@ class _LaunchButtonState extends State<LaunchButton> {
log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}"); log("[${host ? 'HOST' : 'GAME'}] Created game process: ${gameProcess}");
final instance = GameInstance( final instance = GameInstance(
versionName: version.content.toString(), version: version.content,
gamePid: gameProcess, gamePid: gameProcess,
launcherPid: launcherProcess, launcherPid: launcherProcess,
eacPid: eacProcess, eacPid: eacProcess,
@@ -239,7 +242,7 @@ class _LaunchButtonState extends State<LaunchButton> {
}else{ }else{
_gameController.instance.value = instance; _gameController.instance.value = instance;
} }
await _injectOrShowError(InjectableDll.cobalt, host); await _injectOrShowError(InjectableDll.starfall, host);
log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance"); log("[${host ? 'HOST' : 'GAME'}] Finished creating game instance");
return instance; return instance;
} }
@@ -247,12 +250,12 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async { Future<int?> _createGameProcess(FortniteVersion version, File executable, bool host, GameServerType hostType, GameInstance? linkedHosting) async {
log("[${host ? 'HOST' : 'GAME'}] Generating instance args..."); log("[${host ? 'HOST' : 'GAME'}] Generating instance args...");
final gameArgs = createRebootArgs( final gameArgs = createRebootArgs(
_gameController.username.text, host ? _hostingController.accountUsername.text : _gameController.username.text,
_gameController.password.text, host ? _hostingController.accountPassword.text :_gameController.password.text,
host, host,
hostType, hostType,
false, false,
"" host ? _hostingController.customLaunchArgs.text : _gameController.customLaunchArgs.text
); );
log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}"); log("[${host ? 'HOST' : 'GAME'}] Generated game args: ${gameArgs.join(" ")}");
final gameProcess = await startProcess( final gameProcess = await startProcess(
@@ -264,15 +267,23 @@ class _LaunchButtonState extends State<LaunchButton> {
"OPENSSL_ia32cap": "~0x20000000" "OPENSSL_ia32cap": "~0x20000000"
} }
); );
final instance = host ? _hostingController.instance.value : _gameController.instance.value;
void onGameOutput(String line, bool error) { void onGameOutput(String line, bool error) {
log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line"); log("[${host ? 'HOST' : 'GAME'}] ${error ? '[ERROR]' : '[MESSAGE]'} $line");
handleGameOutput( handleGameOutput(
line: line, line: line,
host: host, host: host,
onShutdown: () => _onStop(reason: _StopReason.normal), onShutdown: () => _onStop(reason: _StopReason.normal),
onTokenError: () => _onStop(reason: _StopReason.tokenError), onTokenError: () => _onStop(reason: _StopReason.tokenError),
onBuildCorrupted: () => _onStop(reason: _StopReason.corruptedVersionError), onBuildCorrupted: () {
if(instance == null) {
return;
}else if(!instance.launched) {
_onStop(reason: _StopReason.corruptedVersionError);
}else {
_onStop(reason: _StopReason.crash);
}
},
onLoggedIn: () =>_onLoggedIn(host), onLoggedIn: () =>_onLoggedIn(host),
onMatchEnd: () => _onMatchEnd(version), onMatchEnd: () => _onMatchEnd(version),
onDisplayAttached: () => _onDisplayAttached(host, hostType, version) onDisplayAttached: () => _onDisplayAttached(host, hostType, version)
@@ -387,12 +398,11 @@ class _LaunchButtonState extends State<LaunchButton> {
if(instance != null && !instance.launched) { if(instance != null && !instance.launched) {
instance.launched = true; instance.launched = true;
instance.tokenError = false; instance.tokenError = false;
await _injectOrShowError(InjectableDll.memory, host);
if(!host){ if(!host){
await _injectOrShowError(InjectableDll.console, host); await _injectOrShowError(InjectableDll.console, host);
_onGameClientInjected(); _onGameClientInjected();
}else { }else {
final gameServerPort = int.tryParse(_settingsController.gameServerPort.text); final gameServerPort = int.tryParse(_dllController.gameServerPort.text);
if(gameServerPort != null) { if(gameServerPort != null) {
await killProcessByPort(gameServerPort); await killProcessByPort(gameServerPort);
} }
@@ -425,11 +435,13 @@ class _LaunchButtonState extends State<LaunchButton> {
loading: true, loading: true,
duration: null duration: null
); );
final gameServerPort = _settingsController.gameServerPort.text; final gameServerPort = _dllController.gameServerPort.text;
final localPingResult = await pingGameServer( final pingOperation = pingGameServerOrTimeout(
"127.0.0.1:$gameServerPort", "127.0.0.1:$gameServerPort",
timeout: const Duration(minutes: 2) const Duration(minutes: 2)
); );
this._pingOperation = pingOperation;
final localPingResult = await pingOperation.future;
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
if (!localPingResult) { if (!localPingResult) {
showRebootInfoBar( showRebootInfoBar(
@@ -451,8 +463,8 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
await _hostingController.publishServer( await _hostingController.publishServer(
_gameController.username.text, _hostingController.accountUsername.text,
_hostingController.instance.value!.versionName, _hostingController.instance.value!.version.toString(),
); );
showRebootInfoBar( showRebootInfoBar(
translations.gameServerStarted, translations.gameServerStarted,
@@ -472,16 +484,17 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null duration: null
); );
final publicIp = await Ipify.ipv4(); final publicIp = await Ipify.ipv4();
final externalResult = await pingGameServer("$publicIp:$gameServerPort"); final available = await pingGameServer("$publicIp:$gameServerPort");
if (externalResult) { if(available) {
_gameServerInfoBar?.close();
return true; return true;
} }
_gameServerInfoBar?.close(); final pingOperation = pingGameServerOrTimeout(
final future = pingGameServer(
"$publicIp:$gameServerPort", "$publicIp:$gameServerPort",
timeout: const Duration(days: 365) const Duration(days: 1)
); );
this._pingOperation = pingOperation;
_gameServerInfoBar = showRebootInfoBar( _gameServerInfoBar = showRebootInfoBar(
translations.checkGameServerFixMessage(gameServerPort), translations.checkGameServerFixMessage(gameServerPort),
action: Button( action: Button(
@@ -492,7 +505,9 @@ class _LaunchButtonState extends State<LaunchButton> {
duration: null, duration: null,
loading: true loading: true
); );
return await future; final result = await pingOperation.future;
_gameServerInfoBar?.close();
return result;
}finally { }finally {
_gameServerInfoBar?.close(); _gameServerInfoBar?.close();
} }
@@ -500,12 +515,21 @@ class _LaunchButtonState extends State<LaunchButton> {
Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async { Future<void> _onStop({required _StopReason reason, bool? host, String? error, StackTrace? stackTrace}) async {
if(host == null) { if(host == null) {
try {
_pingOperation?.complete(false);
}catch(_) {
// Ignore: might be running, don't bother checking
} finally {
_pingOperation = null;
}
await _operation?.cancel(); await _operation?.cancel();
_operation = null; _operation = null;
_backendController.cancelInteractive();
} }
host = host ?? widget.host; host = host ?? widget.host;
final instance = host ? _hostingController.instance.value : _gameController.instance.value; final instance = host ? _hostingController.instance.value : _gameController.instance.value;
if(host){ if(host){
_hostingController.instance.value = null; _hostingController.instance.value = null;
}else { }else {
@@ -527,20 +551,18 @@ class _LaunchButtonState extends State<LaunchButton> {
_hostingController.discardServer(); _hostingController.discardServer();
} }
if(instance != null) {
if(reason == _StopReason.normal) { if(reason == _StopReason.normal) {
instance.launched = true; instance?.launched = true;
} }
instance.kill(); instance?.kill();
final child = instance.child; final child = instance?.child;
if(child != null) { if(child != null) {
await _onStop( await _onStop(
reason: reason, reason: reason,
host: child.serverType != null host: child.serverType != null
); );
} }
}
_setStarted(host, false); _setStarted(host, false);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -584,6 +606,10 @@ class _LaunchButtonState extends State<LaunchButton> {
translations.corruptedVersionError, translations.corruptedVersionError,
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button(
onPressed: () => launchUrl(launcherLogFile.uri),
child: Text(translations.openLog),
)
); );
break; break;
case _StopReason.corruptedDllError: case _StopReason.corruptedDllError:
@@ -601,8 +627,9 @@ class _LaunchButtonState extends State<LaunchButton> {
); );
break; break;
case _StopReason.tokenError: case _StopReason.tokenError:
_backendController.stop();
showRebootInfoBar( showRebootInfoBar(
translations.tokenError(instance?.injectedDlls.map((element) => element.name).join(", ") ?? translations.none), translations.tokenError(instance == null ? translations.none : instance.injectedDlls.map((element) => element.name).join(", ")),
severity: InfoBarSeverity.error, severity: InfoBarSeverity.error,
duration: infoBarLongDuration, duration: infoBarLongDuration,
action: Button( action: Button(
@@ -611,6 +638,13 @@ class _LaunchButtonState extends State<LaunchButton> {
) )
); );
break; break;
case _StopReason.crash:
showRebootInfoBar(
translations.fortniteCrashError(host ? "game server" : "client"),
severity: InfoBarSeverity.error,
duration: infoBarLongDuration,
);
break;
case _StopReason.unknownError: case _StopReason.unknownError:
showRebootInfoBar( showRebootInfoBar(
translations.unknownFortniteError(error ?? translations.unknownError), translations.unknownFortniteError(error ?? translations.unknownError),
@@ -631,7 +665,7 @@ class _LaunchButtonState extends State<LaunchButton> {
try { try {
final gameProcess = instance.gamePid; final gameProcess = instance.gamePid;
log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess"); log("[${hosting ? 'HOST' : 'GAME'}] Injecting ${injectable.name} into process with pid $gameProcess");
final dllPath = await _getDllFileOrStop(injectable, hosting); final dllPath = await _getDllFileOrStop(instance.version, injectable, hosting);
log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath"); log("[${hosting ? 'HOST' : 'GAME'}] File to inject for ${injectable.name} at path $dllPath");
if(dllPath == null) { if(dllPath == null) {
log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist"); log("[${hosting ? 'HOST' : 'GAME'}] The file doesn't exist");
@@ -658,9 +692,9 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
} }
Future<File?> _getDllFileOrStop(InjectableDll injectable, bool host, [bool isRetry = false]) async { Future<File?> _getDllFileOrStop(Version version, InjectableDll injectable, bool host, [bool isRetry = false]) async {
log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}..."); log("[${host ? 'HOST' : 'GAME'}] Checking dll ${injectable}...");
final (file, customDll) = _settingsController.getInjectableData(injectable); final (file, customDll) = _dllController.getInjectableData(version, injectable);
log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll"); log("[${host ? 'HOST' : 'GAME'}] Path: ${file.path}, custom: $customDll");
if(await file.exists()) { if(await file.exists()) {
log("[${host ? 'HOST' : 'GAME'}] Path exists"); log("[${host ? 'HOST' : 'GAME'}] Path exists");
@@ -668,15 +702,19 @@ class _LaunchButtonState extends State<LaunchButton> {
} }
log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist"); log("[${host ? 'HOST' : 'GAME'}] Path doesn't exist");
if(customDll || isRetry) { if(customDll) {
log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery"); log("[${host ? 'HOST' : 'GAME'}] Custom dll -> no recovery");
_onStop(
reason: _StopReason.missingCustomDllError,
error: injectable.name,
);
return null; return null;
} }
log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again..."); log("[${host ? 'HOST' : 'GAME'}] Path does not exist, downloading critical dll again...");
await _settingsController.downloadCriticalDllInteractive(file.path); await _dllController.downloadCriticalDllInteractive(file.path, force: true);
log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check..."); log("[${host ? 'HOST' : 'GAME'}] Downloaded dll again, retrying check...");
return _getDllFileOrStop(injectable, host, true); return _getDllFileOrStop(version, injectable, host, true);
} }
InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar( InfoBarEntry _showLaunchingGameServerWidget() => _gameServerInfoBar = showRebootInfoBar(
@@ -727,7 +765,8 @@ enum _StopReason {
matchmakerError, matchmakerError,
tokenError, tokenError,
unknownError, unknownError,
exitCode; exitCode,
crash;
bool get isError => name.contains("Error"); bool get isError => name.contains("Error");
} }

View File

@@ -24,7 +24,9 @@ class InfoBarAreaState extends State<InfoBarArea> {
} }
@override @override
Widget build(BuildContext context) => Obx(() => Padding( Widget build(BuildContext context) => StreamBuilder(
stream: pagesController.stream,
builder: (context, _) => Obx(() => Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: hasPageButton ? 72.0 : 16.0 bottom: hasPageButton ? 72.0 : 16.0
), ),
@@ -38,5 +40,6 @@ class InfoBarAreaState extends State<InfoBarArea> {
child: child child: child
)).toList(growable: false) )).toList(growable: false)
), ),
)); ))
);
} }

View File

@@ -2,8 +2,11 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:reboot_common/common.dart'; import 'package:reboot_common/common.dart';
import 'package:reboot_launcher/src/controller/game_controller.dart'; import 'package:reboot_launcher/src/controller/game_controller.dart';
import 'package:reboot_launcher/src/controller/hosting_controller.dart';
import 'package:reboot_launcher/src/messenger/abstract/overlay.dart'; import 'package:reboot_launcher/src/messenger/abstract/overlay.dart';
import 'package:reboot_launcher/src/messenger/implementation/profile.dart'; import 'package:reboot_launcher/src/messenger/implementation/profile.dart';
import 'package:reboot_launcher/src/page/abstract/page_type.dart';
import 'package:reboot_launcher/src/page/pages.dart';
class ProfileWidget extends StatefulWidget { class ProfileWidget extends StatefulWidget {
final GlobalKey<OverlayTargetState> overlayKey; final GlobalKey<OverlayTargetState> overlayKey;
@@ -15,6 +18,7 @@ class ProfileWidget extends StatefulWidget {
class _ProfileWidgetState extends State<ProfileWidget> { class _ProfileWidgetState extends State<ProfileWidget> {
final GameController _gameController = Get.find<GameController>(); final GameController _gameController = Get.find<GameController>();
final HostingController _hostingController = Get.find<HostingController>();
@override @override
Widget build(BuildContext context) => OverlayTarget( Widget build(BuildContext context) => OverlayTarget(
@@ -22,7 +26,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
child: HoverButton( child: HoverButton(
margin: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0),
onPressed: () async { onPressed: () async {
if(await showProfileForm(context)) { if(await showProfileForm(context, _username, _password)) {
setState(() {}); setState(() {});
} }
}, },
@@ -57,7 +61,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_username, _usernameLabel,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w600 fontWeight: FontWeight.w600
@@ -65,7 +69,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
maxLines: 1 maxLines: 1
), ),
Text( Text(
_email, _emailLabel,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w100 fontWeight: FontWeight.w100
@@ -81,8 +85,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
), ),
); );
String get _username { String get _usernameLabel {
var username = _gameController.username.text; final username = _username.text;
if(username.isEmpty) { if(username.isEmpty) {
return kDefaultPlayerName; return kDefaultPlayerName;
} }
@@ -96,8 +100,8 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return result.substring(0, 1).toUpperCase() + result.substring(1); return result.substring(0, 1).toUpperCase() + result.substring(1);
} }
String get _email { String get _emailLabel {
var username = _gameController.username.text; final username = _username.text;
if(username.isEmpty) { if(username.isEmpty) {
return "$kDefaultPlayerName@projectreboot.dev"; return "$kDefaultPlayerName@projectreboot.dev";
} }
@@ -108,4 +112,7 @@ class _ProfileWidgetState extends State<ProfileWidget> {
return "$username@projectreboot.dev".toLowerCase(); return "$username@projectreboot.dev".toLowerCase();
} }
TextEditingController get _username => pageIndex.value == RebootPageType.host.index ? _hostingController.accountUsername : _gameController.username;
TextEditingController get _password => pageIndex.value == RebootPageType.host.index ? _hostingController.accountPassword : _gameController.password;
} }

View File

@@ -1,5 +1,6 @@
import 'package:bitsdojo_window/bitsdojo_window.dart' show appWindow;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:reboot_launcher/src/util/os.dart';
import 'package:window_manager/window_manager.dart';
import 'title_bar_icons.dart'; import 'title_bar_icons.dart';
import 'title_bar_mouse.dart'; import 'title_bar_mouse.dart';
@@ -132,7 +133,7 @@ class MinimizeWindowButton extends WindowButton {
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
MinimizeIcon(color: buttonContext.iconColor), MinimizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => appWindow.minimize()); onPressed: onPressed ?? () => windowManager.minimize());
} }
class MaximizeWindowButton extends WindowButton { class MaximizeWindowButton extends WindowButton {
@@ -148,7 +149,7 @@ class MaximizeWindowButton extends WindowButton {
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
MaximizeIcon(color: buttonContext.iconColor), MaximizeIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? onPressed: onPressed ??
() => appWindow.maximizeOrRestore()); () => windowManager.maximizeOrRestore());
} }
final _defaultCloseButtonColors = WindowButtonColors( final _defaultCloseButtonColors = WindowButtonColors(
@@ -169,5 +170,5 @@ class CloseWindowButton extends WindowButton {
animate: animate ?? false, animate: animate ?? false,
iconBuilder: (buttonContext) => iconBuilder: (buttonContext) =>
CloseIcon(color: buttonContext.iconColor), CloseIcon(color: buttonContext.iconColor),
onPressed: onPressed ?? () => appWindow.close()); onPressed: onPressed ?? () => windowManager.close());
} }

View File

@@ -15,11 +15,11 @@ import 'package:url_launcher/url_launcher.dart';
class VersionSelector extends StatefulWidget { class VersionSelector extends StatefulWidget {
const VersionSelector({Key? key}) : super(key: key); const VersionSelector({Key? key}) : super(key: key);
static Future<void> openDownloadDialog({bool closable = true}) => showRebootDialog<bool>( static Future<void> openDownloadDialog() => showRebootDialog<bool>(
builder: (context) => AddVersionDialog( builder: (context) => AddVersionDialog(
closable: closable, closable: true,
), ),
dismissWithEsc: closable dismissWithEsc: true
); );
@override @override

View File

@@ -1,6 +1,6 @@
name: reboot_launcher name: reboot_launcher
description: Graphical User Interface for Project Reboot description: Graphical User Interface for Project Reboot
version: "9.2.0" version: "10.0.3"
publish_to: 'none' publish_to: 'none'
@@ -17,53 +17,48 @@ dependencies:
path: ./../common path: ./../common
# Windows UI 3 # Windows UI 3
fluent_ui: ^4.8.7 fluent_ui: ^4.9.1
flutter_acrylic: flutter_acrylic:
path: ./dependencies/flutter_acrylic path: ./dependencies/flutter_acrylic
fluentui_system_icons: ^1.1.238 fluentui_system_icons: ^1.1.258
system_theme: ^2.0.0 system_theme: ^3.1.1
skeletons: skeletons:
git: git:
url: https://github.com/talok/skeletons url: https://github.com/talok/skeletons
ref: main ref: main
# Window management # Window management
bitsdojo_window: ^0.1.5 window_manager: ^0.4.2
window_manager: ^0.3.8
# Extract zip archives (for example the reboot.zip) # Extract zip archives (for example the reboot.zip)
archive: ^3.3.1 archive: ^3.6.1
# Cryptographic functions # Cryptographic functions
crypto: ^3.0.2
bcrypt: ^1.1.3 bcrypt: ^1.1.3
pointycastle: ^3.7.3 pointycastle: ^3.9.1
# Async helpers # Async helpers
async: ^2.8.2 async: ^2.11.0
sync: ^0.3.0 sync: ^0.3.0
# State management # State management
get: ^4.6.5 get: ^4.6.6
# Native utilities # Native utilities
clipboard: ^0.1.3 clipboard: ^0.1.3
app_links: ^6.0.2 app_links: ^6.3.2
url_protocol: ^1.0.0
windows_taskbar: ^1.1.2 windows_taskbar: ^1.1.2
file_picker: ^8.0.3 file_picker: ^8.1.2
url_launcher: ^6.1.5 url_launcher: ^6.3.0
local_notifier: ^0.1.6 local_notifier: ^0.1.6
# Server browser # Server browser
supabase_flutter: ^2.5.2 supabase_flutter: ^2.7.0
uuid: ^3.0.6
dart_ipify: ^1.1.1 dart_ipify: ^1.1.1
# Storage # Storage
get_storage: ^2.0.3 get_storage: ^2.1.1
universal_disk_space: ^0.2.3 path: ^1.9.0
path: ^1.8.3
# Translations # Translations
intl: any intl: any
@@ -71,20 +66,17 @@ dependencies:
# Auto updater # Auto updater
yaml: ^3.1.2 yaml: ^3.1.2
package_info_plus: ^8.0.0 package_info_plus: ^8.0.2
version: ^3.0.2 version: ^3.0.2
dependency_overrides: # Validate profile
xml: ^6.3.0 email_validator: ^3.0.0
http: ^0.13.5
win32: ^3.0.0
ffi: ^2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^4.0.0 flutter_lints: ^5.0.0
flutter: flutter:
uses-material-design: true uses-material-design: true
@@ -98,4 +90,9 @@ flutter:
- assets/backend/profiles/ - assets/backend/profiles/
- assets/backend/public/ - assets/backend/public/
- assets/backend/responses/ - assets/backend/responses/
- assets/backend/responses/Athena/
- assets/backend/responses/Athena/BattlePass/
- assets/backend/responses/Athena/Discovery/
- assets/backend/responses/Campaign/
- assets/backend/responses/CloudDir/
- assets/build/ - assets/build/

View File

@@ -7,7 +7,6 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <local_notifier/local_notifier_plugin.h> #include <local_notifier/local_notifier_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
@@ -19,8 +18,6 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar( AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi")); registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
FlutterAcrylicPluginRegisterWithRegistrar( FlutterAcrylicPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
LocalNotifierPluginRegisterWithRegistrar( LocalNotifierPluginRegisterWithRegistrar(

View File

@@ -4,7 +4,6 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
bitsdojo_window_windows
flutter_acrylic flutter_acrylic
local_notifier local_notifier
screen_retriever screen_retriever

View File

@@ -1,7 +1,3 @@
#define public Dependency_Path_NetCoreCheck "..\..\dependencies\InnoDependencyInstaller\"
#include "..\..\dependencies\InnoDependencyInstaller\CodeDependencies.iss"
[Setup] [Setup]
AppId={{APP_ID}} AppId={{APP_ID}}
AppVersion={{APP_VERSION}} AppVersion={{APP_VERSION}}
@@ -20,10 +16,14 @@ PrivilegesRequired=admin
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64
ChangesEnvironment=yes ChangesEnvironment=yes
SetupLogging=yes
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
[CustomMessages]
InstallingVC2017redist=Installing Visual C++ Redistributable
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkedonce
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
@@ -33,10 +33,12 @@ Name: "{app}"; Permissions: everyone-full
[Files] [Files]
Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full Source: "{{SOURCE_DIR}}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Permissions: everyone-full
Source: "..\..\dependencies\redist\VC_redist.x64.exe"; DestDir: {tmp}; Flags: dontcopy
[Run] [Run]
Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden Filename: "powershell.exe"; Parameters: "-ExecutionPolicy Bypass -Command ""Add-MpPreference -ExclusionPath '{app}'"""; Flags: runhidden
Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent Filename: "{app}\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: runascurrentuser nowait postinstall skipifsilent
Filename: "{tmp}\VC_redist.x64.exe"; StatusMsg: "{cm:InstallingVC2017redist}"; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
[Icons] [Icons]
Name: "{autoprograms}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}" Name: "{autoprograms}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"
@@ -44,13 +46,43 @@ Name: "{autodesktop}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; T
Name: "{userstartup}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup Name: "{userstartup}\{{DISPLAY_NAME}}"; Filename: "{app}\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup
[Code] [Code]
function InitializeSetup: Boolean; function CompareVersion(version1, version2: String): Integer;
var
packVersion1, packVersion2: Int64;
begin begin
Dependency_AddVC2015To2022 if not StrToVersion(version1, packVersion1) then packVersion1 := 0;
Result := True; if not StrToVersion(version2, packVersion2) then packVersion2 := 0;
Result := ComparePackedVersion(packVersion1, packVersion2);
end;
function BoolToStr(Value: Boolean): String;
begin
if Value then
Result := 'Yes'
else
Result := 'No';
end;
function VC2017RedistNeedsInstall: Boolean;
var
Version: String;
begin
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', Version) then
begin
Result := (CompareVersion(Copy(Version, 2, Length(Version)), '14.40.33810.00') < 0);
end
else
begin
Result := True;
end;
Log('Visual C++ Redistributable version: ' + Version);
Log('Needs installation? ' + BoolToStr(Result));
if (Result) then
begin
ExtractTemporaryFile('VC_redist.x64.exe');
end;
end; end;
[Registry] [Registry]
Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName: "OPENSSL_ia32cap"; \ Root: HKCU; Subkey: "Environment"; ValueType:string; ValueName: "OPENSSL_ia32cap"; ValueData: "~0x20000000"; Flags: preservestringtype
ValueData: "~0x20000000"; Flags: preservestringtype

View File

@@ -1,6 +1,3 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
#include <cstdlib> #include <cstdlib>
#include <flutter/dart_project.h> #include <flutter/dart_project.h>

View File

@@ -121,7 +121,7 @@ bool Win32Window::CreateAndShow(const std::wstring &title,
HWND window = CreateWindow( HWND window = CreateWindow(
window_class, window_class,
title.c_str(), title.c_str(),
WS_OVERLAPPED | WS_BORDER | WS_THICKFRAME, WS_OVERLAPPEDWINDOW,
Scale(origin.x, scale_factor), Scale(origin.x, scale_factor),
Scale(origin.y, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.width, scale_factor),
@@ -198,6 +198,9 @@ Win32Window::MessageHandler(HWND hwnd,
SetFocus(child_content_); SetFocus(child_content_);
} }
return 0; return 0;
case WM_NCCALCSIZE:
return 0;
} }
return DefWindowProc(window_handle_, message, wparam, lparam); return DefWindowProc(window_handle_, message, wparam, lparam);